派系 | 框架 |
---|---|
阿里系 | AndFix⚠️、Dexposed⚠️、HotFix⚠️、Sophix |
腾讯系 | Tinker、超级补丁、QFix |
知名公司 | 美团Robust、饿了么Amigo⚠️、蘑菇街Aceso⚠️ |
其他 | RocooFix⚠️、Nuwa⚠️、AnoleFix⚠️ |
特性 | Sophix | Tinker | 超级补丁 | Robust |
---|---|---|---|---|
即时生效 | 支持 | 不支持 | 不支持 | 不支持 |
方法替换 | 支持 | 支持 | 支持 | 支持 |
类替换 | 支持 | 支持 | 支持 | 不支持 |
类结构修改 | 支持 | 支持 | 不支持 | 不支持 |
资源替换 | 支持 | 支持 | 支持 | 不支持 |
so替换 | 支持 | 支持 | 不支持 | 不支持 |
补丁包大小 | 较小 | 较小 | 较大 | 一般 |
性能损耗 | 较小 | 较大 | 较大 | 较小 |
侵入式打包 | 无侵入 | 侵入 | 侵入 | 侵入 |
代码修复主要有3中方案:类加载方案、底层替换方案、Instant Run方案。
采用类加载方案的主要是 Tinker、超级补丁、QFix、Amigo 和 Nuwa 等。
【方法数 65536 限制】
由于 DVM 指令集的方法调用指令 invoke-kind
索引为16bits,即最多能引用 65535 个方法。因此,当一个 dex 文件中法方法数超过 65535 个时,就会抛出编译期异常:com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536
。
【LinearAlloc 限制】
DVM 中的 LinearAlloc 是一个固定的缓存区,当方法数超出了缓存区的大小时会报错。因此,在安装应用时可能会提示 INSTALL_FAILED_DEXOPT
。
【分包方案】
在 Android 的类加载过程中,一个重要环节是调用 DexPathList
的 findClass
方法。
// 每一个dex文件,都对应一个Element元素,并有序排列
private Element[] dexElements;
public Class<?> findClass(String name, List<Throwable> suppressed) {
// 依次遍历dex文件数组
for (Element element : dexElements) {
// element.findClass内部会调用DexFile的loadClassBinaryName方法查找类
Class<?> clazz = element.findClass(name, definingContext, suppressed);
// 找到了这个类,就返回
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
根据类的查找流程:
Key.class
进行修改,再将 Key.class
打包成补丁包 Patch.dex
。Patch.dex
放在 dexElements
数组的第一个元素。Patch.dex
中的 Key.class
会优先加载,而存在 Bug 的 Key.class
就不会被加载。具体到实现细节上,不同的框架就有些差异了。
dexElements
数组的第一个元素。dexElements
数组的第一个元素。采用底层替换方案的主要是 AndFix、Dexposed、HotFix 和 Sophix。
优点:不需要重启 APP,立即生效。
Java 层的每个方法在 ART 中都对应着一个 ArtMethod 的结构体(包含 Java 方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等),只要把原方法的结构体内容替换为新的结构体内容,则在调用原来方法的时候,真正执行的指令是新方法的指令,就可以实现热修复。
在 art/runtime/art_method.h
文件中,定义了 ArtMethod 的结构体内容。
class ArtMethod FINAL {
/* ... */
GcRoot declaring_class_;
std::atomic access_flags_;
uint32_t dex_method_index_;
uint16_t method_index_;
uint16_t hotness_count_;
uint16_t imt_index_;
struct PtrSizedFields {
ArtMethod** dex_cache_resolved_methods_;
void* data_;
void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
}
不同的框架采用了不同方案:
无论采用哪种方案,由于类加载后,类的结构和方法数量就已经固定了,因此该方案有以下不适场景:
Sophix 结合了底层替换方案和类加载方案各自的优点,以底层替换方案为主,类加载方案为辅,在热部署无法使用的情况下,自动降级为冷部署。
在 Android Studio 2.0 版本上,支持了一个新特性 Instant Run,实现了对代码修改的实时生效(热插拔)。
采用 Instant Run 方案的主要是 Robust 和 Aceso。
在第一次构建 Apk 时:
$change
的成员变量,它实现了 IncrementalChange
接口。public class TestActivity {
// 注入一个类型为IncrementalChange的成员
IncrementalChange localIncrementalChange = $change;
public void onCreate(Bundle savedInstanceState){
// 当localIncrementalChange不为null时,可能会执行到access$dispatch从而替换掉之前老的逻辑
if (localIncrementalChange != null) {
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;)V", new Object[] {
this, paramBundle });
return;
}
super.onCreate(savedInstanceState);
}
}
当我们点击 Android Studio 的 InstantRun 按钮时:
$change
为 null
,执行方法中的旧逻辑。TestActivity$override
和 AppPatchesLoaderImpl
类。AppPatchesLoaderImpl
类的 getPatchedClasses
方法会返回被修改的类的列表,根据这个列表,TestActivity
中的 $change
会被赋值为 TestActivity$override
。access$dispatch()
方法会执行 TestActivity$override
类中的 onCreate
方法,从而实现对现有 onCreate
方法的修改。以 Robust 为例
PatchesInfoImpl.java
和 Patch.java
的 patch.dex
到客户端,用 DexClassLoader
加载 patch.dex
,反射拿到 PatchesInfoImpl.java
这个 class 并创建对象。getPatchedClassesInfo
方法,获得需要修复的 class 的混淆后名字,再反射得到当前运行环境中的该 class。changeQuickRedirect
字段赋值为用 patch.dex
中的 Patch.java
这个 class new
出来的对象。很多热修复框架的资源修复都参考了 Instant Run 的资源修复原理。由于 Instant Run 不是 Android 的源码,需要反编译才能知道。
Instant Run 资源修复的核心逻辑在 MonkeyPatcher
类的 monkeyPatchExistingResources
方法中。
public class MonkeyPatcher {
public static void monkeyPatchExistingResources(
Context context, String externalResourceFile, Collection<Activity> activities) {
if (externalResourceFile == null) {
return;
}
try {
// 反射创建新的AssetManager
AssetManager newAssetManager = AssetManager.class.getConstructor(
new Class[0]).newInstance(new Object[0]);
Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
"addAssetPath", new Class[]{
String.class});
mAddAssetPath.setAccessible(true);
// 反射调用addAssetPath方法加载外部资源
if (((Integer) mAddAssetPath.invoke(
newAssetManager, new Object[]{
externalResourceFile})).intValue() == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod(
"ensureStringBlocks", new Class[0]);
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
if (activities != null) {
for (Activity activity : activities) {
Resources resources = activity.getResources();
try {
// 把Resources中的mAssets替换为newAssetManager
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
/* ... */
}
// 获取Activity的主题
Resources.Theme theme = activity.getTheme();
try {
try {
// 把Resources.Theme中的mAssets替换为newAssetManager
Field ma = Resources.Theme.class.getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(theme, newAssetManager);
} catch (NoSuchFieldException ignore) {
/* ... */
}
/* ... */
} catch (Throwable e) {
/* ... */
}
}
Collection<WeakReference<Resources>> references = null;
/* ...根据不同SDK版本,用不同方式得到Resources的弱引用集合 */
for (WeakReference<Resources> wr : references) {
Resources resources = wr.get();
if (resources != null) {
try {
// 把每个Resources中的mAssets替换为newAssetManager
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
/* ... */
}
resources.updateConfiguration(
resources.getConfiguration(), resources.getDisplayMetrics());
}
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
}
资源热修复总结为两个步骤:
addAssetPath
方法加载外部的资源。mAssets
字段的引用全部替换为新创建的 AssetManager 对象。Android 的动态链接库主要是 so 库。
加载 so 主要用到了 System
类的 load
和 loadLibarary
方法。
public final class System {
// 传入so的名字,会直接从系统的目录去加载so文件,
// 系统的路径包括/data/data/${package_name}/lib、/system/lib、/vender/lib等
public static void load(String filename) {
Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
// 传入so的绝对路径,直接从这个路径加载自定义外部so文件
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
}
实际上这两个方法最后都调用 nativeLoad
这个 native 方法去加载 so 库,参数 fileName
为 so 库在磁盘中的完整路径名。
而 nativeLoad 会调用 LoadNativeLibrary 函数来实现 so 的加载:
通过 javah -jni 命令生成的包含 JNI 的头文件,接口的命名方式一般是 Java_
,程序执行时系统会根据这种命名规则来调用对应的 Native 方法。
在加载函数库(.a或.so)的时候进行注册,即在 JNI_OnLoad 方法里进行注册。
System.load
方法来接管 so 的加载入口。Tinker 就是使用 System.load
方法加载自定义.so文件。
System.load
加载该目录下的 so 文件。