Android 热修复小结

主流热更新方案对比

QZone超级补丁

  • 实现原理:

    • 将需要修复的类打包成dex补丁文件

    • 客户端下载补丁包后,在下次app启动时检测到补丁包进行加载

    • 在类加载器中会将加载的dex保存到一个数组里pathList:DexPathList,由于双亲委托机制,如果数组前面的dex文件已经包含需要加载的类,则不会从后面的dex中查找该类

    • Path/DexClassLoader->BaseDexClassLoader->ClassLoader->PathDexList->dexElements->makePathElements

    • 所以应用启动时会将补丁包通过反射调用makePathElements方法,生成一个新的数组,然后与旧的dex数组合并,将补丁包的dex放到数组前面,这样就会优先加载修复后的类

  • 优点:兼容性和稳定性比较好,修复的成功率比较高,可以整个类替换

  • 缺点:

    • 如果修复的类比较多,补丁包会比较大,加载补丁包会比较耗时

    • 不能实时修复问题,需要重启后才可以

    • 在低版本dalvik虚拟机中会因为一个类调用另一个Dex中的类,导致修复失败问题;出现这个问题是因为虚拟机对于没有引用另一个dex文件中的类会做一个标记叫做class_is_pre_verified,这样它就不会再去其他dex中查找引用到的类,如果原来引用的类是在同一个dex中,但是修复后的类在另一个dex中,就会报错

    • 解决的方案是编译时钦通过修改字节码,在构造方法中去引用另一个dex中的固定类来避免被虚拟机标记

Tinker

  • 实现原理:整体上与QZone流程一致,主要区别在于它是通过BSDiffer与原dex文件进行对比生成差分包;

  • 优点:

    • 补丁包相比QZone更小

    • 差分包下发到客户端后会重新合成新的dex修复包,它加载时会整体替换旧的dex,避免了跨dex类调用问题

  • 缺点:

    • 不能实时修复问题,需要重启app才可以

    • dex的合并会在新的进程中进行,如果有多个dex需要合并时,合成的失败率可能也会高一些,而且下发的dex补丁包也会较多

  • 资源修复:将下发的补丁包资源通过AssertManager反射调用方法构建,然后替换掉原来的AssertManager

  • so修复:使用Tinker的类去加载so库,它会先查找是否有so库的更新包,如果没有就加载旧的

AndFix

  • 实现原理:核心原理是在native层查找到需要修复的方法结构体ArtMethod,然后把它替换成我们修复后的方法

  • 优点:不需要重启app,可以实时生效

  • 缺点:

    • 兼容性不大好,因为每个Android系统版本对这个ArtMethod结构会有些变化,比较难适配

    • 不支持新增字段、资源替换等

Robust

  • 实现原理:在编译期间,通过Gradle插件进行代码插桩,在每个类里加上一个静态变量,静态变量指向修复后的接口,每个方法内部,插入if/else代码,判断如果静态变量不为空,则执行修复后的代码;修复补丁下发后,会先通过ClassLoader加载到内存中,然后通过反射修改需要修复的那个类里的静态变量,让它指向修复后的类,当下次调用该方法时就会if语句执行修复后的代码

  • 优点:不需要重启app,可以实时生效,兼容性很好

  • 缺点:

    • 对项目代码有侵入性,增加了很多代码量,使得安装包会增大

    • 不支持资源和so修复

