又是前言
上一篇的 Shizuku 源码分析,我大概从我们开发的应用到 ShizukuService 再到 SystemService 都通了一遍。在文章的结尾我只提到了 moe.shizuku.service 包下的 Starter 这个 Java 类的 main 方法启动了整个 ShizukuService ,所以我们才能愉快地调用 Shizuku 的 API。
我们接下来要更深入分析,去 Navtive 层看看。我想要知道 Starter 的 main 方法又是谁调用的?怎么调用的?
Shizuku 是什么?我就不再多说了。
Github: https://github.com/RikkaApps/Shizuku
(之后会简单涉及到 Android 中的 NDK 开发,和一些 C++ 的内容)
start.sh
当用户使用 ShizukuManager ,通过 ShizukuManager 激活应用时,需要先启动 ShizukuService 。
这里有 adb 和 root 两种启动方式,我们只分析 adb 方式。
adb shell sh /sdcard/Android/data/moe.shizuku.privileged.api/files/start.sh
这条 adb 命令想必用过 ShizukuManager 的人都很熟悉吧?只要在 adb 终端输入这条命令,就能激活 ShizukuManager。
这条命令很简单,就是调用 sh 脚本 start.sh 。这个 start.sh 在源码中 manager 模块的 raw 文件夹中:
这个 shell 脚本的内容是什么我们等会再看,我们先要知道 ShizukuManager 这个 APP 在启动时干了什么 ,看看 Manager 的 MainActivity 的 onCreate 方法:
private static boolean sWriteFilesCalled;
@Override
protected void onCreate(Bundle savedInstanceState) {
if (!sWriteFilesCalled) {
ServerLauncher.writeFiles(this, true);
sWriteFilesCalled = true;
}
}
这里调用了 ServiceLauncher 的 writeFiles 方法,让我们来康康:
public static void writeFiles(Context context, boolean external) {
// 从 MainActivity 传入的 external 是 true
try {
File out;
if (external)
// 执行这里,得到 /sdcard/Android/data/com.example.app/files/
out = context.getExternalFilesDir(null);
else
out = getParent(context);
if (out == null)
return;
int apiVersion = Math.min(ShizukuLegacy.MAX_SDK, Build.VERSION.SDK_INT);
String source = String.format(Locale.ENGLISH, "server-v2-%d.dex", apiVersion);
// 此时 i 为 1
int i = external ? 1 : 0;
// copyDex
DEX_LEGACY_PATH[i] = copyDex(context, source, new File(out, V2_DEX_NAME));
DEX_PATH[i] = copyDex(context, "server.dex", new File(out, V3_DEX_NAME));
// 注意注意这个 writeShellFile 方法
String command = writeShellFile(context, new File(out, "start.sh"), DEX_LEGACY_PATH[i], DEX_PATH[i]);
// external 为 true ,不执行
if (!external) {
COMMAND_ROOT = command;
}
} catch (IOException e) {
e.printStackTrace();
}
}
看到 writeShellFile 这个方法了吗?里面的参数是一个 File 类和一个 “start.sh”,还有两个 dex 文件路径。
继续看 ServiceLauncher#writeShellFile:
private static String writeShellFile(Context context, File out, String dexLegacy, String dex) throws IOException {
if (!out.exists()) {
//noinspection ResultOfMethodCallIgnored
out.createNewFile();
}
BufferedReader is = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.start)));
PrintWriter os = new PrintWriter(new FileWriter(out));
String line;
while ((line = is.readLine()) != null) {
// 这里将 start.sh 写出,并把 start.sh 的那些文字替换成路径值。
os.println(line
.replace("%%%STARTER_PATH%%%", getLibPath(context, "libshizuku.so"))
.replace("%%%STARTER_PARAM%%%", getStarterParam(dexLegacy, dex))
.replace("%%%LIBRARY_PATH%%%", getLibPath(context, "libhelper.so"))
);
}
os.flush();
os.close();
return "sh " + out.getAbsolutePath();
}
还记得我们之前看的那个 start.sh 的内容吗?这里实际上就是把 raw 内的 start.sh 写出放到 /sdcard/Android/data/moe.shizuku.privileged.api/files。
这就是为什么那条激活用的 adb 命令能够执行:
adb shell sh /sdcard/Android/data/moe.shizuku.privileged.api/files/start.sh
看到了吗?如果你安装了 ShizukuManager 并打开了,你可以在 /sdcard/Android/data/moe.shizuku.privileged.api/files 目录里找到这个 start.sh 和两个 dex 文件。
这俩 dex 文件就是通过 ServiceLauncher#writeFiles 用 copyDex 方法把 assets 的 dex 写出到SD目录。
同时 start.sh 中的那开头的三个变量也在写出的时候被 ShizukuManager 替换了:
#!/system/bin/sh
// 这三个变量在 ShizukuManager 写出时被替换了
STARTER_PATH="%%%STARTER_PATH%%%" // 对应 libshizuku.so 文件路径
STARTER_PARAM="%%%STARTER_PARAM%%%" // 对应调用 main 函数的参数,里面包含了那两个 dex 文件的路径和 token
LIBRARY_PATH="%%%LIBRARY_PATH%%%" // 对应 libhelper.so 文件路径
echo "info: start.sh begin"
if [[ -f "$STARTER_PATH" ]]; then
rm -f /data/local/tmp/shizuku_starter
// 将 libshizuku.so 文件移动到
// 实际上 /data/local/tmp/ 下的 shizuku_starter 文件就是 libshizuku.so 文件
cp "$STARTER_PATH" /data/local/tmp/shizuku_starter
// 修改权限
chmod 700 /data/local/tmp/shizuku_starter
chown 2000 /data/local/tmp/shizuku_starter
chgrp 2000 /data/local/tmp/shizuku_starter
// 这似乎是在修改 Selinux 啥的什么东西的啥子命令
chcon u:object_r:shell_data_file:s0 /data/local/tmp/shizuku_starter
// 设置环境变量
export PATH=/data/local/tmp:/system/bin:$PATH
// 调用 shizku_starter so文件(原来是 libshizuku.so)的 main 方法,传入参数
shizuku_starter ${STARTER_PARAM} $1
// 返回上 shizuku_starer 内 main 函数的返回值
result=$?
if [[ ${result} -ne 0 ]]; then
echo "info: shizuku_starter exit with non-zero value $result"
else
echo "info: shizuku_starter exit with 0"
fi
else
echo "Starter file not exist, please open Shizuku Manager and try again."
fi
当我们输入那条用来激活 ShizukuManager 的 adb 命令时候,输出框会噼里啪啦滚出一大堆输出。那些输出是哪里来的?很明显在 start.sh 中没有几条 echo 命令,只能是在 shizuku_starter 的 main 方法里有什么大动作。
这个 libshizuku.so 文件的原身你可以在 ShizukuManager 源码中的 jni 文件夹中找到,文件名是 starter.cpp。
没错,它是一个 C++ 文件,我们看看它的 main 方法(我会适当地跳过一些与主题无关的代码):
#define SERVER_CLASS_PATH_LEGACY "moe.shizuku.server.ShizukuServer"
#define SERVER_CLASS_PATH "moe.shizuku.server.Starter"
int main(int argc, char **argv) {
// argc 是传进来的参数的数量,argv是传入的参数的数组。里面包含了 token, 两个 dec 文件的路径
...
char *token = nullptr;
char *_path = nullptr;
char *_path_legacy = nullptr;
int v2 = 1;
int i;
int use_shell_context = 0;
// for 循环,可以看到这是在取出参数
for (i = 0; i < argc; ++i) {
if (strncmp(argv[i], "--token=", 8) == 0) {
token = strdup(argv[i] + 8);
} else if (strncmp(argv[i], "--path=", 7) == 0) {
_path = strdup(argv[i] + 7);
} else if (strncmp(argv[i], "--path-legacy=", 14) == 0) {
_path_legacy = strdup(argv[i] + 14);
} else if (strncmp(argv[i], "--no-v2", 7) == 0) {
v2 = 0;
} else if (strncmp(argv[i], "--use-shell-context", 19) == 0) {
use_shell_context = 1;
}
}
...
// check_acess 会调用 acess 函数判断路径是否有读写权限
check_access(_path, "source dex path");
if (v2) check_access(_path_legacy, "source legacy dex path");
// 这里我是没看懂的,不知道为什么又建了一个 /data/local/tmp/shizuku 文件夹,将之前 _path 内的文件全部移动到了 path 这个路径里。
mkdir("/data/local/tmp/shizuku", 0707);
chmod("/data/local/tmp/shizuku", 0707);
...
// 一个 char 数组,也就相当于一个 string
char path[PATH_MAX], path_legacy[PATH_MAX];
// 格式化字符,也就是为 path 赋值了
sprintf(path, "/data/local/tmp/shizuku/%s", basename(_path));
sprintf(path_legacy, "/data/local/tmp/shizuku/%s", basename(_path_legacy));
// 开始复制了,将 _path 路径下的两个 dex 移动到 path 路径
copy_if_not_exist(_path, path);
if (v2) copy_if_not_exist(_path_legacy, path_legacy);
check_access(path, "dex path");
if (v2) check_access(path_legacy, "legacy dex path");
printf("info: starter begin\n");
// 强制输出缓冲区的信息,就是为了快速输出上面的 printf 内容啦
fflush(stdout);
...
printf("info: starting server v3...\n");
fflush(stdout);
// 我们重点分析这个 start_service 函数,传入 SERVICE_CLASS_PATH 这个常量,值为 "moe.shizuku.server.Starter",熟悉吗?这就是在应用层的主角啊,用于启动 ShizukuService 的 Java 类。
start_server(path, SERVER_CLASS_PATH, token, SERVER_NAME, use_shell_context);
if (v2) {
printf("info: starting server v2 (legacy)...\n");
fflush(stdout);
start_server(path_legacy, SERVER_CLASS_PATH_LEGACY, token, SERVER_NAME_LEGACY, false);
}
exit_with_logcat(EXIT_SUCCESS);
// starter.cpp 结束
}
注释上已经大概地说了一遍,我们接下来看 start_service 函数,同样 start_service 也是 starter.cpp 中的一个函数:
static int start_server(const char *path, const char *main_class, const char *token,
const char *nice_name, int change_context) {
...
/* 省略了很多内容,详细可自己去看看 Shizuku 的源码 */
char buf[128], class_path[PATH_MAX];
sprintf(buf, "--nice-name=%s", nice_name);
setClasspathEnv(path);
snprintf(class_path, PATH_MAX, "-Djava.class.path=%s", path);
char *appProcessArgs[] = {
// app_process 是 Android 里专门用来启动 Java 程序的
const_cast<char *>("/system/bin/app_process"),
class_path,
const_cast<char *>("/system/bin"),
const_cast<char *>(buf),
// main_class 值为 "moe.shizuku.server.Starter"
const_cast<char *>(main_class),
const_cast<char *>(token),
nullptr
};
// 调用 app_process 去运行 main_class
if (execvp(appProcessArgs[0], appProcessArgs)) {
exit_with_logcat(EXIT_FATAL_APP_PROCESS);
}
...
}
到这里我们的分析已经差不多了。
我们再仔细说说 app_process 的内容:
调用 app_process 的话,你需要在参数中提供 dex 文件,同时必须要使 dex 文件有执行权限
app_process -Djava.class.path=dex文件名 dex所处的目录路径 你要启动的 Java 类(有类名和包名)
我找来了一个参数列表:
vm-options – VM 选项
cmd-dir –父目录 (/system/bin)
options –运行的参数 :
–zygote
–start-system-server
–application (api>=14)
–nice-name=nice_proc_name (api>=14)
start-class-name –包含main方法的主类 (com.android.commands.am.Am)
main-options –启动时候传递到main方法中的参数
小总结
用户通过输入一条 adb 命令去执行 start.sh 文件,start.sh 文件会执行 ShizukuManager 早已准备好的 so 文件。在 so 文件中经过一大堆操作,会通过 app_process 运行 Starter 类的 main 方法,而 Starter 这个 Java 类的 main 方法会直接 new 一个 ShizukuService 使它跑起来。
看了一上午的代码,翘了一上午的在铛铛的网络课。
尼玛,都延长假期了和上个屎的网络课。