Android热修复学习笔记(三):冷启动修复和其他资源修复

 冷启动的作用在于突破了热替换方案无法新增类方法的限制。可以更好地达到修复目的。

冷启动的原理

 冷启动的大致流程为:提供dex差量包,整体替换dex。将补丁dex和应用的classes.dex合并为一个完整的dex。完整的dex加载得到的dexFile对象作为参数构建一个Element对象然后整体替换掉旧的dex Elements数组。

著名的CLASS_ISPREVERIFIFED错误

 首先,介绍一下在davilk虚拟机上特有的“CLASS_ISPREVERIFIFED”错误。
 在apk第一次安装时,会对dex执行类校验(用于校验类的合法性,防止被篡改),成功后会进行类优化,把部分指令优化成虚拟机内部指令。如果apk只有一个dex,那么这个dex就不会被打上CLASS_ISPREVERIFIFED标记。这时假如A类是补丁类,在单独的补丁dex中,类B中的某个方法引用到了补丁类A,该方法在执行时会尝试解析类A。如果此时类B被打上了CLASS_ISPREVERIFIFED标记,虚拟机会判断类A和类B是不是同一个dex,如果不是那么就会报错。davilk虚拟机由于只有一个dex,所以这个问题是必须解决的。
 对于这个问题,业界提出许多解决方案。

(1)插桩

 通过.class字节码修改技术在dex所有类的构造函数中都引用一个单独的无关帮助类。这个帮助类会被放到单独的dex中,这样,因为有两个dex,CLASS_ISPREVERIFIFED就不会被设置。
 这个方案的缺点在于,会造成极大的性能损耗。CLASS_ISPREVERIFIFED的设置,使得类的校验和优化仅仅在第一次安装后被执行一次即可。在日后的加载使用中,只要判断了CLASS_ISPREVERIFIFED标志被设置,那么就会跳过类的校验和优化。去除CLASS_ISPREVERIFIFED,则会使类的校验和优化在每次类加载中都会被执行。

(2)将补丁类加入到原有的pResClasses数组中

 在进行CLASS_ISPREVERIFIFED校验之前,虚拟机会先从pResClasses数组中查询是否存在想要的类。pResClasses数据保存有已经加载过的类。如果虚拟机预先能从pResClasses获取想要的类,那么就不会走CLASS_ISPREVERIFIFED校验了。
 这个方案的缺点在于:会在jni层直接操作dex中的类和索引id,而dexopt后,odex层面的优化会写死字段和方法的访问偏移,导致我们类和索引id调用失败。

(3)全量dex替换

 合成全新的dex来替换原有的dex文件,这也是适配Davilk虚拟机和Art虚拟机的优秀方案。目前腾讯的Tinker和阿里的sophix所用到方案也是基于此。在后面会着重介绍一下。

主流的冷启动修复

 合成全新的dex来替换原有的dex文件的原理是什么呢?Dalvik和Art都是通过dexfile.loadDex这个方法尝试把一个dex文件解析加载到native中内存,通过dexFile.openDexFIleNative这个native方法对dex进行解析。如果查看这个方法可以发现。Dalvik在尝试加载一个压缩文件的时候,只会把classes.dex加载到内存中,所以如果有多个dex,除了主dex,其他dex都会被忽略。当然dalvik也只支持一个dex,在多dex时需要mutidex库, 从主dex来调用分dex的方法。
 ART默认支持多dex,将补丁类放在主dex,即classes.dex中即可,后续分dex中相同的类是不会被加载的。我们在放置好之后,然后一起打包成一个压缩软件,通过dexFile.loadFIle得到dexFIle对象,将新的dexFIle替换旧的dexElements即可。

多态对于冷启动类加载的影响

 在使用pResClasses预先设置这个方案时,会碰到一个难题,使用多态时往往会无法调用到正确的方法。这是为什么?
 首先我们要明白,多态的原理是什么?
 实现多态的技术一般叫作动态绑定,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。多态一般指的是非静态非私有方法的多态,field和静态方法不具有多态性。
 我们来分析一下为什么会产生多态。假设A是父类,B是子类。首先new B()的执行会尝试加载类B,方法调用链dvmResolveClass->dvmLinkClass->createVtable,此时会为类B创建一个vtable,其实在虚拟机中加载每个类都会为这个类生成一张vtable表,vtable表就是当前类的所有virtual方法的一个数组,当前类和所有继承父类的public/protected/default方法就是virtual方法,因为public/protected/default修饰的方法是可以被继承的。private/static方法不属于这个范畴,所以不能被继承。
