热修复

Posted by ooftf on April 12, 2021

热更新技术的关键点

  1. 如何生成补丁
  2. 如何让补丁替换相应的资原
    • 代码
    • res 下的资源
    • manifest
    • so文件

代码修复

  • 底层替换方案
    底层替换方案是在已经加载了的类中直接替换掉原有方法,是在原来类的基础上进行修改的。因而无法实现对与原有类进行方法和字段的增减,因为这样将破坏原有类的结构。

    一旦补丁类中出现了方法的增加和减少,就会导致这个类以及整个Dex的方法数的变化。方法数的变化伴随着方法索引的变化,这样在访问方法时就无法正常地索引到正确的方法了。如果字段发生了增加和减少,和方法变化的情况一样,所有字段的索引都会发生变化。并且更严重的问题是,如果在程序运行中间某个类突然增加了一个字段,那么对于原先已经产生的这个类的实例,它们还是原来的结构,这是无法改变的。而新方法使用到这些老的实例对象时,访问新增字段就会产生不可预期的结果。

    具体实现:在JNI层,替换整个ArtMethod

  • 类加载方案 类加载方案的原理是在app重新启动后让Classloader去加载新的类。因为在app运行到一半的时候,所有需要发生变更的类已经被加载过了,在Android上是无法对一个类进行卸载的。如果不重启,原来的类还在虚拟机中,就无法加载新类。因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会Resolve为新类。从而达到热修复的目的。

    直接利用Android原先的类查找和合成机制,快速合成新的全量dex。这么一来,我们既不需要处理合成时方法数超过的情况,对于dex的结构也不用进行破坏性重构。

    如下图所示:重新编排包中dex的顺序。这样,在虚拟机查找类的时候,会优先找到classes.dex中的类,然后才是classes2.dex、classes3.dex,也可以看做是dex文件级别的类插桩方案。这个方式十分巧妙,它对旧包与补丁包中classes.dex的顺序进行了打破与重组,最终使得系统可以自然地识别到这个顺序,以实现类覆盖的目的。这将会大大减少合成补丁的开销。

资原修复

目前市面上的很多资源热修复方案基本上都是参考了Instant Run的实现。实际上,Instant Run的推出正是推动这次热修复浪潮的主因,各家热修复方案,在代码、资源等方面的实现,很大程度上地参考了Instant Run的代码,而资源修复方案正是被拿来用到最多的地方。

简要说来,Instant Run中的资源热修复分为两步:

构造一个新的AssetManager,并通过反射调用addAssetPath,把这个完整的新资源包加入到AssetManager中。这样就得到了一个含有所有新资源的AssetManager。 找到所有之前引用到原有AssetManager的地方,通过反射,把引用处替换为AssetManager。 我们发现,其实大量代码都是在处理兼容性问题和找到所有AssetManager的引用处,真正的替换的逻辑其实很简单。

我们的方案没有直接使用Instant Run的技术,而是另辟蹊径,构造了一个package id为0x66的资源包,这个包里只包含改变了的资源项,然后直接在原有AssetManager中addAssetPath这个包就可以了。由于补丁包的package id为0x66,不与目前已经加载的0x7f冲突,因此直接加入到已有的AssetManager中就可以直接使用了。补丁包里面的资源,只包含原有包里面没有而新的包里面有的新增资源,以及原有内容发生了改变的资源。并且,我们采用了更加优雅的替换方式,直接在原有的AssetManager对象上进行析构和重构,这样所有原先对AssetManager对象的引用是没有发生改变的,所以就不需要像Instant Run那样进行繁琐的修改了。

可以说,我们的资源修复方案,优越性超过了Google官方的Instant Run方案。整个资源替换的方案优势在于:

不修改AssetManager的引用处,替换更快更完全。(对比Instanat Run以及所有copycat的实现) 不必下发完整包,补丁包中只包含有变动的资源。(对比Instanat Run、Amigo等方式的实现) 不需要在运行时合成完整包。不占用运行时计算和内存资源。(对比Tinker的实现) 所以,我们不要被所谓的“官方实现”束缚住手脚,其实Instant Run的开发团队和Android framework的开发团队并不是同一个团队,他们对于Android系统机制的理解未必十分深入。只要你认真研读系统代码,实现一个比官方更好的方案绝非难事。所以我想说的是,要想实现技术方案的突破,首先就需要破除所谓“权威”的观念。

资源修复的更多技术细节,可通过这篇文章一探究竟:Android热修复升级探索——资源更新之新思路

so库修复

so库的修复本质上是对native方法的修复和替换。

我们知道JNI编程中,native方法可以通过动态注册和静态注册两种方式进行。动态注册的native方法必须实现JNI_OnLoad方法,同时实现一个JNINativeMethod[]数组,静态注册的native方法必须是Java+类完整路径+方法名的格式。

Nativi

动态注册的native方法映射通过加载so库过程中调用JNI_OnLoad方法调用完成,静态注册的native方法映射是在该native方法第一次执行的时候才完成映射,当然前提是该so库已经load过。

我们采用的是类似类修复反射注入方式。把补丁so库的路径插入到nativeLibraryDirectories数组的最前面,就能够达到加载so库的时候是补丁so库,而不是原来so库的目录,从而达到修复的目的。

SO

采用这种方案,完全由Sophix在启动期间反射注入patch中的so库。对开发者依然是透明的。不用像某些其他方案需要手动替换系统的System.load来实现替换目的。

热修复技术

  • 腾讯QQ控件超级补丁技术
  • 微信Tinker
  • 饿了么Amigo
  • 美团Robust
  • alibaba的AndFix、sophix

热修复技术对比图

相关资料

《深入探索Android热修复技术原理》 —— 业界首部全方位系统介绍热修复原理书籍,从阿里Sophix方案开发过程入手权威解读!

Sophix技术概览

即时生效的代码热修复

资源热更新技术详解

Dalvik下冷启动修复的新探索