1. 背景
在「Android插件化开发指南——类加载器」一文中曾提到,在Android中的类加载示意图为:
图中可知,加载外部jar、dex、apk都需要构建一个DexClassLoader的实例,并将对应的jar、dex、apk文件塞入其中,以构建出一个可用的类加载器实例。具体示例在「Android插件化开发指南——Hook技术(一)【长文】」有给出,实现方式一般都是【将外部dex加载到宿主app的dexElements中】。
jar、dex、apk在使用的时候,也就是有两个思路:
- 创建多个ClassLoader来加载方案。需要在某个地方持有,毕竟类加载器是相互独立的,不能期望在APK的类加载中加载到独属于某个外部jar的class文件;
- 将外部jar/dex/apk文件通过反射加载到宿主APK的dexElements中。此时使用可当做正常APK调用来使用,可在application project构建中自定义provided aar这个配置,完成APK构建的compileOnly的效果,在外部jar/dex/apk被塞入后,就能走到正常流程。但由于Android系统版本的变化性,塞入源码文件到dexElements中这里需要做一些额外的适配工作。可参考:https://github.com/Tencent/tinker - SystemClassLoaderAdder.java中的实现。
上述其实各有优缺点,那么对于热修复场景,我们生成的path类和原类重名,或者新增了类,此时我们应该如何做?找到一篇介绍文章:Android热修复技术原理(最新最全)。大致分为三类:
- 基于Xposed思路的函数指针思路。
- 塞入dexElements的前面,在前面的先加载思路。
- Instant Run构建时预插桩思路。
函数指针思路笔者没什么了解。这里探讨下Instant Run下的类加载: 对这个原理感兴趣的可以阅读美团发布的博客:
关键原理:
也就是说这里其实是为外部jar构造了一个DexClassLoader,但实际上仅仅用来new出来ChangeQuickRedirect,并将这个new出来的对象塞入到了APK的类加载器加载的类的实例对象属性changeQuickRedirect中。那这为什么能生效?
可能会有疑惑,那就是类加载器不是相互独立的吗,怎么能将实例化的对象给设置到APK加载的实例对象中。
类加载器是独立加载,但是可互操作的。虽然 JAR 中的类是通过 DexClassLoader 加载的,并且APK的类和JAR的类是独立的命名空间(因为它们各自使用的类加载器),但是您可以通过反射将 JAR 中的类的实例传递给 APK 中的类。此操作不会受到类加载器的限制,因为实际上是通过引用将对象设置到了 APK 中的某个字段。这一步并没有将 JAR 中的类 “直接” 交给 APK 的类加载器,而是将它的一个实例放入了 APK 的字段中。
因此即使它是通过不同的类加载器加载的,只要能在 APK 的上下文中持有这个实例,就可以正常调用它的方法。
2. 实验
实践下上述描述。可参考:https://github.com/Meituan - PatchExecutor.java
先构造了一个DexClassLoader:
然后获取相关class,进行new实例对象和塞入:
类似的,简化下代码逻辑,模拟下也就是:
// 属性
private ChangeQuickRedirect changeQuickRedirect;
// 点击事件
start_activity_by_nav.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
File jarPath = new File(getFilesDir(), "testClassLoader/a.jar");
File dexOptDir = new File(getCacheDir(), "testClassLoader/opt");
File nativeLibraryDir = new File(getFilesDir().getParentFile(), "lib");
DexClassLoader patchClassLoader = new DexClassLoader(jarPath.getPath(), dexOptDir.getPath(),
nativeLibraryDir.getPath(), getClass().getClassLoader());
try {
Class<?> aClass = Class.forName("com.aaa.TestClass", true, patchClassLoader);
Object o = aClass.newInstance();
Field iPatchClassProvider = OtherActivity.class.getDeclaredField("changeQuickRedirect");
iPatchClassProvider.setAccessible(true);
iPatchClassProvider.set(OtherActivity.this, o);
} catch (Exception e) {
e.printStackTrace();
}
}
});
start_activity_testbtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String strings = changeQuickRedirect.patchedTest();
Toast.makeText(OtherActivity.this, strings, Toast.LENGTH_SHORT).show();
}
});
的确很丝滑。那么要想这个框架顺利运行起来,那就需要处理编译时动态插桩、patch包生成,并设计一套判断调用流程,以及Robust文章中所提到的R8、混淆、super等问题。后续继续学习:
- patch如何生成
- 如何插桩