相关文章
解析ClassLoader系列html
在Android应用开发中,热修复技术被愈来愈多的开发者所使用,也出现了不少热修复框架,好比:AndFix、Tinker、Dexposed和Nuwa等等。若是只是会这些热修复框架的使用那意义并不大,咱们还须要了解它们的原理,这样无论热修复框架如何变化,只要基本原理不变,咱们就能够很快的掌握它们。这一个系列不会对某些热修复框架源码进行解析,而是讲解热修复框架的通用原理。java
在开发中咱们会遇到以下的状况:android
为了解决上面的问题,热修复框架就产生了。对于Bug的处理,开发人员不要过于依赖热修复框架,在开发的过程当中仍是要按照标准的流程作好自测、配合测试人员完成测试流程。数组
热修复框架的种类繁多,按照公司团队划分主要有如下几种:缓存
类别 | 成员 |
---|---|
阿里系 | AndFix、Dexposed、阿里百川、Sophix |
腾讯系 | 微信的Tinker、QQ空间的超级补丁、手机QQ的QFix |
知名公司 | 美团的Robust、饿了么的Amigo、美丽说蘑菇街的Aceso |
其余 | RocooFix、Nuwa、AnoleFix |
虽然热修复框架不少,但热修复框架的核心技术主要有三类,分别是代码修复、资源修复和动态连接库修复,其中每一个核心技术又有不少不一样的技术方案,每一个技术方案又有不一样的实现,另外这些热修复框架仍在不断的更新迭代中,可见热修复框架的技术实现是繁多可变的。做为开发需须要了解这些技术方案的基本原理,这样就能够以不变应万变。bash
部分热修复框架的对好比下表所示。微信
特性 | AndFix | Tinker/Amigo | QQ空间 | Robust/Aceso |
---|---|---|---|---|
即时生效 | 是 | 否 | 否 | 是 |
方法替换 | 是 | 是 | 是 | 是 |
类替换 | 否 | 是 | 是 | 否 |
类结构修改 | 否 | 是 | 否 | 否 |
资源替换 | 否 | 是 | 是 | 否 |
so替换 | 否 | 是 | 否 | 否 |
支持gradle | 否 | 是 | 否 | 否 |
支持ART | 是 | 是 | 是 | 是 |
支持Android7.0 | 是 | 是 | 是 | 是 |
咱们能够根据上表和具体业务来选择合适的热修复框架,固然上表的信息很难作到彻底准确,由于部分的热修复框架还在不断更新迭代。 从表中也能够发现Tinker和Amigo拥有的特性最多,是否是就选它们呢?也不尽然,拥有的特性多也意味着框架的代码量庞大,咱们须要根据业务来选择最合适的,假设咱们只是要用到方法替换,那么使用Tinker和Amigo显然是大材小用了。另外若是项目须要即时生效,那么使用Tinker和Amigo是没法知足需求的。对于即时生效,AndFix、Robust和Aceso都知足这一点,这是由于AndFix的代码修复采用了底层替换方案,而Robust和Aceso的代码修复借鉴了Instant Run原理,如今咱们就来学习代码修复。app
代码修复主要有三个方案,分别是底层替换方案、类加载方案和Instant Run方案。框架
类加载方案基于Dex分包方案,什么是Dex分包方案呢?这个得先从65536限制和LinearAlloc限制提及。 65536限制 随着应用功能愈来愈复杂,代码量不断地增大,引入的库也愈来愈多,可能会在编译时提示以下异常:ide
com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536
复制代码
这说明应用中引用的方法数超过了最大数65536个。产生这一问题的缘由就是系统的65536限制,65536限制的主要缘由是DVM Bytecode的限制,DVM指令集的方法调用指令invoke-kind索引为16bits,最多能引用 65535个方法。 LinearAlloc限制 在安装时可能会提示INSTALL_FAILED_DEXOPT。产生的缘由就是LinearAlloc限制,DVM中的LinearAlloc是一个固定的缓存区,当方法数过多超出了缓存区的大小时会报错。
为了解决65536限制和LinearAlloc限制,从而产生了Dex分包方案。Dex分包方案主要作的是在打包时将应用代码分红多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到主Dex中,其余代码放到次Dex中。当应用启动时先加载主Dex,等到应用启动后再动态的加载次Dex,从而缓解了主Dex的65536限制和LinearAlloc限制。
Dex分包方案主要有两种,分别是Google官方方案、Dex自动拆包和动态加载方案。由于Dex分包方案不是本章的重点,这里就再也不过多的介绍,咱们接着来学习类加载方案。 在Android解析ClassLoader(二)Android中的ClassLoader中讲到了ClassLoader的加载过程,其中一个环节就是调用DexPathList的findClass的方法,以下所示。 libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {//1
Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
复制代码
Element内部封装了DexFile,DexFile用于加载dex文件,所以每一个dex文件对应一个Element。 多个Element组成了有序的Element数组dexElements。当要查找类时,会在注释1处遍历Element数组dexElements(至关于遍历dex文件数组),注释2处调用Element的findClass方法,其方法内部会调用DexFile的loadClassBinaryName方法查找类。若是在Element中(dex文件)找到了该类就返回,若是没有找到就接着在下一个Element中进行查找。 根据上面的查找流程,咱们将有bug的类Key.class进行修改,再将Key.class打包成包含dex的补丁包Patch.jar,放在Element数组dexElements的第一个元素,这样会首先找到Patch.dex中的Key.class去替换以前存在bug的Key.class,排在数组后面的dex文件中的存在bug的Key.class根据ClassLoader的双亲委托模式就不会被加载,这就是类加载方案,以下图所示。
类加载方案须要重启App后让ClassLoader从新加载新的类,为何须要重启呢?这是由于类是没法被卸载的,所以要想从新加载新的类就须要重启App,所以采用类加载方案的热修复框架是不能即时生效的。 虽然不少热修复框架采用了类加载方案,但具体的实现细节和步骤仍是有一些区别的,好比QQ空间的超级补丁和Nuwa是按照上面说得将补丁包放在Element数组的第一个元素获得优先加载。微信Tinker将新旧apk作了diff,获得patch.dex,而后将patch.dex与手机中apk的classes.dex作合并,生成新的classes.dex,而后在运行时经过反射将classes.dex放在Element数组的第一个元素。饿了么的Amigo则是将补丁包中每一个dex 对应的Element取出来,以后组成新的Element数组,在运行时经过反射用新的Element数组替换掉现有的Element 数组。
采用类加载方案的主要是以腾讯系为主,包括微信的Tinker、QQ空间的超级补丁、手机QQ的QFix、饿了么的Amigo和Nuwa等等。
与类加载方案不一样的是,底层替换方案不会再次加载新类,而是直接在Native层修改原有类,因为是在原有类进行修改限制会比较多,不可以增减原有类的方法和字段,若是咱们增长了方法数,那么方法索引数也会增长,这样访问方法时会没法经过索引找到正确的方法,一样的字段也是相似的状况。 底层替换方案和反射的原理有些关联,就拿方法替换来讲,方法反射咱们能够调用java.lang.Class.getDeclaredMethod,假设咱们要反射Key的show方法,会调用以下所示。
Key.class.getDeclaredMethod("show").invoke(Key.class.newInstance());
复制代码
Android 8.0的invoke方法,以下所示。 libcore/ojluni/src/main/java/java/lang/reflect/Method.java
@FastNative
public native Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
复制代码
invoke方法是个native方法,对应Jni层的代码为: art/runtime/native/java_lang_reflect_Method.cc
static jobject Method_invoke(JNIEnv* env, jobject javaMethod, jobject javaReceiver, jobject javaArgs) {
ScopedFastNativeObjectAccess soa(env);
return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs);
复制代码
Method_invoke函数中又调用了InvokeMethod函数: art/runtime/reflection.cc
jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod, jobject javaReceiver, jobject javaArgs, size_t num_frames) {
...
ObjPtr<mirror::Executable> executable = soa.Decode<mirror::Executable>(javaMethod);
const bool accessible = executable->IsAccessible();
ArtMethod* m = executable->GetArtMethod();//1
...
}
复制代码
注释1处获取传入的javaMethod(Key的show方法)在ART虚拟机中对应的一个ArtMethod指针,ArtMethod结构体中包含了Java方法的全部信息,包括执行入口、访问权限、所属类和代码执行地址等等,ArtMethod结构以下所示。 art/runtime/art_method.h
class ArtMethod FINAL {
...
protected:
GcRoot<mirror::Class> declaring_class_;
std::atomic<std::uint32_t> access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint16_t method_index_;
uint16_t hotness_count_;
struct PtrSizedFields {
ArtMethod** dex_cache_resolved_methods_;//1
void* data_;
void* entry_point_from_quick_compiled_code_;//2
} ptr_sized_fields_;
}
复制代码
ArtMethod结构中比较重要的字段是注释1处的dex_cache_resolved_methods_和注释2处的entry_point_from_quick_compiled_code_,它们是方法的执行入口,当咱们调用某一个方法时(好比Key的show方法),就会取得show方法的执行入口,经过执行入口就能够跳过去执行show方法。 替换ArtMethod结构体中的字段或者替换整个ArtMethod结构体,这就是底层替换方案。 AndFix采用的是替换ArtMethod结构体中的字段,这样会有兼容问题,由于厂商可能会修改ArtMethod结构体,致使方法替换失败。Sophix采用的是替换整个ArtMethod结构体,这样不会存在兼容问题。 底层替换方案直接替换了方法,能够当即生效不须要重启。采用底层替换方案主要是阿里系为主,包括AndFix、Dexposed、阿里百川、Sophix。
除了资源修复,代码修复一样也能够借鉴Instant Run的原理, 能够说Instant Run的出现推进了热修复框架的发展。 Instant Run在第一次构建apk时,使用ASM在每个方法中注入了相似以下的代码:
IncrementalChange localIncrementalChange = $change;//1
if (localIncrementalChange != null) {//2
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
paramBundle });
return;
}
复制代码
其中注释1处是一个成员变量localIncrementalChange ,它的值为$change
,$change
实现了IncrementalChange这个抽象接口。当咱们点击InstantRun时,若是方法没有变化则$change
为null,就调用return,不作任何处理。若是方法有变化,就生成替换类,这里咱们假设MainActivity的onCreate方法作了修改,就会生成替换类MainActivity$override
,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的$change
设置为MainActivity$override
,所以知足了注释2的条件,会执行MainActivity$override
的access$dispatch
方法,access$dispatch
方法中会根据参数"onCreate.(Landroid/os/Bundle;)V"执行MainActivity$override
的onCreate方法,从而实现了onCreate方法的修改。 借鉴Instant Run的原理的热修复框架有Robust和Aceso。