插件化知识点原理解析

1.前言

插件化技术最初源于免安装运行 apk 的想法,这个免安装的 apk 就可以理解为插件,而支持插件的 app 我们一般叫宿主。宿主可以在运行时加载和运行插件,这样便可以将 app 中一些不常用的功能模块做成插件,一方面减小 了安装包的大小,另一方面可以实现 app 功能的动态扩展。

2.插件化的实现

我们如何去实现一个插件化呢?
首先我们要知道,插件apk是没有安装的,那我们怎么加载它呢?不知道。。。
一个 apk 主要就是由代码和资源组成,所以上面的问题我们可以变为:如何加载插件的类?如何加载插件的资源?这样的话是不是就有眉目了。
然后我们还需要解决类的调用的问题,这个地方主要是四大组件的调用问题。我们都知道,四大组件是需要注册 的,而插件的四大组件显然没有注册,那我们怎么去调用呢?
所以我们接下来就是解决这三个问题,从而实现插件化

    1. 如何加载插件的类?
    1. 如何加载插件的资源?
    1. 如何调用插件类

3.类加载

  • 我们在学 java 的时候知道,java 源码文件编译后会生成一个 class 文件,而在 Android 中,将代码编译后会生成一个 apk 文件,将 apk 文件解压后就可以看到其中有一个或多个 classes.dex 文件,它就是安卓把所有 class 文件进行合并,优化后生成的。
  • java 中 JVM 加载的是 class 文件,而安卓中 DVM 和 ART 加载的是 dex 文件,虽然二者都是用的 ClassLoader 加载的,但因为加载的文件类型不同,还是有些区别的,所以接下来我们主要介绍安卓的 ClassLoader 是如何加载dex 文件的。

3.1 ClassLoader的实现类

ClassLoader是一个抽象类,实现类主要分为两种类型:系统类加载器和自定义加载器。 其中系统类加载器主要包括三种:

  • BootClassLoader 用于加载Android Framework层class文件。
  • PathClassLoader 用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex
  • DexClassLoader 用于加载指定的dex,以及jar、zip、apk中的classes.dex

在这里插入图片描述
parent关系:
DexClassLoader —》PathClassLoader —》BootClassLoader

3.2 加载原理

假设我们有一个apk文件,路径是 apkPath,然后里面有个类 com.android.first.Test,那么我们可以通过如下方式去加载 Test 类:

DexClassLoader dexClassLoader = new DexClassLoader(dexPath,context.getCacheDir().getAbsolutePath(),	null, context.getClassLoader());
Class<?> clazz = dexClassLoader.loadClass("com.android.first.Test");

因为我们需要将插件的 dex 文件加载到宿主里面,所以我们接下来分析源码,看 DexClassLoader 类加载器到底是怎么加载一个 apk 的 dex 文件的。
通过查找发现,DexClassLoader 类中没有 loadClass 方法,一路向上查找,最后在 ClassLoader 类中找到了改方法,(api28)源码如下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            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进行加载,依次递归,如果找到了或者加载了就返回,如果即没找到也加载不了,才自己 去加载。这个过程就是 双亲委托机制。

由上面可知,BootCloassLoader是最后一个加载器,所以接下来看下它是如何结束向上递归查找的。

class BootClassLoader extends ClassLoader {
	@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return Class.classForName(name, false, null);
    }
    @Override
    protected Class<?> loadClass(String className, boolean resolve)
           throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

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

        return clazz;
    }
}

在BootCloassLoader类中重写了findClass和loadClass方法,并且在loadClass方法中,不再获取parent,从而结束递归。
接着我们再来看下,在所有parent都没加载成功的情况下,DexClassLoader是如何加载的。通过查找我们发现在他的父类BaseDexClassLoader中,重写了findClass方法。

public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        //初始化 pathList
        this.pathList = new DexPathList(this, dexFiles);
    }

    @Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //在pathList中查找指定的ClassLoader
        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;
  }

接着再来看DexPathList类中的findClass方法。

private Element[] dexElements;
 
public Class<?> findClass(String name, List<Throwable> suppressed) {
//	通过Element获取Class对象
        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;
    }

3.3 插件化原理总结

在这可以看到,Class对象就是从Element中获取的,而每一个Element就对应一个dex文件,因为我们的dex文件可能有多个,所有这是个数组Element[]。到这里,插件化就可以分为以下几步:

  • 1、创建插件的DexClassLoader类加载器,然后通过反射获取插件的dexElements值。
  • 2、创建宿主的PathClassLoader类加载器,然后通过反射获取宿主的dexElements值。
  • 3、合并宿主的dexElements与插件的dexElements,生成新的Elements[]。
  • 4、最后通过反射将新的Elements[]赋值给宿主的dexElements。

3.4 插件化代码总结

public static void loadClass(Context context) {
        // 获取 pathList 的字段
        Class baseDexClassLoaderClass = null;
        try {
            baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
            pathListField.setAccessible(true);

            // 获取 dexElements 字段
            Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);

            // 拿到宿主的dexElements
            ClassLoader hostClassload = context.getClassLoader();
            Object hostPathlist = pathListField.get(hostClassload);
            Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathlist);

            //拿到插件的dexElements
            DexClassLoader dexClassLoader = new DexClassLoader(apkPath, null, context.getCacheDir().getAbsolutePath(), hostClassload);
            Object pluginPathList = pathListField.get(dexClassLoader);
            Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);

            //创建新的数组
            Object[] newElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(), hostDexElements.length + pluginDexElements.length);

            //填充数据
            System.arraycopy(hostDexElements, 0, newElements, 0, hostDexElements.length);
            System.arraycopy(pluginDexElements, 0, newElements, hostDexElements.length, pluginDexElements.length);

            //覆盖系统的 hostDexElements
            //将生成的新值赋给“dexElements”
            //hostDexElements -》newElements
            dexElementsField.set(hostDexElements, newElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

到此,插件化原理告一段落。后续更新请稍等哈。
`

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值