如何令Root进程持久而优雅
前言
当我们的应用程序需要使用Root权限时,必不可少的要实现基本的检测与申请操作,并设置相应的标志表示该应用程序已获取Root权限
然而该应用程序是否从真正意义上获取了Root权限?这是个值得思索的问题。
传统做法
对于用户的设备有无Root权限,我们常常通过判断/system/bin目录下是否存在su文件来实现,某些市面上的APP(例如滴滴出行)甚至通过执行su命令使安装有Magisk的用户的设备上弹出授权弹窗——这使得他们惊恐万分。不过这些都不重要,作为开发者,如何保证应用合理的判断并使用Root权限是我们的首要目标。我们可以构造一位闲得蛋疼的用户(具体怎么蛋疼的,且往后看),他在进入APP之前于超级用户管理器内给予了其Root权限,这使得他在进入APP后,一切有关该设备上是否存在su文件或su命令的判断都是可行的,首页的TextView也许正自信的展示着"已获取Root权限”的字样。然而这位用户出于某种原因(具体什么原因我们不得而知)“懊悔”了,竟是回到自己的管理器撤销了Root授权,发誓自己从来没有进行过此类授权。然而,我们的App依然是正在运行的,内存中还保留着一切已获取Root权限的“证据”,意想不到的错误便在此刻发生。
对于“使用Root权限执行shell命令”,通常写法为:
public static void excCommend(String cmd) {
Process process = null;
try {
process = Runtime.getRuntime().exec("su"); //1、执行su切换到root权限
DataOutputStream dos = new DataOutputStream(ps.getOutputStream());
dos.writeBytes(cmd + "\n"); // 2、向进程内写入shell指令,cmd为要执行的shell命令字符串
dos.flush();
process.waitFor();
} catch (Exception ignored) {
} finally {
if (process != null) {
process.destroy();
}
}
}
这里我们创建了一个Process(英译‘进程’) ,并向其中写入shell命令,每次调(diao)用该方法,便意味着新进程的建立,这不仅为Shizuku所唾弃,也并不较为稳定,因为命令能否被执行取决于Root进程是否建立,而Root进程能否被建立则取决于管理器内的授权是否被准允、su是否可用,此时前面所做的判断似乎便显得有些抽离,不是那么的优雅。上一秒su文件还在,下一秒谁知道呢?
Each of the "Execute" means a new process creation, su internally uses sockets to interact with the su daemon, and a lot of time and performance are consumed in such process. (Some poorly designed app will even execute su every time for each command)
su 内部使用 socket 与 su daemon 交互,大量的时间和性能被消耗在这样的过程中。(部分设计不佳的应用甚至会每次执行指令都执行一次 su)
不管是为了节约性能也好,还是为了应对蛋疼哥也罢,每次运行命令前检查su文件是否存在并创建新进程去执行一条简单到可怜的shell并不是一个优雅的解决方案(Great solution),我们倡议,尽量减少新进程的建立,以减少资源的消耗与错误的发生,促进良好编码习惯的养成,最终促进整个社区生态的进化(咳咳,扯远了)
改进方案
进程一旦被创建,它的uid便是固定的,无法被轻易更改,可以理解为对应用授予Root权限,本质上是授予应用对Root进程的创建权,而不是通俗意义上的使用权,一个独立自主的man(已被建立的进程),在生命走向尽头之前,永远在属于自己的岗位上,意味着我们可以在该进程被创建后随意去撤销授权,而不会影响后续的rm -rf /*在我们的设备上被执行。所以前面的“已获取Root权限”在我们看来便成为了枯燥的文字,上一秒还在嘻嘻,下一秒随时有可能不嘻嘻。我们要做的,便是复用同一Root进程,令其一直做我们想要做的事,并将该进程的创建与否应用于是否获取Root权限的标志,直到应用被结束。
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class ShellUtils {
// 异步执行线程和通信
private HandlerThread mHandlerThread;
private Handler mBackgroundHandler;
private Handler mMainHandler = new Handler(Looper.getMainLooper());
// SU 进程相关
private Process mSuProcess;
private DataOutputStream mOutputStream;
private BufferedReader mInputStreamReader;
private BufferedReader mErrorStreamReader;
// 结果回调接口
public interface CommandCallback {
void onOutput(String line);
void onError(String error);
void onComplete();
}
public ShellUtils() {
// 创建后台线程
mHandlerThread = new HandlerThread("ShellUtils");
mHandlerThread.start();
mBackgroundHandler = new Handler(mHandlerThread.getLooper());
// 初始化 SU 进程
mBackgroundHandler.post(new Runnable() {
@Override
public void run() {
try {
mSuProcess = Runtime.getRuntime().exec("su");
mOutputStream = new DataOutputStream(mSuProcess.getOutputStream());
mInputStreamReader = new BufferedReader(new InputStreamReader(mSuProcess.getInputStream()));
mErrorStreamReader = new BufferedReader(new InputStreamReader(mSuProcess.getErrorStream()));
// 启动独立的线程来读取输入流和错误流
startStreamReader(mInputStreamReader, true);
startStreamReader(mErrorStreamReader, false);
} catch (IOException e) {
notifyError("Failed to start SU process", null);
}
}
});
}
/**
* 启动独立的线程读取流
*
* @param reader BufferedReader 对象
* @param isInput 是否为输入流(true 为输入流,false 为错误流)
*/
private void startStreamReader(final BufferedReader reader, final boolean isInput) {
new Thread(new Runnable() {
@Override
public void run() {
try {
String line;
while ((line = reader.readLine()) != null) {
if (isInput) {
notifyOutput(line);
} else {
notifyError(line, null);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
/**
* 异步执行命令
*
* @param command 需要执行的命令
* @param callback 结果回调(在主线程触发)
*/
public void executeCommandAsync(final String command, final CommandCallback callback) {
mBackgroundHandler.post(new Runnable() {
@Override
public void run() {
try {
// 写入命令
mOutputStream.writeBytes(command + "\n");
mOutputStream.flush();
// 通知命令完成
mMainHandler.post(new Runnable() {
@Override
public void run() {
callback.onComplete();
}
});
} catch (IOException e) {
notifyError("Command execution failed", callback);
}
}
});
}
/**
* 安全关闭资源
*/
public void destroy() {
mBackgroundHandler.post(new Runnable() {
@Override
public void run() {
try {
if (mOutputStream != null) {
mOutputStream.writeBytes("exit\n");
mOutputStream.flush();
mOutputStream.close();
}
if (mInputStreamReader != null) mInputStreamReader.close();
if (mErrorStreamReader != null) mErrorStreamReader.close();
if (mSuProcess != null) mSuProcess.destroy();
} catch (IOException e) {
e.printStackTrace();
}
mHandlerThread.quitSafely();
}
});
}
private void notifyOutput(final String line) {
mMainHandler.post(new Runnable() {
@Override
public void run() {
// 可以通过全局回调或事件总线传递输出
Log.d("ShellUtils", "Output: " + line);
}
});
}
private void notifyError(final String error, final CommandCallback callback) {
mMainHandler.post(new Runnable() {
@Override
public void run() {
if (callback != null) {
callback.onError(error);
callback.onComplete();
} else {
Log.e("ShellUtils", "Error: " + error);
}
}
});
}
}
当前时间凌晨3:12,有点累了,后面有时间再写