美文网首页
Xposed搭车客指南 - 免重启调试

Xposed搭车客指南 - 免重启调试

作者: BWELCO | 来源:发表于2018-03-12 22:24 被阅读0次

    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

    这里要注意的是,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 更新之后加载真正需要加载的钩子函数。

    参考方案

    相关文章

      网友评论

          本文标题:Xposed搭车客指南 - 免重启调试

          本文链接:https://www.haomeiwen.com/subject/yiiofftx.html