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 只在 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ù)挪丢。
參考方案