Xposed搭車客指南 - 免重啟調(diào)試

What

xposed 模塊調(diào)試需要重啟手機(jī)一直是一個(gè)令人頭疼的問題麻养,浪費(fèi)大量寶貴的開發(fā)時(shí)間褐啡。再遇上 android studio 這個(gè)"編碼五分鐘、編譯兩小時(shí)"的家伙鳖昌,開發(fā)體驗(yàn)差到極點(diǎn)备畦。所以有沒有解決方案呢低飒?答案是有。

Why

先來看看為什么懂盐。以下代碼分析基于 XposedBridge/a535c02 褥赊。Xposed 在安裝的過程中,將可執(zhí)行文件 app_process(xposed定制版) 拷貝到 /system/bin 中莉恼,代替 android 本身的 app_process 來實(shí)現(xiàn)對(duì)整個(gè)系統(tǒng)的 hook拌喉。它會(huì)在手機(jī)啟動(dòng)過程中加載 XposedBridge.jar,然后用 XposedBridge.jar 來進(jìn)行一些必要的初始化并加載 xposed modules俐银。

我們姑且猜測(cè):xposed 在啟動(dòng)過程中掃描 app 的 manifest 來找到合法的 xposed_module尿背,然后解包找到 assets/xposed_init 文件,并通過某種方式來進(jìn)行 xposed_module 的初始化捶惜。and 可能由于某些原因這些初始化只在開機(jī)過程中執(zhí)行一次田藐,所以如果能理清楚 xposed_module 的初始化流程,然后重放 xposed_module.init() 不就可以解決我們的問題么售躁。

老規(guī)矩坞淮,知己知彼,百戰(zhàn)不殆陪捷。我們首先分析一下回窘,XposedBridge 是如何加載 xposed_module 的 (注: 以下代碼均有刪減,請(qǐng)參考源代碼)
.
.
.
先從 XposedBridge.main() 開始

protected static void main(String[] args) {
    ...
        if (isZygote) {
            XposedInit.hookResources();
            XposedInit.initForZygote();
        }

        XposedInit.loadModules();
    ...
}

上面進(jìn)行了一些初始化市袖、然后緊接著開始加載 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啡直,
這個(gè)配置會(huì)在安裝了新的 xposed_module 之后進(jìn)行更新,形如

/data/app/com.youzan.mobile.hook-1/base.apk
/data/app/com.gh0u1l5.wechatmagician-1/base.apk

里面記錄了 xposed_module 的 apk 文件路徑苍碟。解析之后循環(huán)進(jìn)行 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路徑構(gòu)造出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));
    ...
}

剝?nèi)ヒ恍┬r?yàn) Instant Run微峰、Xposed 依賴檢測(cè)等多余的代碼后舷丹,剩下的邏輯就很清晰了。loadModule 函數(shù)先通過找到 assets/xposed_init 中定義的 module_entry (鉤子函數(shù))蜓肆,然后通過反射拿到 module_entry 對(duì)象颜凯,并調(diào)用 XposedBridge.hookLoadPackage(XC_LoadPackage callback) 方法

public static void hookLoadPackage(XC_LoadPackage callback) {
        synchronized (sLoadedPackageCallbacks) {
            sLoadedPackageCallbacks.add(callback);
        }
}

這里將 callback 放入了一個(gè) set 中,那么 set 里面的鉤子什么時(shí)候才會(huì)被調(diào)用呢仗扬?
回到 XposedBridge.main() 函數(shù)症概,里面調(diào)用了

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 初始化最為重要的函數(shù),這里可以拿到 packageName早芭、processName彼城、classLoader、appInfo 等一些我們熟悉的參數(shù)。然后 XposedBridge 會(huì)遍歷 set 中的所有鉤子函數(shù)募壕,并進(jìn)行回調(diào)调炬。

簡單地理一下流程


XposedBridge

這里要注意的是,XposedBridge 只在 android 系統(tǒng)加載的時(shí)候初始化一次司抱,之后就將鉤子函數(shù)放入 set 中筐眷,之后所有的application 加載行為都回調(diào)第一次初始化的 callBack。
所以回到本文的問題: "為什么 xposed 覆蓋安裝需要重啟手機(jī)"习柠?我想大家已經(jīng)知道了答案匀谣。鉤子函數(shù)在 android 初始化的時(shí)候被放入 set 中,并且這個(gè)鉤子函數(shù)在系統(tǒng)重啟之前都不會(huì)被更新资溃。所以我們必須通過重啟系統(tǒng)來更新鉤子函數(shù)武翎。

How

