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

发布于:2025-02-23 ⋅ 阅读:(12) ⋅ 点赞:(0)

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实例对象和塞入:
在这里插入图片描述

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

// 属性
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如何生成
  • 如何插桩