Android | 类加载器与插件化

点赞关注,不再迷路,你的支持对我意义重大!

Hi,我是丑丑。本文「Android 路线」| 导读 —— 从零到无穷大 已收录。这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 随着应用功能模块的增多,组件化和插件化的需求日益强烈;
  • 在这篇文章里,我将分析 实现插件化的基本原理。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

目录


前置知识

  • 反射: 「Java 路线」反射机制(含 Kotlin)

  • 动态代理: 「Java 路线」| 动态代理 & 静态代理

  • 类加载: Java 虚拟机| 类加载机制

  • so 库加载: 「NDK 路线」| so 库加载到卸载的全过程

  • 资源加载: 【点赞催更】


1. 类加载的委派模型

Java 类加载是一种委托机制(parent delegate),即:除了顶级启动类加载器(bootstrap classloader)之外,每个类加载器都有一个关联的上级类加载器(parent 字段)。当一个类加载器准备执行类加载时,它首先会委托给上级加载器去加载,而上级加载器可能还会继续向上委托,递归这个过程。如果上级构造器无法加载,才会返回由自己加载。

更多内容:类加载: Java 虚拟机| 类加载机制

2. Android 中的类加载器

在 Java 中,JVM 加载的是 .class 文件,而在 Android 中,Dalvik 和 ART 加载的是 dex 文件。这里的 dex 文件不仅仅指 .dex 后缀的文件,而是指携带 classed.dex 项的任何文件(例如:jar / zip / apk

这一节我们就来分析 Android ART 虚拟机 中的类加载器:

ClassLoader 实现类 作用
BootClassLoader 加载 SDK 中的类
PathClassLoader 加载应用程序的类
DexClassLoader 加载指定的类

2.1 BootClassLoader

在 Java / Android 中,BootClassLoader 是委托模型中的顶级加载器,作为委托链的最后一个成员,它总是最先尝试加载类的。它是 ClassLoader 的非静态内部类,源码如下:

ClassLoader.java

class BootClassLoader extends ClassLoader {

    public static synchronized BootClassLoader getInstance() {
        单例
    }

    public BootClassLoader() {
        没有上级类加载器
        super(null);
    }

    @Override
    protected Class findClass(String name) {
        注意 ClassLoader 参数:传递 null
        return Class.classForName(name, false, null);
    }

    @Override
    protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException {
        1、检查是否加载过
        Class clazz = findLoadedClass(className);

        2、尝试加载
        if (clazz == null) {
            clazz = findClass(className);
        }
        return clazz;
    }

Class.java

static native Class classForName(String className, boolean shouldInitialize, ClassLoader classLoader) throws ClassNotFoundException;

要点如下:

  • 1、BootClassLoader 是单例的,一个进程只会有一个 BootClassLoader 对象,并在 JVM 启动的时候启动;
  • 2、BootClassLoader 的 parent 字段为空,没有上级类加载器(可以通过判断一个 ClassLoader#getParent() 是否来空来判断是否为 BootClassLoader);
  • 3、BootClassLoader#findClass(),最终调用 native 方法,我在 第 节 再说。

2.2 BaseDexClassLoader

在 Android 中,Java 代码的编译产物是 dex 格式字节码,所以 Android 系统提供了 BaseDexClassLoader 类加载器,用于从 dex 文件中加载类。

BaseDexClassLoader.java

public class BaseDexClassLoader extends ClassLoader {

    private final DexPathList pathList;

    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        从 DexPathList 的路径中加载类
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            throw new ClassNotFoundException(...);
        }
        return c;
    }

    添加 dex 路径
    public void addDexPath(String dexPath, boolean isTrusted) {
        pathList.addDexPath(dexPath, isTrusted);
    }

    添加 so 动态库路径
    public void addNativePath(Collection libPaths) {
        pathList.addNativePath(libPaths);
    }
}

可以看到,BaseDexClassLoader 将 findClass() 的任务委派给 DexPathList 对象处理,这个 DexPathList 指定了搜索类和 so 动态库的路径。

2.3 PathClassLoader & DexClassLoader

从源码可以看出,PathClassLoader & DexClassLoader 其实都没有重写方法,所以主要的逻辑还是在 BaseDexClassLoader。

这两个类其实只有一点不同,在 Android 9.0 之前,DexClassLoader 的构造方法需要传入第二个参数optimizedDirectory,这个路径是存放优化后的 dex 文件的路径(odex)。

不过在 Android 9.0 之后,DexClassLoader 也不需要传这个参数了。

参数 描述
dexPath 加载 dex 文件的路径
optimizedDirectory 加载 odex 文件的路径
librarySearchPath 加载 so 库文件的路径
parent 上级类加载器

DexClassLoader.java - Android 8.0

public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}

