Android插件化学习(一):如何调用插件的普通类

插件化技术最初源于免安装运行 apk 的想法

免安装的 apk 我们称它为 插件
支持插件的 app 我们称它为 宿主

插件化解决的问题
  1. APP的功能模块越来越多,体积越来越大
  2. 模块之间的耦合度高,协同开发沟通成本越来越大
  3. 方法数目可能超过65535,APP占用的内存过大
  4. 应用之间的互相调用
插件化与组件化的区别

组件化开发就是将一个app分成多个模块,每个模块都是一个组件,开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并统一成一个apk,这就是组件化开发。

插件化开发是将整个app拆分成多个模块, 这些模块包括一个宿主和多个插件,每个模块都是一个apk,最终打包的时候宿主apk和插件apk分开打包。

插件化实现思路:

  1. 如何加载插件的类?
  2. 如何启动插件的四大组件?
  3. 如何加载插件的资源?
如何加载插件的类

Dalvik虚拟机和其他java虚拟机一样,运行时先将.class文件加载到内存中。在标准的java虚拟机中,类加载可以从class文件中读取,也可以是其他形式的二进制流。而Android虚拟机读取的是.dex文件,一种对class文件优化的产物(把不同class文件重复的东西保留一份)

在加载阶段虚拟机主要完成三件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区运行时数据结构
  3. 在Java堆中生成一个代表这个类的Class对象,作为方法区访问数据的入口

在这里插入图片描述

forName 与 loadClass 区别,是否有初始化

  1. loadClass主要是加载,没有连接、初始化
  2. forName是加载、连接、初始化

Android的类加载器是ClassLoader

在这里插入图片描述

1. PathClassLoader 与 DexClassLoader 区别

先看PathClassLoader源码


public class PathClassLoader extends BaseDexClassLoader {

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

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

PathClassLoader继承于BaseDexClassLoader,只有两个构造方法
再看DexClassLoader的源码


public class DexClassLoader extends BaseDexClassLoader {

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

DexClassLoader同样继承于BaseDexClassLoader,同样只有构造方法,构造函数与 PathClassLoader 的第二个构造方法完全一样!

在Android 8.0 之前,二者的唯一区别是第二个参数 optimizedDirectory,这个参数的意思是生成odex(优化的dex)存放的路径
在Android 8.0 (API 26)之后PathClassLoader 与 DexClassLoader 完全一样。再看上面ClassLoader的图,错的!!
2. BootClassLoader 与 PathClassLoader 区别
2.1 BootClassLoader 与 PathClassLoader 的关系
    private void printClassLoader() {

        ClassLoader loader = getClassLoader();

        while (loader != null) {
            Log.e(TAG, "printClassLoader: " + loader);
            loader = loader.getParent();
        }

    }

下面是运行结果

11-29 08:49:13.698 4151-4151/com.miss.pluginstart E/TPC: printClassLoader: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.miss.pluginstart-2/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
11-29 08:49:13.698 4151-4151/com.miss.pluginstart E/TPC: printClassLoader: java.lang.BootClassLoader@3ce29b12

PathClassLoader 的 parent 是 BootClassLoader, BootClassLoader 没有 parent ,此处 parent 不是父类,是链表结构。parent 是ClassLoader 类型的对象,是一个成员变量

2.1 BootClassLoader 与 PathClassLoader 都加载什么类

PathClassLoader 加载应用的类,比如 dependencies 引入的依赖包;Androidx 的 AppCompatActivity 等等

BootClassLoader 加载SDK的类

3. 如何用类加载器加载一个类
  1. 用 dx.bat (sdk\AndroidSdk\build-tools\30.0.1 具体版本无所谓)生成目标文件的 dex 文件
    1. 把路径 cd 到 …/build/intermediates/javac/debug/classes 下,
    2. 用Terminal执行下面命令行,test.dex 是生成的文件,后面的是目标文件的路径
dx --dex --output=test.dex 
/com/miss/android_plugin/util/MyPrint.class
  1. 把生成的 test.dex 文件拖到 /sdcard 下。运行下面代码
    private void perform() {
        
        /**
         * cmd.exe /c dx --dex --output=text.dex /com/miss/android_plugin/util/MyPrint.class
         * 四个参数 String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent
         *  dexPath 要加载的类的路径,直接用的话只能是dex文件,插件化可以用apk文件
         *  optimizedDirectory 优化路径,类中有相同的东西可以合并
         *  PathClassLoader 只能穿路径,不能传 apk
         */
        DexClassLoader loader = new DexClassLoader("/sdcard/test.dex",
                context.getCacheDir().getAbsolutePath(),
                null, getClassLoader());
        try {
            Class<?> clazz = loader.loadClass("com.miss.android_plugin.util.MyPrint");
            Method print = clazz.getMethod("print");
            // 此处是静态方法,静态方法与对象无关,所以是null 
            print.invoke(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

下面是目标代码

public class MyPrint {

    public static final void print() {
        System.out.println("android plugin print log~");
    }

}

这样就实现了类的加载,那它是怎么加载的呢

4. 类加载机制

DexClassLoader 只有一个构造方法, loadClass 的实现在 ClassLoader 中,下面是源码

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            //  先查询是否已经加载过这个类,如果加载过直接返回,如果没有则先委派给他的parent
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

首先检测这个类是否已经被加载了,如果已经加载 了,直接获取并返回。如果没有被加载,parent 不 为 null,则调用parent的loadClass进行加载,依次递 归,如果找到了或者加载了就返回,如果即没找到 也加载不了,才自己去加载。这个过程就是我们常 说的 双亲委托机制。关系如下图:

在这里插入图片描述

那为什么要这么设计呢

  1. 避免重复加载,当父加载器已经加载了该类的 时候,就没有必要子ClassLoader再加载一次。
  2. 安全性考虑,防止核心API库被随意篡改。
时序图

在这里插入图片描述

  1. 先调用findLoadClass查看是否已经加载,此步会调用VM的方法,在此不深入
    protected final Class<?> findLoadedClass(String name) {
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        return VMClassLoader.findLoadedClass(loader, name);
    }
  1. 如果此类没有被加载过,调用BootClassLoader的loadClass,因BootClassLoader主要加载SDK的类,在此不做深入
    @Override
    protected Class<?> loadClass(String className, boolean resolve)
           throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            clazz = findClass(className);
        }

        return clazz;
    }
  1. 双亲未能加载,则调用自己的findClass,下面是 ClassLoader 的 findClass方法
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

此处没有实现,则找BaseDexClassLoader的findClass方法

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

这里得知,类由pathList.findClass(name, suppressedExceptions)加载得到,
pathList是DexPathList,继续深入

    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

此处的 dexElements 是个数组,遍历Element数组,调用其findClass方法,继续深入

        public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }

loadClassBinaryName,加载二进制,加载文件
Element是PathList的内部类,dexFile就是dex文件的抽象类,一个Element对应一个dexFile,一个apk是有多个dex文件的。也就是说一个APP的所有class文件都在dexElements 里面

总结,app之所以能加载某个类,是因为dex文件在app中,即这个类在dexElements中。

基于这个原理,如果我们把插件的dexElements数组宿主的dexElements数组合并给宿主,岂不是宿主可以加载插件的类

宿主dexElements = 宿主的dexElements + 插件的dexElements

在这里插入图片描述

实现步骤
  1. 创建插件的 DexClassLoader 类加载器,然后通过反射获取插件的 dexElements 值。
  2. 获取宿主的 PathClassLoader 类加载器,然后通过反射获取宿主的 dexElements 值。
  3. 合并宿主的 dexElements 与 插件的 dexElements,生成新的 Element[]。
  4. 最后通过反射将新的 Element[] 赋值给宿主的 dexElements 。
5. 加载插件的类(代码实现)

目标 dexElements ----> DexPathList 类对象 ----> BaseDexClassLoader 的对象

