漫聊Android 热修复原理

前言

这篇算是我2021年的第一篇,开个好头。这次聊的主题是热修复。

一、简介

1、分类:

  • 代码修复
  • 资源修复
  • 动态链接库修复

2、修复框架:

  • 阿里系的AndFix、Sophix;不支持类替换,支持即时生效,方法替换
  • 腾讯系的Tinker、QFix;不支持即时生效,其他的都支持
  • 美团的Robust等;不支持类替换,支持即时生效,方法替换

二、资源修复

很多资源修复的框架都参考了Instant Run 原理。因此Instant Run 原理需要先搞明白。

1、Instant Run 原理

Instant Run是Android 2.0引入的运行机制,可以减少二次构建的时间。分三种模式:

  • Hot Swap 热部署;效率最高,不需要重新启动APP 、Activity。例如:修改现有方法的代码时会使用
  • Warm Swap 温部署;APP 不需要重启,Activity 需要重启。例如:修改、删除一个资源时会使用。
  • Cold Swap 冷部署;APP需要重启;例如添加、删除或者修改一个字段和方法等。
    无论使用哪种部署,都不需要重新安装APP。

2、修复资源分两个步骤:

  • 创建新的AssertManager,通过反射调用addAssetPath方法加载外部资源。
  • 将AssertManager类型的mAssets字段的引用全部换成新创建的AssertManager。
    关于AssertManager 的内容,我会在插件化总结中会详细讲解

总结:热修复资源的方式和插件化中加载资源的方式是一样的。

三、代码修复

  • 底层替换:
  • 类加载方案:
  • Instant Run 方案:

1、类加载方案

  • 类加载方案基于Dex分包方案。
  • 65536 的限制;原因是DVM Bytecode 的限制,DVM 指令集的方法调用指令索引为16位,最多只能引用65535个方法。

2、Dex 分包简介:

Dex分包做的就是在打包时,将应用代码分成多个Dex,将应用启动时必须要用到的类和这些类的直接引用类放在主Dex中,其他代码放在次Dex中。当应用启动时,先启动主Dex,然后再动态的加载次Dex,从而缓解主Dex的65536 的限制。 (这里有个潜在的坑,就是插件化时会遇到类找不到的崩溃,这里就和分dex有一定的关系,我之前在和Nubia浏览器那边联调接入时,就遇到过这个潜在的坑,其实就是就是在系统分dex时,没有将目标类打到主dex中,导致在启动时先从主dex中找,找不到,而目标类被系统打到其他dex中了,例如dex2中,这个时候可也反编译看看。谷歌官方是给过一个方案可以自行调节dex是否可以打入到主dex的配置,这里我补充下)

3、补充:

声明在主Dex文件中需要的类:
为 Dalvik 可执行文件分包构建每个 DEX 文件时,构建工具会执行复杂的决策制定来确定主要 DEX 文件中需要的类,以便应用能够成功启动。如果启动期间需要的任何类未在主 DEX 文件中提供,那么您的应用将崩溃并出现错误 java.lang.NoClassDefFoundError。

上面这个错误在插件化中经常出现,下面谈谈解决办法:
必须使用构建类型中的 multiDexKeepFile 或 multiDexKeepProguard 属性声明它们,以手动将这些其他类指定为主 DEX 文件中的必需项。如果类在 multiDexKeepFilemultiDexKeepProguard 文件中匹配,则该类会添加至主 DEX 文件。具体操作如下:

方式一:

  • 创建一个名为 multidex-config.txt 的文件。
  • 在这个文件里面添加需要声明的类的完整路径。
  • 将multiDexKeepFile在gradle中声明,代码如下:
android {
    buildTypes {
        release {
            multiDexKeepFile file 'multidex-config.txt'
           ...
        }
    }
}

注意:Gradle 会读取相对于 build.gradle 文件的路径,因此如果 multidex-config.txt 与 build.gradle 文件在同一目录中,以上示例将有效

方式二:

multiDexKeepProguard 文件使用与 Proguard 相同的格式,并且支持整个 Proguard 语法
*创建一个名为 multidex-config.pro文件

  • 在这个文件里面添加需要配置的类,语法和Proguard一致,例如:
    -keep class com.example.MyClass
    -keep class com.example.MyClassToo
  • 将multiDexKeepProguard在gradle中声明,代码如下:
android {
    buildTypes {
        release {
            multiDexKeepProguard 'multidex-config.pro'
            ...
        }
    }
}

4、类加载的原理:

ClassLoader (Android系统默认加载的类是BaseDexClassLoader)加载类时,会调用DexPathList的findClass方法,每一个Dex文件对应一个Element,多个Element 组成一个dexFileElements数组,Element 内部封装了DexFile,这个DexFile 用于加载Dex文件。查找类时,会遍历这个Element 的数组,然后调用里面的DexFile ——》loadClassBinaryName()方法查找类。如果找到了,就返回,没找到则找下一个Element 的Dex中查找。【建议看看源码】

根据上述原理,我们仅需要将修改好的(例如key.class)文件打包成包含dex的补丁包Patch.jar,放在Element 数组dexElement的第一个元素,这样会首先找到Patch.dex中的key.class文件,排在后面的key.class由于双亲委托机制,不会再被加载。

类加载方案需要重新启动APP后让ClassLoader重新加载类,原因是类无法被卸载,因此采用类加载方案不会立即生效。

腾讯的Tinker 是将新旧APK 做了diff,得到Patch.dex,再将Patch.dex 与手机中APK的classes.dex做合并,生成新的classes.dex,然后在运行时通过反射将classes.dex 放在Element 数组的第一个元素。腾讯系用的多

5、底层替换

底层替换方案不会再次加载新类,而是直接在Native层修改原有的类,主要实现是阿里系的

6、Instant Run 方案

在构建时使用ASM 的方式插入代码,这个涉及到字节码的操作,内容太多,以后单独出一篇文章。

四、动态链接库修复

修复so
实现方案:
• 将so补丁插入到NativeLibraryElement数组的前部,让so补丁的路径先被返回和加载
• 调用System的load方法来接管so 的加载入口。
Coffer 寄语:
热修复的原理和插件化有很多相似的地方,两者只要搞明白其中一个,就能很容易的理解另一个的实现。这篇文章有一部分来自《Android 进阶解密》,还有一部分来自谷歌的官方文档,当然了,自己的总结和理解也少不了。

猜你喜欢

转载自blog.csdn.net/qq_26439323/article/details/112401709