DexClassLoader.java - Android 9.0

public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}

PathClassLoader.java

public PathClassLoader(String dexPath,  String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}

3. DexPathList 源码分析

第 2 节里,我们提到 BaseDexClassLoader 将 findClass() 的任务委派给 DexPathList 对象处理,这一节我们就来分析 DexPathList 里的处理过程。

DexFile 是 dex 文件在内存中的映射
Elment - dexFile
Element[] dexElements 一个app所有的class文件都在 dexElements里


4. 插件化的基本流程

4.1 如何加载插件中的类?

4.1.1 生成 dex 文件

  • 1、将dx.bat文件添加到环境变量
sdk
├─ build-tools
     ├── 28.0.2
             ├── dx.bat

dx.bat是用于生成 dex 文件的命令,将它添加到环境变量里使用起来会方便些。

  • 2、javac 命令编译

  • 3、dx 命令生成 dex 文件

dx --dex --output=「输出文件名.dex」 「com.xurui.test.class」
  • 4、将 dex 文件放置在 sdcard(外部存储)

4.1.2 使用 DexClassLoader 加载 dex 文件

DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex",
    context.getCacheDir().getAbsolutePath(),
    null,
    context.getClassLoader());

4.1.3 执行类加载

try {
    执行类加载
    Class clazz = dexClassLoader.loadClass("com.xurui.test");
    ...
} catch (Exception e) {
    e.printStackTrace();
}

4.2 加载插件的步骤

  • 1、创建插件的 DexClassLoader 类加载器;
  • 2、获取宿主 App 的 PathClassLoader 类加载器;
  • 3、合并两个类加载器中的 dexElements,生成新的 Element[];
  • 4、通过反射将新值赋值给宿主的 dexElements 字段。
1、宿主类加载器
ClassLoader appClassLoader = context.getClassLoader();
宿主 DexPathList
Object appPathList = pathListField.get(appClassLoader);
宿主 dexElements
Object[] appDexElements = (Object[]) dexElementsField.get(appPathList);

2、插件加载器
ClassLoader pluginClassLoader = new DexClassLoader(apkPath,
        context.getCacheDir().getAbsolutePath(),
        null,
        appClassLoader);
插件 DexPathList
Object pluginPathList = pathListField.get(pluginClassLoader);
插件 dexElements
Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);

3、合并 dexElements
// Object[] obj = new Object[appDexElements.length + pluginDexElements.length]; // x

Object[] newElements = (Object[]) Array.newInstance(appDexElements.getClass().getComponentType(),
        appDexElements.length + pluginDexElements.length);
System.arraycopy(appDexElements, 0, newElements, 0, appDexElements.length);
System.arraycopy(pluginDexElements, 0, newElements, 0, pluginDexElements.length);

4、赋值
dexElementsField.set(appPathList, newElements);

5. 启动插件中的四大组件

5.1 矛盾

第 4 节 中,我们已经成功实现了插件中类的加载。但是对于四大组件来说,由于插件中的组件没有在宿主AndroidManifest.xml中注册,即时完成了类加载,也无法启动。

5.2 解决策略

解决策略是使用一个代理 Activity 作为中转,实现偷天换日:

  • 1、在宿主 App 中注册「ProxyActivity」;
  • 2、Hook AMS 中启动 Activity 的流程,将 「启动 PluginActivity」修改为「启动 ProxyActivity」;
  • 3、Hook AMS