版本兼容性问题

  • 反射调用时源码上的差异

    • 低版本上PathDexList中将dex转成数组的方法叫makeDexElements但是高本版上叫makePathElements,参数也不一样
  • Android N混合编译执行问题

    • Android N开始为了解决ART虚拟机安装app比较慢的问题,在安装过程中只编译使用比较多的热代码,剩下的则在系统闲置时去编译

    • 在应用启用时会创建PathClassLoader,在构建PathClassLoader时,底层会优先加载已经编译好的热代码,这就导致正常情况下,我们无法再后续通过将补丁dex文件放到数组最前面来实现修复效果

    • 解决的办法就是,在应用启动时,重新构建一个新的PathClassLoader,然后将旧的PathClassLoader关键数据拷贝过来,忽略掉已经加载了的热代码,然后将我们创建的PathClassLoader替换要原来的ClassLoader

  • class_is_pre_verified问题:

    • 在dalvik虚拟机中,如果一个类没有引用其他dex里的类,就会被标记class_is_pre_verified,被标记后可以优化它的性能,如果修复的类里面,被引用的类又在另一个dex中,就会报错

    • 解决办法是在编译时期通过Gradle插件对代码插桩,在构造方法中去引用另一个dex中固定的类,从而避免被打上标签

资源修复

SO库修复

  • 加载so库的方式:

    • System.loadLibrary(libraryName):传入so库名称,位于libs目录下,最后会复制到apk安装目录

    • System.load(pathName):传入完整so库文件路径

    • 最后都会调用nativeLoad方法去加载so库文件

  • JNI方法动态注册和静态注册区别

    • 静态注册的方法需要在c/c++文件中以固定格式声明,Java开头,后面跟着完整类名+方法名;在方法调用时才去查找映射,效率较低

    • 动态注册方法需要实现JNI_OnLoad方法,并且注册一个JNINativeMethod数组;在so库加载时就完成映射,调用方法时效率高

  • 动态注册Native方法修复

    • 由于动态注册的native方法,方法映射是在so库加载时调用JNI_OnLoad方法完成的;所以只需要再次加载修复后的so库就可以了

    • 在ART虚拟机上是没问题的,但是在dalvik虚拟机里,它是根据so库文件名来判断so库是否已经加载过了,如果已经加载过了就不会去加载修复后的so库;ART虚拟机则是通过文件完整路径来判断是否加载过,所以没这个问题;我们可以通过改so库文件名来达到在dalvik虚拟机上修复的目的

  • 静态注册Native方法修复

    • JNI提供一个方法可以取消注册Native方法,但是重新加载so之后无法保证一定能修复,因为重新加载后的so可能在表中原来so的前面,也可能在后面
  • 所以没有可以同时兼容动态注册和静态注册实时修复so的方案,只能通过重启app进行修复

  • 冷修复so方案有两种:

    • 一种是提供一个加载so库的方法去替换自带的System.loadBinary方法,然后在自定义方法里优先去加载更新文件夹中的so库;这种方式会稍微麻烦点,需要开发者改代码,但是如果是第三方库里的代码就无法修改,这个情况也可以在我们代码中提前加载好第三方so库去实现,优点是没有版本兼容性问题

    • 另一种方法是通过反射修改DexPathList中存放so库路径的数组,将修复后的so路径添加到数组前面,这种方式就存在兼容性问题,如果Android版本对这个类有更新,就需要去适配,但是优点是可以做到无感修复,不需要开发者改代码

Resources资源修复

  • Android L(5.0)以及以上版本只要在原有AssertManager上反射调用addAssetPath方法将新apk资源路径添加进去就可以了

  • 【普遍实现方案】AndroidL(5.0)以下版本,由于native层查找资源时会优先查找旧的资源,所以需要重新创建一个AssetManager对象,并反射替换掉原来的AssetManager

  • 最佳实践:

    • AssetManager里有提供一个destroy方法用于释放资源,可以先调用该方法释放当前AssertManager中的资源,然后重新调用native层的init方法重新初始化当前AssetManager对象,最后再调用addAssetPath添加修复后的资源路径,从而实现不改变原有AssetManager对象进行资源修复

补丁包管理

  • 通过自定义Gradle插件,指定上个版本的混淆结果文件和R资源映射文件,在编译时期遍历所有的class文件并记录md5值,与上一个版本记录的md5值对比,将有修改过的class打包成dex补丁文件;或者打包结束后将生成的apk文件与上个版本apk文件使用差分工具bsdiff生成差分包作为补丁文件

猜你喜欢

转载自blog.csdn.net/guangdeshishe/article/details/129968756