  public class LoadUtil {

    private static final String dexPath = "/data/data/com.miss.plugin/cache/myplugin-debug.apk";

    public static void loadUtil(Context context) {

        /**
         * 宿主dexElements = 宿主dexElements + 插件dexElements
         *
         * 1.获取宿主dexElements
         * 2.获取插件dexElements
         * 3.合并两个dexElements
         * 4.将新的dexElements 赋值到 宿主dexElements
         *
         * 目标:dexElements  -- DexPathList类的对象 -- BaseDexClassLoader的对象,类加载器
         *
         * 获取的是宿主的类加载器  --- 反射 dexElements  宿主
         *
         * 获取的是插件的类加载器  --- 反射 dexElements  插件
         */

        try {
            //  BaseDexClassLoader的对象
            Class<?> dexClazz = Class.forName("dalvik.system.BaseDexClassLoader");
            Field fieldPathList = dexClazz.getDeclaredField("pathList");
            fieldPathList.setAccessible(true);

            Class<?> pathListClazz = Class.forName("dalvik.system.DexPathList");
            Field fieldElements = pathListClazz.getDeclaredField("dexElements");
            fieldElements.setAccessible(true);

            //  宿主的类加载器
            ClassLoader hostClassLoader = context.getClassLoader();
            //  宿主的 pathList
            Object hostPathList = fieldPathList.get(hostClassLoader);
            //  宿主 dexElements
            Object[] hostElements = (Object[]) fieldElements.get(hostPathList);


            //  插件的类加载器
            ClassLoader pluginClassLoader = new DexClassLoader(dexPath,
                    context.getCacheDir().getAbsolutePath(), null, hostClassLoader);
            //  插件的 pathList
            Object pluginPathList = fieldPathList.get(pluginClassLoader);
            //  插件的 dexElements
            Object[] pluginElements = (Object[]) fieldElements.get(pluginPathList);


            // 宿主dexElements = 宿主dexElements + 插件dexElements

            Object[] newElements = (Object[]) Array.newInstance(hostElements.getClass().getComponentType(),
                    pluginElements.length + hostElements.length);

            System.arraycopy(hostElements, 0, newElements, 0, hostElements.length);
            System.arraycopy(pluginElements, 0, newElements, hostElements.length, pluginElements.length);

            fieldElements.set(hostPathList, newElements);

            
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

在Application中调用

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //	调用 把插件的dex文件放在宿主中
        LoadUtil.loadUtil(this);
    }
}

在MainActivity中的调用

    public void jump(View view) {
        try {
            Class<?> aClass = Class.forName("com.miss.myplugin.MyPrint");
            Method method = aClass.getMethod("printString");
            method.invoke(null);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

运行的结果

2020-12-03 15:57:52.846 17149-17149/com.miss.plugin I/System.out:  插件  普通类 方法调用~
2020-12-03 15:57:52.846 17149-17149/com.miss.plugin I/System.out: 1606982272846

附上插件的普通类

public class MyPrint {

    public static final void printString() {
        System.out.println(" 插件  普通类 方法调用~");
        System.out.println(Util.getTime());
    }

}
郑重说明:Android 10 一定要把把 apk 放在私有目录下
private static final String dexPath = "/data/data/com.miss.plugin/cache/myplugin-debug.apk";
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值