阿里电子书《深入探索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使用静态内部类避免内存泄漏 - 内部类和外部类之间,访问彼此的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库热修复技术
无