代码修复
代码修复有两大主要方案,一种是阿里系的底层替换方案,另一种是腾讯系的类加载方案。
这两类方案各有优劣:
- 底层替换方案限制颇多,但时效性最好,加载轻快,立即见效。
- 类加载方案时效性差,需要重新冷启动才能见效,但修复范围广,限制少
底层替换方案
底层替换方案是在已经加载了的类中直接替换掉原有方法,是在原来类的基础上进行修改的。因而无法实现对与原有类进行方法和字段的增减,因为这样将破坏原有类的结构。
一旦补丁类中出现了方法的增加和减少,就会导致这个类以及整个Dex的方法数的变化。方法数的变化伴随着方法索引的变化,这样在访问方法时就无法正常地索引到正确的方法了。如果字段发生了增加和减少,和方法变化的情况一样,所有字段的索引都会发生变化。并且更严重的问题是,如果在程序运行中间某个类突然增加了一个字段,那么对于原先已经产生的这个类的实例,它们还是原来的结构,这是无法改变的。而新方法使用到这些老的实例对象时,访问新增字段就会产生不可预期的结果。
这是这类方案的固有限制,而底层替换方案最为人诟病的地方,在于底层替换的不稳定性。
传统的底层替换方式,不论是 Dexposed、Andfix或者其他安全界的Hook方案,都是直接依赖修改虚拟机方法实体的具体字段。例如,改Dalvik方法的jni函数指针、改类或方法的访问权限等等。这样就带来一个很严重的问题,由于 Android是开源的,各个手机厂商都可以对代码进行改造,而 Andfix里ArtMethod的结构是根据公开的 Android源码中的结构写死的。如果某个厂商对这个ArtMethod结构体进行了修改,就和原先开源代码里的结构不一致,那么在这个修改过了的设备上,通用性的替换机制就会出问题。这便是不稳定的根源。
类加载方案
类加载方案的原理是在app重新启动后让 Classloader去加载新的类。因为在app运行到一半的时候,所有需要发生变更的类已经被加载过了,在 Android上是无法对一个类进行卸载的。如果不重启,原来的类还在虚拟机中,就无法加载新类因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会 Resolve为新类。从而达到热修复的目的。
资源修复
主要需要修改资源包的内容。正常情况下,资源包就是整个apk安装包,如果想要新增一个原有安装包里不存在的资源,就必须修改资源包的内容。所以,就必须想办法把原有安装包替换为新资源包,或者把新的资源包插入程序的查找过程中。
普通资源修复方案
目前市面上的很多资源热修复方案基本上都是参考了 Instant Run的实现。实际上, Instant Run的推出正是推动这次热修复浪潮的主因,各家热修复方案在代码、资源等方面的实现,很大程度上地参考了 Instant Run的代码,而资源修复方案正是被拿来用到最多的地方。
简要说来, Instant Run中的资源热修复分为两步:
- 构造一个新的 AssetManager,并通过反射调用 addAssetPath,把这个完整的新资源包加入到 AssetManager中。这样就得到了一个含有所有新资源的 AssetManager。
- 找到所有之前引用到原有 AssetManager的地方,通过反射,把引用处替换为 AssetManager。
sophix资源修复的方案
- 不修改 AssetManager的引用处,替换更快更完全。
- 不必下发完整包,补丁包中只包含有变动的资源。
- 不需要在运行时合成完整包。不占用运行时计算和内存资源。
-
对于新增资源,直接加入补丁包,然后新代码里直接 就可以了
-
对于减少资源,我们只要不使用它就行了,因此不用考虑这种情况影响补丁包
-
对于修改资源,比如替换了一张图片之类的情况。我们把它视为新增资源,在打入补丁的时候,代码在引用处也会做相应修改,也就是直接把原来使用旧资源id的地方变为新id
so库的修复
普通so库修复方案
在Android系统中,所有的so库都是由System.load进行加载的,因此只要找到办法在加载的时候优先加载补丁包的so库,而不是原有安装包的so库,就能进行完整的底层代码替换了。
sophix so库修复的方案
so库的修复本质是对native方法的修复和替换。可以采用类似类修复反射注入的方式。把补丁so库的路径插入到nativeLibraryDirectories数组的最前边。
具体实现可以参考官方文档