【Android】类加载器&热修复-随记

1. 背景

在「Android插件化开发指南------类加载器」一文中曾提到,在Android中的类加载示意图为:

图中可知,加载外部jar、dex、apk都需要构建一个DexClassLoader的实例,并将对应的jar、dex、apk文件塞入其中,以构建出一个可用的类加载器实例。具体示例在「Android插件化开发指南------Hook技术(一)【长文】」有给出,实现方式一般都是【将外部dex加载到宿主app的dexElements中】。

jar、dex、apk在使用的时候,也就是有两个思路:

  1. 创建多个ClassLoader来加载方案。需要在某个地方持有,毕竟类加载器是相互独立的,不能期望在APK的类加载中加载到独属于某个外部jar的class文件;
  2. 将外部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实例对象和塞入:

类似的,简化下代码逻辑,模拟下也就是:

java 复制代码
// 属性
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如何生成
  • 如何插桩
相关推荐
幻雨様2 小时前
UE5多人MOBA+GAS 45、制作冲刺技能
android·ue5
Jerry说前后端4 小时前
Android 数据可视化开发:从技术选型到性能优化
android·信息可视化·性能优化
Meteors.5 小时前
Android约束布局(ConstraintLayout)常用属性
android
alexhilton6 小时前
玩转Shader之学会如何变形画布
android·kotlin·android jetpack
whysqwhw10 小时前
安卓图片性能优化技巧
android
风往哪边走10 小时前
自定义底部筛选弹框
android
Yyyy48211 小时前
MyCAT基础概念
android
Android轮子哥11 小时前
尝试解决 Android 适配最后一公里
android
雨白12 小时前
OkHttp 源码解析:enqueue 非同步流程与 Dispatcher 调度
android
风往哪边走13 小时前
自定义仿日历组件弹框
android