代码修复:
- 底层替换方案:实时生效、兼容性差(厂商可以改动Method结构体)、限制条件多(不能对原有类进行方法和字段的增减,会改变字段和方法得索引,如果是增加,还会存在访问不到新增字段的问题)
- 支付宝 AndFix:底层结构替换、运行时生效、Dalvik和ART全兼容
- 阿里百川 HotFix:逻辑解耦版
- 类加载方案:冷启动生效(在App运行到一半的时候,所有需要发生变更的类已经被加载过了,在Android上是无法对一个类进行卸载的)、兼容性好
- 新增DEX:需要解决PreVerify问题,Dalvik下应用启动速度变慢(Art虚拟机采用了Aot模式,在应用安装时便生成好了机器码,关键数据结构:class_index、method_index,Dalvik在安装时会生成odex文件,但不会进一步生成字节码,而是在每次应用启动加载类时生成对应的字节码,如果没有打上preverify标记,则每次生成机器码是都会进行verify、optimize操作)、另外Art模式下地址写死,如果Patch类新增了方法,那么必须包含Patch的引用类,导致Patch包体积过大(Art虚拟机在当前Dex对应的Index表中找不到对应方法时,会执行Trampoline蹦床指令,用于从Native Code切换到虚拟机
- 合并DEX:复杂度高、不存在PreVerify问题、不影响应用启动速度(Tinker在方法指令维度进行合成,复杂度高、包体积小;Sophix在类维度进行合成、时间空间均衡)
- 代码插桩方案:实时生效、兼容性好、增加包大小
- 美团Robust
- 底层替换方案限制条件:
- 引起原有类发生结构变化的修改(方法中访问内部类或者外部类的私有成员会新增access方法、方法中增加或减少内部类会导致类名混乱、方法内联、方法参数裁剪),方法内联和方法裁剪可以通过-dontoptimize关掉(android sdk默认的混淆配置文件proguard-android-optimize.txt和proguard-android.txt,这两者的区别就是后者应用了-dontoptimize选项
- 修复的非静态方法会被反射调用
资源修复:
- Instant Run:构造全部资源的AssetManager,全量替换AssetManager引用
- 主流热修复:Hook AAPT任务,修改Package Id解决资源冲突(热修复包只需包含新增资源,不需要全量替换AssetManager引用)
SO热修复:
//TODO
Sophix(集大成者):
- 代码修复:
- 底层替换方案:整体替换Method结构(Method结构体线性排列,连续两个静态方法的地址相减),解决机型兼容问题
- 类加载方案:类层级的Dex Merge,复杂度低,解决PreVerify问题
知识点:
范型方法:类型擦除(Object参数)和多态的冲突(通过桥接方法来解决:覆写方法里面调用真正的子类方法)
Tinker原理:
- Dex Merge示意图:https://blog.youkuaiyun.com/qq_22393017/article/details/73999210
- Tinker在合并完成后,启动应用时,先将放在本地目录下合成后的新dex文件读取出来,然后通过反射获取应用ClassLoader的 dexElements 数组,并将新的dex给合并进去。由于完全使用了新的Dex,在Dalvik环境下无须通过插桩来避免类被打上CLASS_ISPREVERIFYED ,因为Tinker合成后的补丁类依旧在原来的dex中,而不是像Qzone那样把补丁类单独放到一个dex去
- 混合编译问题:运行时生成新的PathClassLoader,并全量替换老的PathClassLoader
- 创建新Loader的流程:替换老loader的pathList对象的definingContext属性,然后把老loader的pathList赋值给新的loader、反射老loader的pathList的makePathElements方法并调用,注意第二个方法参数设置为null,重新生成dexElements数组,并替换原来的数组,最终完成AndroidNClassLoader的创建
- 替换新Loader的流程:在全局Context中持有的LoadedApk的对象mPackageInfo的属性中,有一个ClassLoader类的对象mClassLoader,替换掉。同时将Thread中持有的ClassLoader也同步替换为AndroidNClassLoader
- Aot关键方法:
-
if (useInterpretMode) { interpretDex2Oat(dexFile.getAbsolutePath(), optimizedPath); } else if (Build.VERSION.SDK_INT >= 28 || (Build.VERSION.SDK_INT >= 27 && Build.VERSION.PREVIEW_SDK_INT != 0)) { AndroidNClassLoader.triggerDex2Oat(context, dexFile.getAbsolutePath()); } else { DexFile.loadDex(dexFile.getAbsolutePath(), optimizedPath, 0); } public static void triggerDex2Oat(Context context, String dexPath) { final ClassLoader bootClassLoader = Context.class.getClassLoader(); // Suggestion from Huawei: Only PathClassLoader (Perhaps other ClassLoaders known by system // like DexClassLoader also work ?) can be used here to trigger dex2oat so that JIT // mechanism can participate in runtime Dex optimization. new PathClassLoader(dexPath, bootClassLoader); }
- 详细原理参考:https://blog.youkuaiyun.com/l2show/article/details/53307523、https://www.jianshu.com/p/aff8f887a49f
- 知识点:
- DexClassLoader和PathClassLoader的区别:https://blog.youkuaiyun.com/mynameishuangshuai/article/details/52737581