【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如何生成
  • 如何插桩
相关推荐
xiangpanf7 小时前
Laravel 10.x重磅升级:五大核心特性解析
android
robotx10 小时前
安卓线程相关
android
消失的旧时光-194311 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon11 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon12 小时前
VSYNC 信号完整流程2
android
dalancon12 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户693717500138413 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android13 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才14 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶14 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle