【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如何生成
  • 如何插桩
相关推荐
ansondroider43 分钟前
Android adb 安装应用失败(安装次数限制)
android·adb·install
艾小逗2 小时前
uniapp中检查版本,提示升级app,安卓下载apk,ios跳转应用商店
android·ios·uni-app·app升级
tangweiguo030519874 小时前
Android Kotlin ViewModel 错误处理:最佳 Toast 提示方案详解
android·kotlin
火柴就是我5 小时前
android 基于 PhotoEditor 这个库 开发类似于dlabel的功能_2
android
每次的天空5 小时前
Android学习总结之Java篇(一)
android·java·学习
8931519606 小时前
Android开发Glide做毛玻璃效果
android·glide·android开发·android教程·glide做毛玻璃效果
whysqwhw7 小时前
DRouter代码走读
android
人生游戏牛马NPC1号8 小时前
学习Android(五)玩安卓项目实战
android·kotlin
前行的小黑炭9 小时前
Android Lifecycle代码分析:为什么使用;注解的方式为什么过期?源码分析;状态与事件
android
和煦的春风9 小时前
案例分析 | SurfaceFlinger 大片Runnable引起的卡顿
android·linux