SDK热修复

1,714 阅读6分钟

一、前言

随着业务的迭代发展,消金的业务量也在快速增长。在这个过程中也扩展了一些新的业务,“嵌入式”SDK。也就是把已有的一些业务嵌入到第三方APP里面消金SDK累计接入6个依赖方,如果快狗打车、驾校一点通、58同城本地版未来会有更多接入方,故对代码质量和bug就有了一定的要求。

记忆尤深的两个事情:

1、项目初期产品的期望是只发布一个版本,即项目的开始便是结束。业务不会有迭代,对bug的要求是0。

2、线上出现了问题,具体问题我就不细节描述了。。

二、ClassLoader


1、ClassLoader

类加载器是个抽象类,是所有加载器的父类。主要有两个构造方法一个有参一个无参。传入parent基本上不允许为null。每个ClassLoader实例都有一个父类加载器的引用。


有两个重要的方法loadClass()、findClass()。loadClass加载类时首先判断是否已经加载过,加载过直接return
否则就去调用传入parent的loadClass一层一层的去找如果还是找不到才会由自己加载。最后还是没有找到则会调用findClass抛出ClassNotFoundException异常。

这个过程就是就双亲委派模型。

2、双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。

只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

好处:

1、可以避免类的重复加载

2、避免了Java的核心API被篡改(如果自己编写了个Object类,用自定义的ClassLoader去加载那java的核心代码也无法保证)


这也就是Tinker发完补丁包后需要冷启动才能应用成功。

Tinker原理:

  1. 通过反射获取到DexPathList属性对象pathList
  2. 进行合并Element[],把path.dex合并的放到dexElements最前面去
  3. 根据双亲委派模型就会先加载修复后dex

3、DexClassLoader

主要用于加载我们修复bug后的dex

  • dexPath:dex、jar文件的路径
  • optimizedDirectory:缓存路径,存放优化后的 dex(Android 8.0后为null)
  • librarySearchPath:native库的路径,一般都穿空
  • parent:父类加载器

三、patch.dex


在我们正常的业务逻辑中一个Activity会有很多方法来实现各种各样的逻辑,避免不了个别方法会出现异常比如我们常见的NullPointerException。

Hunk会在重要的类中添加ChangeMethod接口,和方法最前面插入空方法,当methodB出现异常后我们根据收集到的崩溃信息来判断具体出现问题的代码。

然后对methodB插入的这个空方法来实现对有问题代码的修改。

判断methodB入的方法是有修改,如果有这直接retrun不会再调用有问题的代码。这样就达到我们修改bug的目的。


1、修复问题代码


比如我们要获取北京天气内容并显示在页面上,天气内容写死在代码里。这时候如果我们想改变天气让北京下雨怎么做那?

    • 通过工具定位到问题所在类、代码行数
    • 检查导致北京不下雨的原因是什么?
    • 修复北京的天气为暴雨。

2、生成dex

.java-->.class→.dex

    我们通过DexClassLoader加载修复后的dex来达到修复的bug的能力。首先生成dex
    • 把修复后的代码run一下。在build/intermediates/classes/debug/包名 下会有对应的class文件
    • 找到对应的class文件copy到和项目结构一样的包名下
    • 通过Android SDK 的dx.bat把class打包成dex文件(dx --dex --output :\dex\patch.dex :\dex\)
    • 把生成后dex文件下载到手机的固定位置,通过dexClassLoader

3、加载dex

找到生成后的dex的指定目录,通过classloader加载changeMethod的具体实现类,把加载到的具体实现类赋值给要修复bug的类中的静态变量。

再次调用这个方法时会先调用我们的hunk实现,这样就达成了我们要修复bug的能力。

(举例:小明想了解当天的天气状况,当小明拿出手机要查看的时候我告诉了他天气状态。这样小明就不需要拿手机查看天气了)

问题一:如何把changeMethod的具体实现类赋值给有Bug类的静态变量?


首先通过dexClassLoader加载有bug的HunkAcitivty,因为这个类里面有一个我们声明静态的ChangeMethod接口。然后通过反射拿到这个类里面的所有字段进行对比直到拿到ChangeMethod。

再加载我们打成dex里面ChangeMethod实现类,把具体实现类赋值给bug类里面ChangeMethod接口。

这样就完成赋值操作

问题二:怎样实现加载修复后的具体方法?


实现accessDispatch方法,也就是每个方法前面插入的代理方法。还是通过dexClassLoader加载指定类,再通过反射调用这个类里方法。

这样就完成一整套的流程。

4、效果

代码以开源到github:欢迎star https://github.com/blindmonk/Android

四、插桩(之前写过直接插桩超链接已加上)

1、打包流程

2、自定义Gradle插件

3、ASM字节码

如果SDK模块比较多类比较复杂,可以使用字节码插桩的形式为每个类每个方法进行插桩,这样就避免了手动去写代码。

不过这样也有一些弊端会使代码量增大,包的体积增大,也会有一些不必要的代码。

各有各的优势和劣势,取决于自己项目的实际情况选择最优的方案。

总之没有最好、最优的热修复框架只有最适合当前项目的。

五、未来优化方向

1、补丁上传?

2、补丁下发?

3、补丁加载?

3、补丁失效策略?

对补丁的管理也是比较繁琐的,补丁下发的版本平台管理(像tinker的大于1W下载量就会收费),避免补丁被加载时被篡改做加密解密处理

如果补丁本身就有异常需要针对补丁做失效策略,对补丁做标记让不让其加载

理论上只要成功加载补丁对补丁的应用成功率可以打达到99%的,当然需要也有必要对补丁的成功率做统计分析。