使用动态代理和反射机制可以实现 Hook,而在寻找 Hook 点时需要遵循以下原则:

  • 1、尽量 Hook 静态变量或单例变量(不容易被改变);
  • 2、尽量 Hook public 的对象和方法(影响范围最小)。

5.3 实现步骤

提示: 以下源码基于 Android Q - API 26。

  • 1、注册 ProxyActivity

  • 2、Hook AMS

1、获取 singleton 对象
Class amsClazz = Class.forName("android.app.ActivityManager");
Field singletonField = amsClazz.getDeclaredField("IActivityManagerSingleton");
singletonField.setAccessible(true);
Object singleton = singletonField.get(null);

2、获取 IActivityManager 对象
Class singletonClazz = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClazz.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
final Object mInstance = mInstanceField.get(singleton);

3、动态代理 IActivityManager
Class iActivityManagerClazz = Class.forName("android.app.IActivityManager");
Object proxyInstance = Proxy.newProxyInstance(context.getClassLoader(),
new Class[]{iActivityManagerClazz},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    5、修改 Intent,替换 PluginActivity 到 ProxyActivity

    6、不修改原有执行流程
    return method.invoke(mInstance, args);
    }
});

4、反射修改字段
mInstanceField.set(singleton, proxyInstance);
public static final String EXTRA_TARGET_INTENT = "target_intent";

5、修改 Intent,替换 PluginActivity 到 ProxyActivity

5.1 过滤
if ("startActivity".equals(method.getName())) {
    int indexOfIntent = -1;
    for (int index = 0; index < args.length; index++) {
        if (args[index] instanceof Intent) {
            indexOfIntent = index;
            break;
        }
    }

    5.2 启动 PluginActivity 的Intent
    Intent pluginIntent = (Intent) args[indexOfIntent];

    5.3 启动 ProxyActivity 的Intent
    Intent proxyIntent = new Intent();
    proxyIntent.setClassName("com.xurui", "com.xurui.ProxyActivity");
    args[indexOfIntent] = proxyIntent;

    5.4 保存原本的 intent
    proxyIntent.putExtra(EXTRA_TARGET_INTENT, pluginIntent);
}

6、不修改原有执行流程
return method.invoke(mInstance, args);
  • 3、Hook Handler
1、创建 Handler.callback
Handler.Callback callback = new Handler.Callback() {
    @Override
    public boolean handleMessage(@NonNull Message msg) {
        // msg.obj == ActivityClientRecord
        switch (msg.what) {
            case 100: // LAUNCH_ACTIVITY
                try {
                    Field intentField = msg.obj.getClass().getDeclaredField("intent");
                    intentField.setAccessible(true);

                    1.1 获取 proxyIntent
                    Intent proxyIntent = (Intent) intentField.get(msg.obj);
                    
                    1.2 替换为 pluginIntent
                    Intent pluginIntent = proxyIntent.getParcelableExtra(EXTRA_TARGET_INTENT);
                    if (null != pluginIntent) {
                        intentField.set(msg.obj, pluginIntent);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
        }
        // 不改变原有流程
        return false;
    }
};

2、获取 ActivityThread 对象
Class clazz = Class.forName("android.app.ActivityThread");
Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
activityThreadField.setAccessible(true);
Object activityThread = activityThreadField.get(null);

3、获取 mH 对象
Field mHField = clazz.getDeclaredField("mH");
mHField.setAccessible(true);
final Handler mH = (Handler) mHField.get(activityThread);

4、赋值
Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(mH, callback);
  • 4、版本适配

针对每个版本的源码,需要分别对 Hook 点进行适配。


6. 加载插件中的资源

资源加载:asset / res

通过resource访问,其实也是通过assetmanager去访问

2020年12月27 暂停


7. 总结


创作不易,你的「三连」是丑丑最大的动力,我们下次见!

你可能感兴趣的:(Android | 类加载器与插件化)