美文网首页
「Android 路线」插件化的基本原理

「Android 路线」插件化的基本原理

作者: 彭旭锐 | 来源:发表于2020-12-26 23:52 被阅读0次

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

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

    前言

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

    目录


    1. 前置知识


    2. Android 中的类加载器

    在 Java 中,JVM 加载的是 .class 文件,而在 Android 中,Dalvik 和 ART 加载的是 dex 文件。这一节我们就来分析 Android ART 虚拟机 中的类加载器:

    提示: dex 文件不仅仅指 .dex 后缀的文件,而是指携带 classed.dex 项的任何文件(例如:jar / zip / apk)。

    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<String> libPaths) {
            pathList.addNativePath(libPaths);
        }
    }
    

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

    2.3 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去访问


    7. 总结


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

    相关文章

      网友评论

          本文标题:「Android 路线」插件化的基本原理

          本文链接:https://www.haomeiwen.com/subject/qkpnnktx.html