阿里电子书深入探索 Android 热修复技术原理整理的笔记
1. 热修复技术介绍
代码修复两大主要方案
底层替换方案: 限制较多, 但时效性好, 立即见效
类加载方案时效性差, 需要重新冷启动才能见效, 但限制少
代码修复底层替换方案
底层替换方案是在已经加载了的类中直接替换掉原有方法.
不能对原有类进行方法和字段的增减, 因为这样将破坏原有类的结构.
方法增减将导致这个类及整个 Dex 方法数的变化, 伴随着方法索引的变化, 这样访问方法时就无法正常的索引到正确的方法;
字段增加或减少, 所有字段的索引都会发生变化;
传统底层替换方案, 都是直接依赖修改虚拟机方法实体中的具体字段. 依据的是 Android 开源版本. 如果厂商修改了虚拟机方法实体, 替换机制就可能出问题;
代码修复类加载方案
类加载方案的原理是在 app 重新启动后让 Classloader 去加载新的类.
在 app 运行到一半的时候, 所有需要发生变更的类都已经被加载过, Android 无法对一个类进行卸载. 如果不重启, 原来的类还在虚拟机中, 就无法加载新类.
只有在下次重启时候, 在还没有走到业务逻辑前抢先加载补丁中的新类, 后续访问才是新的类.
dex 比较的最佳粒度, 应该是在类的粒度
Sophix 采用的也是全量合成 dex 的技术. 可以看做是 dex 文件级别的类插装方案. Sophix 对旧包与补丁包中 classes.dex 的顺序进行了打破与重组, 使得系统可以自然地识别到这个顺序, 以实现类覆盖的目的
资源修复
市面上很多资源热修复方案都采用了 Instant Run 的实现
Instant Run 中资源修复步骤:
构造一个新的 AssetManager, 通过反射调用 addAssetPath, 把这个完整的新资源包加到 AssetManager 中. 这样就得到一个含有所有新资源的 AssetManager.
找到所有之前引用到 AssetManager 的地方, 通过反射, 将引用出替换为 AssetManager.
Sophix 资源热修复没有采用 Instant Run 的技术, 而是构造了一个 package id 为 0x66 的资源包, 这个包里只包含改变了的资源项, 然后直接在原有 AssetManager 中 addAssetPath 这个包即可. 无需变更 AssetManager 对象的引用.
Sophix 构造的补丁包的 package id 为 0x66, 不与已经加载的 0x7f 冲突, 所以直接加入到已有的 AssetManager 中可以直接使用.
Sophix 资源补丁包中, 只包含新增资源, 以及原有内容发生了改变的资源.
SO 库修复: 本质上是对 native 方法的修复和替换
Sophix 采用的是类似类修复反射注入方式, 把补丁 so 库的路径插入到 nativeLibraryDirectories 数组的最前面, 这样加载 so 库的时候就是补丁 so 库而不是原来的 so 库
2. 代码热修复技术
底层热替换原理
Android 的 java 运行环境, 在 4.4 以下用的是 dalvik 虚拟机, 4.4 以上是 art 虚拟机.
在各种 Android 热修复方案中, Andfix 即时生效. Andfix 采用的方法是, 在已经加载了的类中直接在 native 层替换掉所有方法, 是在原来的类的基础上进行修改的.
以 art,Android6.0 为例, 每一个 Java 方法在 art 中都对应着一个 ArtMethod 对象, ArtMethod 记录了这个 Java 方法的所有信息, 包括所属类, 访问权限, 代码执行地址等.
Andfix 会将一个旧 Java 方法对应的 ArtMethod 实例中的所有字段值替换为新方法的值, 这样所有执行到旧方法的地方, 都会取得新方法的执行入口, 所属 class, 方法索引, 所属 dex. 像调用旧方法一样执行了新方法的逻辑.
底层热替换兼容性根源
市面上几乎所有的 native 替换替换, 都是写死了 ArtMethod 结构体
写死的 ArtMethod 结构和 Android 开源版本中完全一致, 但各个厂家可以对 ArtMethod 进行修改, 那么在修改过的设备上, 市面上的 native 替换方案 (将方案中写死了的 ArtMethod 关联的新方法的属性赋值到设备中的 ArtMethod 实例) 就会出现问题, 因为两个 ArtMethod 中相同字段的索引不同
突破底层热替换兼容问题
native 层面替换, 实质是替换 ArtMethod 实例的所有字段.
只要把 ArtMethod 作为整体进行替换, 即可解决兼容问题.
ArtMethod 实例之间, 是紧密线形排列的, 所以一个 ArtMethod 的大小, 就是其相邻的两个方法对应的 ArtMethod 实例的起始地址的差值.
包括 Sophix 在内的底层替换方案, 都只能支持方法的替换, 不支持补丁类中增减方法和字段
补丁类中增减方法, 会导致这个类及整个 dex 方法数的变化, 方法数的变化伴随方法索引的变化, 这样在调用方法时无法正常的所引导正确的方法.
补丁类中增减字段, 也会导致所有字段的索引发生变化.
你说不知的 Java
内部类在编译期会被编译为根外部类一样的顶级类;
非静态内部类持有外部类的引用, 静态内部类不持有外部类的引用. 所以 android 性能优化中建议自定义 Handler 的实现尽量使用静态内部类, 防止外部类 Activity 类不能被回收导致内存泄漏.
自定义 Handler 使用静态内部类避免内存泄漏 https://blog.csdn.net/ucxiii/article/details/50972747
内部类和外部类之间, 访问彼此的 private 属性及方法, 编译期间:
外部类访问内部类的私有成员及方法, 编译期间自动为内部类生成 access&** 方法
内部类访问外部类的 private 属性及方法, 编译期间也会生成 access&** 方法提供给内部类
同一个类及其内部类, 如果老代码没有访问对方的私有属性 / 方法, 新代码有访问对方的私有属性 / 方法, 如果不能避免生成 access & 的生成, 就会导致方法数的变化, 导致热修复失败. 避免生成 access & 方法需要:
外部类所有的属性及方法改为 public 或 protected;
内部类所有的属性及方法改为 public 或 protected;
在编译期间, 根据匿名内部类在外部类中出现的先后顺序, 匿名内部类的名称依次累加: 外部类名称 & 数字
外部类名称是 OutClass, 其中对应的内部类在编译期间的名称依次是: OutClass&1,OutClass&2,-----
为了实现热修复, 外部类应该极力避免新增及减少匿名内部类;
除非是新增匿名内部类到外部类的尾部, 不会影响之前添加过的匿名内部类的名称, 不然会导致热修复失效;
Java 原始类型: double,float,byte,short,int,long,char,boolean
如果一个常量的类型是 Java 原始类型, 或 String, 为了优化性能, 应该用 static final 修饰;
static final 引用类型, 没有任何优化效果.
因为 static final 修饰的原始类型及 String 常量, 是在所属类的初始化时赋值, 直接在内存中读取;
而 static final 引用类型常量, 初始化是在 clinit 方法中, 本质上是通过 sget-object 指令去获取值, 从虚拟机运行性能上无任何优化;
市面上的冷启动类加载实现方案
1: 采用 dex 插桩的方式, 单独放一个帮助类在独立的 dex 中让其他类调用. 最后加载补丁 dex 得到 dexFile 对象, 将 dexFile 作为参数构建一个 Element 对象插入到 dexElements 数组最前面
2: 提供 dex 差量包, 整体替换 dex 的方案: 差量 patch.dex 和应用的 classes.dex 合成完整 dex. 加载完整 dex 得到 dexFile 对象, 作为参数构建一个 Element 对象, 然后整体替换掉旧的 dexElements 数组
1 的缺点是: Dalvik 下影响类加载性能, Art 下类地址写死, 导致必须包含父类及引用, 补丁包很大
2 的缺点是: dex 的合并, 内存消耗在 vm heap 上, 容易导致 OOM, 合并失败
Sophix 采用的代码修复冷启动方案
Dalvik 下使用全量 Dex 方案;
Art 下本质上虚拟机已经支持多 dex 的加载, 只要把补丁 dex 作为主 dex(classes)即可
Sophix 在 Dalvik 下全量 Dex 方案思路
基线包 dex 里面, 去掉补丁包 dex 中包含的 class; 这样补丁 + 去除了补丁中包含类的基线包, 就等于新 app 中所有类;
Sophix 并没有把某个 class 的所有信息从基线 dex 中移除, 仅仅移除了定义的入口, 让解析基线 dex 时候找不到这个 class 的定义即可; 这样不会导致 dex 的各个部分都发生变化, 防止大量调整 offset.
只要把所有的 dex 都 load 进去, 单个 dex 中不存在的类就可以在运行期间在其他 dex 中找到. 补丁中的类和基线中的类可以互相访问到
3. 资源热修复技术
Android 资源的热修复, 就是在 app 不重新安装的情况下, 利用下发的补丁包直接更新 app 中的资源
Sophix 的资源热修复方案
1: 构造一个 package id 为 0x66 的资源包, 这个包里只含有变更的资源, 以及新增资源;
2: 然后直接在原有 AssetManager 上调用 addAssetPath 加入这个资源包即可;
因为我们补丁包的 id 和已经加载的 0x7f 冲突, 所以直接加入原有 AssetManager 即可直接使用
4.SO 库热修复技术
无
来源: http://www.jianshu.com/p/f2d8f7d11854