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

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

参考方案

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容