What
xposed 模块调试需要重启手机一直是一个令人头疼的问题,浪费大量宝贵的开发时间。再遇上 android studio 这个"编码五分钟、编译两小时"的家伙,开发体验差到极点。所以有没有解决方案呢?答案是有。
Why
先来看看为什么。以下代码分析基于 XposedBridge/a535c02 。Xposed 在安装的过程中,将可执行文件 app_process(xposed定制版) 拷贝到 /system/bin 中,代替 android 本身的 app_process 来实现对整个系统的 hook。它会在手机启动过程中加载 XposedBridge.jar,然后用 XposedBridge.jar 来进行一些必要的初始化并加载 xposed modules。
我们姑且猜测:xposed 在启动过程中扫描 app 的 manifest 来找到合法的 xposed_module,然后解包找到 assets/xposed_init 文件,并通过某种方式来进行 xposed_module 的初始化。and 可能由于某些原因这些初始化只在开机过程中执行一次,所以如果能理清楚 xposed_module 的初始化流程,然后重放 xposed_module.init() 不就可以解决我们的问题么。
老规矩,知己知彼,百战不殆。我们首先分析一下,XposedBridge 是如何加载 xposed_module 的 (注: 以下代码均有删减,请参考源代码)
.
.
.
先从 XposedBridge.main() 开始
protected static void main(String[] args) {
...
if (isZygote) {
XposedInit.hookResources();
XposedInit.initForZygote();
}
XposedInit.loadModules();
...
}
上面进行了一些初始化、然后紧接着开始加载 xposed_module
/*package*/ static void loadModules() throws IOException {
final String filename = BASE_DIR + "conf/modules.list";
ClassLoader topClassLoader = XposedBridge.BOOTCLASSLOADER;
String apk;
while ((apk = apks.readLine()) != null) {
loadModule(apk, topClassLoader);
}
apks.close();
}
从 BASE_DIR + "conf/modules.list" 也就是 XposedInstaller 的配置文件中读取已安装的 xposed_module,
这个配置会在安装了新的 xposed_module 之后进行更新,形如
/data/app/com.youzan.mobile.hook-1/base.apk
/data/app/com.gh0u1l5.wechatmagician-1/base.apk
里面记录了 xposed_module 的 apk 文件路径。解析之后循环进行 xposed_module 的初始化,传入 classloader 和 apk 路径。
private static void loadModule(String apk, ClassLoader topClassLoader) {
...
ZipEntry zipEntry = zipFile.getEntry("assets/xposed_init");
is = zipFile.getInputStream(zipEntry);
BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is));
// 通过apk路径构造出ClassLoader
ClassLoader mcl = new PathClassLoader(apk, XposedBridge.BOOTCLASSLOADER);
Class<?> moduleClass = mcl.loadClass(moduleClassName);
final Object moduleInstance = moduleClass.newInstance();
if (moduleInstance instanceof IXposedHookLoadPackage)
XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));
...
}
剥去一些校验 Instant Run、Xposed 依赖检测等多余的代码后,剩下的逻辑就很清晰了。loadModule 函数先通过找到 assets/xposed_init 中定义的 module_entry (钩子函数),然后通过反射拿到 module_entry 对象,并调用 XposedBridge.hookLoadPackage(XC_LoadPackage callback) 方法
public static void hookLoadPackage(XC_LoadPackage callback) {
synchronized (sLoadedPackageCallbacks) {
sLoadedPackageCallbacks.add(callback);
}
}
这里将 callback 放入了一个 set 中,那么 set 里面的钩子什么时候才会被调用呢?
回到 XposedBridge.main() 函数,里面调用了
XposedInit.initForZygote();
initForZygote 中又 Hook 了 handleBindApplication
findAndHookMethod(ActivityThread.class, "handleBindApplication",
"android.app.ActivityThread.AppBindData", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
ActivityThread activityThread = (ActivityThread) param.thisObject;
ApplicationInfo appInfo = (ApplicationInfo) getObjectField(param.args[0], "appInfo");
String reportedPackageName = appInfo.packageName.equals("android") ? "system" : appInfo.packageName;
LoadedApk loadedApk = activityThread.getPackageInfoNoCheck(appInfo, compatInfo);
XResources.setPackageNameForResDir(appInfo.packageName, loadedApk.getResDir());
XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks);
lpparam.packageName = reportedPackageName;
lpparam.processName = (String) getObjectField(param.args[0], "processName");
lpparam.classLoader = loadedApk.getClassLoader();
lpparam.appInfo = appInfo;
lpparam.isFirstApplication = true;
XC_LoadPackage.callAll(lpparam);
if (reportedPackageName.equals(INSTALLER_PACKAGE_NAME))
hookXposedInstaller(lpparam.classLoader);
}
});
handleBindApplication 是 android application 初始化最为重要的函数,这里可以拿到 packageName、processName、classLoader、appInfo 等一些我们熟悉的参数。然后 XposedBridge 会遍历 set 中的所有钩子函数,并进行回调。
简单地理一下流程
这里要注意的是,XposedBridge 只在 android 系统加载的时候初始化一次,之后就将钩子函数放入 set 中,之后所有的application 加载行为都回调第一次初始化的 callBack。
所以回到本文的问题: "为什么 xposed 覆盖安装需要重启手机"?我想大家已经知道了答案。钩子函数在 android 初始化的时候被放入 set 中,并且这个钩子函数在系统重启之前都不会被更新。所以我们必须通过重启系统来更新钩子函数。
How
1、既然 XposedInit.loadModules() 只在 XposedBridge 初始化的时候才被调用,那我们能不能通过 hack 的方式来强行调用XposedInit.loadModules()来达到我们刷新钩子函数的目的呢?
可以参考这篇帖子,在每次 handleBindApplication 的时候调用 loadModules() 函数。这样就可以强行刷新钩子函数。但是这样也有弊端,就是操作起来比较复杂,需要重新编译 XposedBridge.jar 并安装到系统框架中,而且每当新的 app 启动都会重新刷新一次钩子函数,性能稍微差了点,不过用来调试的话可以忽略。
2、我们可以将 hook 的逻辑写到 hook_app里面去,然后写个启动这个 hook_app 的壳,传入需要的
XC_LoadPackage.LoadPackageParam 参数。然后通过反射加载 hook_app。加载 hook_app 可以通过 apk 路径来构造 PathClassLoader,再然后用 PathClassLoader 来查找需要加载的钩子函数。并通过 newInstance() 来加载目标 hook_app,来打到我们的免重启调试
xposed 模块的目的。避免了修改 Xposed 框架的源码。
为了方便我们把壳和真正的 hook_app 都写到我们的模块中去,并通过 debug 来判断正常加载/反射调试。
// 查找apk路径
private fun getApplicationApkPath(context: Context, packageName: String): String {
val pm = context.packageManager
val apkPath = pm.getApplicationInfo(packageName, 0)?.publicSourceDir
return apkPath ?: throw Error("Failed to get the APK path of $packageName")
}
// 真正的钩子函数
private fun readHandler(lpparam: XC_LoadPackage.LoadPackageParam, context: Context) {
XposedBridge.log("load realHandler(), packageName = ${lpparam.packageName}")
}
// 通过反射来调用钩子函数
private fun loadRealHandlerByReflect(lpparam: XC_LoadPackage.LoadPackageParam, context: Context) {
val apkPath = getApplicationApkPath(context, "com.youzan.mobile.hook")
if (!File(apkPath).exists()) {
XposedBridge.log("Cannot load handler: APK not found")
return
}
// 通过apk来构造PathClassLoader
val pathClassLoader = PathClassLoader(apkPath, ClassLoader.getSystemClassLoader())
// 找到真正的入口并反射调用
val hookEntryClazz = Class.forName("com.youzan.mobile.hook.HookEntry", true, pathClassLoader)
val realHandlerMethod = hookEntryClazz.getDeclaredMethod("readHandler", lpparam::class.java, Context::class.java)
realHandlerMethod.isAccessible = true
realHandlerMethod.invoke(hookEntryClazz.newInstance(), lpparam, context)
}
// entry
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
tryVerbosely {
when (lpparam.packageName) {
TARGET_PACKAGE ->
hookApplicationAttach(lpparam.classLoader, { context ->
if (BuildConfig.DEBUG) {
loadRealHandlerByReflect(lpparam, context)
} else {
readHandler(lpparam, context)
}
})
}
}
}
重启之后再修改安装就可以立即生效啦。
Last
想实现免重启调试 xposed module 有俩种方法
- 改 XposedBridge 的代码,在合适的时机刷新钩子函数。
- 不刷新钩子函数,写一个可以加载 xposed module 的壳,在 xposed module 更新之后加载真正需要加载的钩子函数。
参考方案