1、既然 XposedInit.loadModules() 只在 XposedBridge 初始化的時(shí)候才被調(diào)用溶锭,那我們能不能通過 hack 的方式來強(qiáng)行調(diào)用XposedInit.loadModules()來達(dá)到我們刷新鉤子函數(shù)的目的呢宝恶?
可以參考這篇帖子,在每次 handleBindApplication 的時(shí)候調(diào)用 loadModules() 函數(shù)趴捅。這樣就可以強(qiáng)行刷新鉤子函數(shù)垫毙。但是這樣也有弊端,就是操作起來比較復(fù)雜拱绑,需要重新編譯 XposedBridge.jar 并安裝到系統(tǒng)框架中综芥,而且每當(dāng)新的 app 啟動(dòng)都會(huì)重新刷新一次鉤子函數(shù),性能稍微差了點(diǎn)猎拨,不過用來調(diào)試的話可以忽略膀藐。

2、我們可以將 hook 的邏輯寫到 hook_app里面去红省,然后寫個(gè)啟動(dòng)這個(gè) hook_app 的殼额各,傳入需要的
XC_LoadPackage.LoadPackageParam 參數(shù)。然后通過反射加載 hook_app吧恃。加載 hook_app 可以通過 apk 路徑來構(gòu)造 PathClassLoader虾啦,再然后用 PathClassLoader 來查找需要加載的鉤子函數(shù)。并通過 newInstance() 來加載目標(biāo) hook_app痕寓,來打到我們的免重啟調(diào)試
xposed 模塊的目的缸逃。避免了修改 Xposed 框架的源碼。

為了方便我們把殼和真正的 hook_app 都寫到我們的模塊中去厂抽,并通過 debug 來判斷正常加載/反射調(diào)試。

// 查找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")
    }

    // 真正的鉤子函數(shù)
    private fun readHandler(lpparam: XC_LoadPackage.LoadPackageParam, context: Context) {
        XposedBridge.log("load realHandler(), packageName = ${lpparam.packageName}")
    }

    // 通過反射來調(diào)用鉤子函數(shù)
    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來構(gòu)造PathClassLoader
        val pathClassLoader = PathClassLoader(apkPath, ClassLoader.getSystemClassLoader())
        
        // 找到真正的入口并反射調(diào)用
        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

想實(shí)現(xiàn)免重啟調(diào)試 xposed module 有倆種方法

  • 改 XposedBridge 的代碼筷凤,在合適的時(shí)機(jī)刷新鉤子函數(shù)。
  • 不刷新鉤子函數(shù),寫一個(gè)可以加載 xposed module 的殼藐守,在 xposed module 更新之后加載真正需要加載的鉤子函數(shù)挪丢。

參考方案

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市卢厂,隨后出現(xiàn)的幾起案子乾蓬,更是在濱河造成了極大的恐慌,老刑警劉巖慎恒,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件任内,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡融柬,警方通過查閱死者的電腦和手機(jī)死嗦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粒氧,“玉大人越除,你說我怎么就攤上這事⊥舛ⅲ” “怎么了摘盆?”我有些...
    開封第一講書人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長饱苟。 經(jīng)常有香客問我孩擂,道長,這世上最難降的妖魔是什么掷空? 我笑而不...
    開封第一講書人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任肋殴,我火速辦了婚禮,結(jié)果婚禮上坦弟,老公的妹妹穿的比我還像新娘护锤。我一直安慰自己,他們只是感情好酿傍,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開白布烙懦。 她就那樣靜靜地躺著,像睡著了一般赤炒。 火紅的嫁衣襯著肌膚如雪氯析。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,785評(píng)論 1 314
  • 那天莺褒,我揣著相機(jī)與錄音掩缓,去河邊找鬼。 笑死遵岩,一個(gè)胖子當(dāng)著我的面吹牛你辣,可吹牛的內(nèi)容都是我干的巡通。 我是一名探鬼主播,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼舍哄,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼宴凉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起表悬,我...
    開封第一講書人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤弥锄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蟆沫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體籽暇,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年饥追,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了图仓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡但绕,死狀恐怖救崔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捏顺,我是刑警寧澤六孵,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站幅骄,受9級(jí)特大地震影響劫窒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拆座,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一主巍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧挪凑,春花似錦孕索、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至菇绵,卻和暖如春肄渗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背咬最。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來泰國打工翎嫡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人永乌。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓钝的,卻偏偏與公主長得像翁垂,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子硝桩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,332評(píng)論 25 707
  • Lua 5.1 參考手冊(cè) by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,836評(píng)論 0 38
  • 安全博客 > 技術(shù)研究 > 淺談android hook技術(shù) 淺談android hook技術(shù) 您當(dāng)前的位置:...
    光劍書架上的書閱讀 6,177評(píng)論 0 8
  • 1.感恩昨天晚上工作群里通知今天早上9點(diǎn)開會(huì),我居然沒發(fā)現(xiàn)枚荣,同事黃姐給我打電話說馬上開會(huì)我才知道然后從老屋里感到單...
    梁鑫爍閱讀 396評(píng)論 2 0
  • 荏苒又一年碗脊,人生正步入第38個(gè)年頭。過往塵封記憶橄妆,迷失在靈魂深處衙伶。隨著時(shí)間的侵蝕能留下的星星點(diǎn)點(diǎn),留給我在某天角落...
    老李同志講故事閱讀 317評(píng)論 0 4