如何令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,有点累了,后面有时间再写

参考资料

Introduction | Shizuku


如何令Root进程持久而优雅
https://sunight.cn/archives/zGRtqXta
作者
Sunight
发布于
2025年02月02日
许可协议