一, 代码修复
1, 类加载方案
(1)Dex 分包原理
单个 Dex 文件里面方法数不能超过 65536 个方法.
(1) 原因:
因为 Android 会把每一个类的方法 id 检索起来, 存在一个链表结构里面. 但是这个链表的长度是用一个 short 类型来保存的, short 占两个字节 (保存 - 2 的 15 次方到 2 的 15 次方 - 1, 即 - 32768~32767), 最大保存的数量就是 65536.
(2) 解决方案:
精简方法数量, 删除没用到的类, 方法, 第三方库.
使用 ProGuard 去掉一些未使用的代码
对部分模块采用本地插件化的方式.
分割 Dex
Dex 分包方案主要做的是在打包时将应用代码分成多个 Dex, 将应用启动时必须用到的类和这些类的直接引用类放到主 Dex 中, 其他代码放到次 Dex 中. 当应用启动时先加载主 Dex, 等到应用启动后再动态的加载次 Dex.
(2) 类加载修复方案
如果 Key.Class 文件中存在异常, 将该 Class 文件修复后, 将其打入 Patch.dex 的补丁包
(1) 方案一:
通过反射获取到 PathClassLoader 中的 DexPathList, 然后再拿到 DexPathList 中的 Element 数组, 将 Patch.dex 放在 Element 数组 dexElements 的第一个元素, 最后将数组进行合并后并重新设置回去. 在进行类加载的时候, 由于 ClassLoader 的双亲委托机制, 该类只被加载一次, 也就是说 Patch.dex 中的 Key.Class 会被加载.
(2) 方案二:
提供 dex 差量包 patch.dex, 将 patch.dex 与应用的 classes.dex 合并成一个完整的 dex, 完整 dex 加载后得到 dexFile 对象, 作为参数构建一个 Element 对象, 然后整体替换掉旧的 dex-Elements 数组.(Tinker)
(3) 类加载方案的限制
方案一:
由于类是无法进行卸载, 所以类如果需要重新加载, 则需要重启 App, 所以类加载修复方案不是即时生效的.
在 ART 模式下, 如果类修改了结构, 就会出现内存错乱的问题. 为了解决这个问题, 就必须把所有相关的调用类, 父类子类等等全部加载到 patch.dex 中, 导致补丁包大, 耗时严重.
方案二:
下次启动修复
dex 合并内存消耗可能导致 OOM, 最终 dex 合并失败
2, 底层替换方案
(1) 基本方案
主要是在 Native 层替换原有方法, ArtMethod 结构体中包含了 Java 方法的所有信息, 包括执行入口, 访问权限, 所属类和代码执行地址等. 替换 ArtMethod 结构体中的字段或者替换整个 ArtMethod 结构体, 就是底层替换方案. 由于直接替换了方法, 可以立即生效不需要重启.
(2) 优缺点
(1) 缺点
不能够增减原有类的方法和字段, 如果我们增加了方法数, 那么方法索引数也会增加, 这样访问方法时会无法通过索引找到正确的方法.
平台兼容性问题, 如果厂商对 ArtMethod 结构体进行了修改, 替换机制就有问题.
(2) 优点
Bug 修复的即时性
生成的 PATCH 体积小, 性能影响低
二, 资源修复
1,Instant Run
核心代码:
- #MonkeyPatcher
- public static void monkeyPatchExistingResources(@Nullable Context context,
- @Nullable String externalResourceFile,
- @Nullable Collection<Activity> activities) {
- ......
- try {
- // Create a new AssetManager instance and point it to the resources installed under
- // (1) 通过反射创建了一个 newAssetManager, 调用 addAssetPath 添加了 sdcard 上的资源包
- AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
- Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
- mAddAssetPath.setAccessible(true);
- if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
- throw new IllegalStateException("Could not create new AssetManager");
- }
- // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
- // in L, so we do it unconditionally.
- Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
- mEnsureStringBlocks.setAccessible(true);
- mEnsureStringBlocks.invoke(newAssetManager);
- if (activities != null) {
- //(2) 反射获取 Activity 中 AssetManager 的引用, 替换成新创建的 newAssetManager
- for (Activity activity : activities) {
- Resources resources = activity.getResources();
- try {
- Field mAssets = Resources.class.getDeclaredField("mAssets");
- mAssets.setAccessible(true);
- mAssets.set(resources, newAssetManager);
- } catch (Throwable ignore) {
- Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
- mResourcesImpl.setAccessible(true);
- Object resourceImpl = mResourcesImpl.get(resources);
- Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
- implAssets.setAccessible(true);
- implAssets.set(resourceImpl, newAssetManager);
- }
- Resources.Theme theme = activity.getTheme();
- try {
- try {
- Field ma = Resources.Theme.class.getDeclaredField("mAssets");
- ma.setAccessible(true);
- ma.set(theme, newAssetManager);
- } catch (NoSuchFieldException ignore) {
- Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
- themeField.setAccessible(true);
- Object impl = themeField.get(theme);
- Field ma = impl.getClass().getDeclaredField("mAssets");
- ma.setAccessible(true);
- ma.set(impl, newAssetManager);
- }
- ......
- }
- //(3) 遍历 Resource 弱引用的集合, 将 AssetManager 替换成 newAssetManager
- for (WeakReference<Resources> wr : references) {
- Resources resources = wr.get();
- if (resources != null) {
- // Set the AssetManager of the Resources instance to our brand new one
- try {
- Field mAssets = Resources.class.getDeclaredField("mAssets");
- mAssets.setAccessible(true);
- mAssets.set(resources, newAssetManager);
- } catch (Throwable ignore) {
- Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
- mResourcesImpl.setAccessible(true);
- Object resourceImpl = mResourcesImpl.get(resources);
- Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
- implAssets.setAccessible(true);
- implAssets.set(resourceImpl, newAssetManager);
- }
- resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
- }
- }
- } catch (Throwable e) {
- throw new IllegalStateException(e);
- }
- }
反射构建新的 AssetManager, 并反射调用 addAssertPath 加载 sdcard 中的新资源包, 这样就得到一个含有所有新资源的 AssetManager
将原来引用到 AssetManager 的地方, 通过反射把引用处替换为新的 AssetManager
2, 资源包替换 (Sophix)
默认由 Android SDK 编译出来的 apk, 其资源包的 package id 为 0x7f.framework-res.jar 的资源 package id 为 0x01
构造一个 package id 为 0x66 的资源包 (非 0x7f 和 0x01), 只包含已经改变的资源项.
由于不与已经加载的 Ox7f 冲突, 所以可以通过原有的 AssetManager 的 addAssetPath 加载这个包.
三, SO 库修复
本质是对 native 方法的修复和替换
1,so 库加载
(1) 通过以下方法加载 so 库
- #System
- public static void loadLibrary(String libname) {
- Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
- }
参数为 so 库名称, 位于 apk 的 lib 目录下
- public static void load(String filename) {
- Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);
- }
加载外部自定义 so 库文件, 参数为 so 库在磁盘中的完整路径
private static native String nativeLoad(String filename, ClassLoader loader, String librarySearchPath);
最终都是调用了 native 方法 nativeLoad, 参数 fileName 为 so 在磁盘中的完整路径名
(2) 遍历 nativeLibraryDirectories 目录
- #DexPathList
- public String findLibrary(String libraryName) {
- String fileName = System.mapLibraryName(libraryName);
- for (File directory : nativeLibraryDirectories) {
- File file = new File(directory, fileName);
- if (file.exists() && file.isFile() && file.canRead()) {
- return file.getPath();
- }
- }
- return null;
- }
类似于类加载的 findClass 方法, 在数组中每一个元素对应一个 so 库, 最终返回了 so 的路径. 如果将 so 补丁添加到数组的最前面, 在调用方法加载 so 库时, 会先将补丁 so 的路径返回.
2,SO 修复方案
(1) 接口替换
提供方法替代 System.loadLibrary 方法
如果存在补丁 so, 则加载补丁 so 库, 不去加载 apk 安装目录下的 so 库
如果不存在补丁 so, 调用 System.loadLibrary 去加载安装 apk 目录下的 so 库
(2) 反射注入
因为加载 so 库会遍历 nativeLibraryDirectories
通过反射将补丁 so 库的路径插入到 nativeLibraryDirectories 数组的最前面
遍历 nativeLibraryDirectories 时, 就会将补丁 so 库进行返回并加载, 从而达到修复目的
四, 热修复框架分析
底层替换方案: 阿里的 AndFix,HotFix
类加载方案: QQ 空间补丁技术, 微信的 Tinker 方案, 饿了么的 Amigo
二者结合: Sophix
参考资料:
主流热修复方案分析 https://mp.weixin.qq.com/s/txpZP5MqPZj0EULCzCRetg
Android 热修复技术, 你会怎么选? https://www.jianshu.com/p/6ae1e09ebbf5
《Android 进阶解密》
《深入探索 Android 热修复技术原理》
来源: https://juejin.im/post/5c66bb866fb9a049eb3c76fc