背景
Tinker开源挺长时间了,使用的开发者也越来越多,对于一些小白开发者来说对接Tinker的成本还是挺高的,其中主要因素还是不能理解为什么Application要修改成ApplicationLike,以及改造后对项目中使用Application的地方也要同步修改。
在上篇文章Android热补丁之Tinker原理解析中我们已经讲解了这样做的目的以及Tinker的加载补丁的流程,本篇文章主要讲一下一键接入Tinker的实现思路。
InstantRun
我们的目的是要实现不修改Application达到替换Application的效果,在这篇文章从Instant run谈Android替换Application和动态加载机制中,详细讲述了如何动态替换Application,总结起来就两步:
打包时替换Application标签,插入BootstrapApplication
运行时hook系统api,将BootstrapApplication换回MyApplication
那么,我们依然可以用这套方案来实现Tinker的一键接入,动态替换Application。
实现
有了思路我们就可以敲代码了。
打包
打包时我们要改变Manifest中Application的标签值,可以通过自定义Gradle插件来实现,关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @TaskAction def updateManifest() { def ns = new Namespace("http://schemas.android.com/apk/res/android", "android") def xml = new XmlParser().parse(new InputStreamReader(new FileInputStream(manifestPath), "utf-8")) def application = xml.application[0] if (application) { def metaDataTags = application['meta-data'] String rawApplicationName = application.attributes()[ns.name] metaDataTags.findAll { it.attributes()[ns.name].equals(TINKER_APPLICATION) }.each { it.parent().remove(it) } application.appendNode('meta-data', [(ns.name): TINKER_APPLICATION, (ns.value): rawApplicationName]) application.attributes()[ns.name] = TINKER_APPLICATION_VALUE def printer = new XmlNodePrinter(new PrintWriter(manifestPath, "utf-8")) printer.preserveWhitespace = true printer.print(xml) } } |
打包出的apk中的AndroidManifest.xml文件基本是这样的
1 2 3 4 | <application android:name="com.w4lle.onekeytinker.BootstrapApplication"> ... <meta-data android:name="ONEKEY_TINKER_APPLICATION" android:value="com.w4lle.onekeytinker.App"/> </application> |
其中的App是项目中原有的Application,BootstrapApplication是后期我们插入的Application。自定义Gradle插件时可以封装一个Extension配置参数,把Tinker的相关配置封装起来,一些不变的默认配置项都可以写到里面,这样项目的gradle配置可以更简洁。另外说一句,这个Gradle插件的顺序应该是打包工具生成Manifest之后,Tinker相关Task之前。
运行时替换Application
这一步的主要工作也是分两步,第一就是解析Manifest文件,拿到realApplication(App)和BootstrapApplication;然后hook 系统完成替换。
InstantRun中的替换实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | public static void monkeyPatchApplication(@Nullable Context context, @Nullable Application bootstrap, @Nullable Application realApplication) { try { // Find the ActivityThread instance for the current thread Class<?> activityThread = Class.forName("android.app.ActivityThread"); Object currentActivityThread = getActivityThread(context, activityThread); // Find the mInitialApplication field of the ActivityThread to the real application Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication"); mInitialApplication.setAccessible(true); Application initialApplication = (Application) mInitialApplication.get(currentActivityThread); if (realApplication != null && initialApplication == bootstrap) { //**2.替换掉ActivityThread.mInitialApplication** mInitialApplication.set(currentActivityThread, realApplication); } // Replace all instance of the stub application in ActivityThread#mAllApplications with the // real one if (realApplication != null) { Field mAllApplications = activityThread.getDeclaredField("mAllApplications"); mAllApplications.setAccessible(true); List<Application> allApplications = (List<Application>) mAllApplications .get(currentActivityThread); for (int i = 0; i < allApplications.size(); i++) { if (allApplications.get(i) == bootstrap) { //**1.替换掉ActivityThread.mAllApplications** allApplications.set(i, realApplication); } } } // Figure out how loaded APKs are stored. // API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know. Class<?> loadedApkClass; try { loadedApkClass = Class.forName("android.app.LoadedApk"); } catch (ClassNotFoundException e) { loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo"); } Field mApplication = loadedApkClass.getDeclaredField("mApplication"); mApplication.setAccessible(true); // 10 doesn't have this field, 14 does. Fortunately, there are not many Honeycomb devices // floating around. Field mLoadedApk = null; try { mLoadedApk = Application.class.getDeclaredField("mLoadedApk"); } catch (NoSuchFieldException e) { // According to testing, it's okay to ignore this. } // Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and // ActivityThread#mResourcePackages and do two things: // - Replace the Application instance in its mApplication field with the real one // - Set Application#mLoadedApk to the found LoadedApk instance for (String fieldName : new String[]{"mPackages", "mResourcePackages"}) { Field field = activityThread.getDeclaredField(fieldName); field.setAccessible(true); Object value = field.get(currentActivityThread); for (Map.Entry<String, WeakReference<?>> entry : ((Map<String, WeakReference<?>>) value).entrySet()) { Object loadedApk = entry.getValue().get(); if (loadedApk == null) { continue; } if (mApplication.get(loadedApk) == bootstrap) { if (realApplication != null) { //**3.替换掉mApplication** mApplication.set(loadedApk, realApplication); } if (realApplication != null && mLoadedApk != null) { //**4.替换掉mLoadedApk** mLoadedApk.set(realApplication, loadedApk); } } } } } catch (Throwable e) { throw new IllegalStateException(e); } } |
主要做了两件事:
替换Application
baseContext.mPackageInfo.mApplication 代码3处
baseContext.mPackageInfo.mActivityThread.mInitialApplication 代码2处
baseContext.mPackageInfo.mActivityThread.mAllApplications 代码1处
替换mLoadedApk对象,代码4处
详细请查看从Instant run谈Android替换Application和动态加载机制
做完上面这两步这样就可以实现一键接入了。
兼容性
在上篇文章中我们提到,由于该方案大量hook系统api,在国内Android碎片化如此严重的市场环境下,该方案兼容性有一些问题,大概有 1/1w的概率会出现替换失败的问题,如果替换失败,那么在系统中运行的Application还是BootstrapApplication,而我们App中的Application已经没有了Application的生命周期和作用。
所以我们要在失败catch中调用下Application的生命周期方法以保证程序能够正常初始化启动起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | try { ... } catch { e = true; realApplication.onCreate(); } public void onConfigurationChanged(Configuration paramConfiguration) { if (e && realApplication != null) { realApplication.onConfigurationChanged(paramConfiguration); return; } super.onConfigurationChanged(paramConfiguration); } public void onLowMemory() { if (e && realApplication != null) { realApplication.onLowMemory(); return; } super.onLowMemory(); } @TargetApi(14) public void onTrimMemory(int paramInt) { if (e && realApplication != null) { realApplication.onTrimMemory(paramInt); return; } super.onTrimMemory(paramInt); } public void onTerminate() { if (e && realApplication != null) { realApplication.onTerminate(); return; } super.onTerminate(); } |
这么做虽然能保证App能启动,但是实际上还会有隐性问题存在。比如App中有如下代码((App) getApplication()).xxx();
,那么在替换失败的情况下可能就会崩了, 因为getApplication()
得到的是BootstrapApplication,强转为App
类型肯定就挂了。
总结
整体思路大概讲清楚了,虽然这种方案接入成本低,但是兼容性问题是个很麻烦的事情,说不定啥时候就崩了。推荐大家还是使用Tinker自有的接入方案。
共同学习,写下你的评论
评论加载中...
作者其他优质文章