子类vtable的大小等于子类virtual方法数+父类vtable的大小。
 1.整体赋值父类vtable到子类的vtable
 2.遍历子类的vtable方法合集,如果方法原型一致,说明是重写父类父方法,那么在想用的索引处,子类重写方法覆盖父类的方法。
 3.若方法不一致,那么把该方法添加到vtable的末尾。方法在vtable的顺序是根据方法被定义的顺序来确定的。


 class Main {
    A a = new A();
    a.methodB();

}

 class A {
    //补丁类中新加的
    public void methodA() {
        System.out.println("this is method A");
    }

    // 原有的
    public void methodB() {
        System.out.println("this is method B");

    }
}


 如果我们在补丁中为A新加了methodA,那么实际上 a.methodB();最后输出的是语句是“this is method A"。出现这种情况的原因是: a.methodB()在底层的实现方式可以理解为vtable[0]=methodB,底层获取了vtable[0],来进行实现。如果新增了方法methodA,那么在底层的映射则是vtable[0]=methodA,vtable[1]=methodB,依旧获取vtable[0]的话,会发现实现的对象变成了methodA。所以,在涉及到了两个类时,只是单纯地考虑其中一个类,有可能会导致方法调用错乱的问题。
 那么全量dex替换是如何解决这个问题的呢?大致思路是利用谷歌的开源的dexmerge方法,将补丁dex和原dex合并为一个完整的dex,然后重新将dex载入内存,那么此时每一个类都会重新载入,vtable也会被刷新,那么这个问题就会被解决。其中需要注意的是如何更细颗粒化得生成补丁dex,避免在合成时造成内存风暴。

冷启动修复需要注意的点

 我们在看一个问题。Application是程序的入口,热修复的启动再早也是不可能早于Application的载入的,所以Application类必定是在老dex中获得,而不是在新的dex中获取。Application初始化时,解析某个类,这时候补丁没有加载,如果解析的类使用到了要修复的类(这时只能加载原始的类,而不能加载补丁中的类),那么就会出现pre-verified问题。

解决方案:
 把Application里面除了热修复框架代码之外的其他代码都剥离开来,单独提出放到一个其他类里面,这样使得Application不会直接用到过多非系统类,这样保证单独拿出来的类和Application处于同一个dex的概率极大。或者用反射方式访问这个单独的类。市面上很多热修复框架都会要求将Application替换为特定的Application,就是希望能够接管实际的Application,将实际的Application和用户代码隔离开来,从而解决这个问题。

 除了修复代码以外,还有一点需要的实现的是资源的修复,比如图片资源,so文件修复等。

资源修复

 资源修复原理:

  1. 构造一个新的AssertManager,并通过反射调用addAssertPath,把这个完整的新资源包加入到assertManager中,这样就得到了一个含有所有新资源的AssertManager
  2. 找到所有之前引用到了原有的AssertManager的地方,通过反射,把应用处替换为AssertManager.

 so文件修复:
 soso文件的载入,都是调用了nativeLoad这个native方法加载so库。所以关键在于如何实现so库的重新载入。 so的注册有动态和静态两种。动态注册的native方法映射通过加载so库过程中调用jni_onload方法调用完成,静态注册的native方法映射是在该native方法第一次执行的时候才完成映射,当然前提是该so库已经加载过了。因此,对于动态注册,我们只需要将so载入地址替换为补丁类中的so地址即可。而对于静态注册,系统JNI为我们提供了解注册的接口,它会使得native方法无论是否实行后,都会重新去进行一次映射。

总结

 我很喜欢《深入探索Android热修复技术原理》一书中所写的,AndFix作者说的话,“AndFix作为早期的热修复方案,在如今优秀的热修方法层出不穷的情况,势必会被淘汰,希望它的思想能给未来的开发者一些启发。”Android热修复需要有对底层非常深刻的认识和见解,在书中,我看到了许多优秀,富有创新性的方案和令人倾佩的开发者的文章,这对于每一个开发者,都是宝贵的学习财富。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章