Implementar un marco de complementos desde cero (1)

¿Qué es el complemento?

concepto

La tecnología de complementos se originó a partir de la idea de ejecutar apk sin instalación. Este apk sin instalación puede entenderse como un complemento, y las aplicaciones que admiten complementos generalmente se denominan hosts. El host puede cargar y ejecutar complementos en tiempo de ejecución, por lo que algunos módulos funcionales poco comunes de la aplicación se pueden convertir en complementos. Por un lado, el tamaño del paquete de instalación se reduce y, por otro lado, puede realizar la expansión dinámica de las funciones de la aplicación.


Sabemos que la placa base de la computadora está compuesta por una serie de ranuras, qué función necesitamos, enchufarla en el chip o tarjeta gráfica correspondiente, para lograr el intercambio en caliente. Basado en este principio, la conexión en caliente de software es la conexión

Problemas resueltos por plug-in

La APP tiene cada vez más módulos funcionales y volúmenes más grandes. De esta manera, algunos módulos comerciales se pueden enchufar y cargar a pedido, reduciendo así el volumen del paquete de instalación
. El acoplamiento entre los módulos es alto y el costo del desarrollo colaborativo y la comunicación es mayor. Cuanto mayor sea el
número, el número de métodos puede superar los 65535 y la memoria ocupada por la aplicación es demasiado grande
. La llamada mutua entre aplicaciones
. La diferencia entre la creación de
componentes y la incorporación. El desarrollo de componentes consiste en dividir una aplicación en varios módulos, y cada módulo es un componente, desarrollado En el proceso, podemos hacer que estos componentes dependan entre sí o depurar algunos componentes por separado, pero en la versión final, estos componentes se fusionan en una sola apk, que es desarrollo en componentes.

El desarrollo de complementos y el desarrollo basado en componentes son ligeramente diferentes. El desarrollo de complementos consiste en dividir la aplicación completa en varios módulos. Estos módulos incluyen un host y varios complementos. Cada módulo es un apk. El paquete final es el apk del host y los complementos. El apk se empaqueta por separado.

Comparación de marcos de complementos

También hay muchos complementos populares en el mercado ¿Cuáles son las diferencias entre ellos?

Implementación de complementos

El complemento apk no está instalado, entonces, ¿cómo dejas que el host lo cargue? Sabemos que un apk se compone de código y recursos, por lo que solo se deben considerar dos cuestiones:

¿Cómo cargar las clases en el complemento?

¿Cómo cargar los recursos en el complemento?

Por supuesto, existe la pregunta más importante, ¿cómo llamar a los cuatro componentes principales? Los cuatro componentes principales deben registrarse, y los componentes en el complemento apk obviamente no se registrarán por adelantado en el host, entonces, ¿cómo llamarlo?

Resolvamos estos problemas paso a paso

ClassLoader类加载器
以前在讲热修复的时候,我简单地介绍了一下ClassLoader的加载机制。java源码文件在编译后会生成一个class文件,而在Android中,将代码编译后会生成一个 apk 文件,将 apk 文件解压后就可以看到其中有一个或多个 classes.dex 文件,它就是安卓把所有 class 文件进行合并,优化后生成的。

java 中 JVM 加载的是 class 文件,而安卓中 DVM 和 ART 加载的是 dex 文件,虽然二者都是用的 ClassLoader 加
载的,但因为加载的文件类型不同,还是有些区别的,所以接下来我们主要介绍安卓的 ClassLoader 是如何加载
dex 文件的。

ClassLoader实现类

在Android中,ClassLoader是一个抽象类,它的实现类主要分为两种类型:系统类加载器(BootClassLoader),和自定义类加载器(PathClassLoader | DexClassLoader)

先看一下ClassLoader加载流程图:


BootClassLoader

用于加载Android Framework层的class文件,比如 Activity、Fragment,不过需要注意的是AppCompatActivity虽然也是google工程师提供的类,但是一个第三方包中的类,并不输入Framwork层,所以AppCompatActivity并不是使用BootClassLoader加载的

PathClassLoader

用于Android应用程序类加载器。可以加载指定的dex, 以及jar、zip、apk中的classes.dex

DexClassLoader

在Android8.0以后的API中,和 PathClassLoader是没有任何区别的,而在以前的API中,两者只有一个设置加载路径的区别(有的文章说,PathClassLoader只支持直接操作dex格式文件,而DexClassLoader可以支持.apk、.jar和.dex文件,并且会在指定的outpath路径释放出dex文件。其实不然,甚至可以说两者没有任何区别)


先放一张ClassLoader类继承关系图,相信都能看懂,就不多讲了,下面来看一下PathClassLoader 和 DexClassLoader的源码:

// /libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
    // optimizedDirectory 直接为 null
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    // optimizedDirectory 直接为 null
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent)    {
        super(dexPath, null, librarySearchPath, parent);
    }
}
// API 小于等于 26/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
        String librarySearchPath, ClassLoader parent) {
        // 26开始,super里面改变了,看下面两个构造方法
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}
// API 26/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
    String librarySearchPath, ClassLoader parent) {
        super(parent);
        // DexPathList 的第四个参数是 optimizedDirectory,可以看到这儿为 null
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
}
// API 25/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath,       optimizedDirectory);
}

根据源码就可以了解到,PathClassLoader 和 DexClassLoader 都是继承自 BaseDexClassLoader,且类中只有构造方法,它们的类加载逻辑完全写在 BaseDexClassLoader 中。

其中我们值的注意的是,在8.0之前,它们二者的唯一区别是第二个参数 optimizedDirectory,这个参数的意思是
生成的 odex(优化的dex)存放的路径,PathClassLoader 直接为null,而 DexClassLoader 是使用用户传进来的
路径,而在8.0之后,二者就完全一样了。
下面我们再来了解下 BootClassLoader 和 PathClassLoader 之间的关系:// 在 onCreate 中执行下面代码

ClassLoader classLoader = getClassLoader();
while (classLoader != null) {
    Log.e("leo", "classLoader:" + classLoader);
    classLoader = classLoader.getParent();
}
Log.e("leo", "classLoader:" + Activity.class.getClassLoader());

打印结果:

classLoader:dalvik.system.PathClassLoader[DexPathList[[zip file
"/data/user/0/com.enjoy.pluginactivity/cache/plugin-debug.apk", zip file
"/data/app/com.enjoy.pluginactivity-T4YwTh-
8gHWWDDS19IkHRg==/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.pluginactivity-
T4YwTh-8gHWWDDS19IkHRg==/lib/x86_64, /system/lib64, /vendor/lib64]]]
classLoader:java.lang.BootClassLoader@a26e88d
classLoader:java.lang.BootClassLoader@a26e88d

通过打印结果可知,应用程序类是由 PathClassLoader 加载的,Activity 类是 BootClassLoader 加载的,并且
BootClassLoader 是 PathClassLoader 的 parent,这里要注意 parent 与父类的区别。这个打印结果我们下面还
会提到。

加载原理
那么如何使用类加载器去从dex中加载一个插件类呢?很简单

比如,有一个apk文件,路径是apkPath,里面有个类com.plugin.Test,就可以通过反射加载一个类:

// 初始化一个类加载器
DexClassLoader classLoader = new DexClassLoader(dexPath, context.getCacheDir().getAbsolutePath, null, context.getClassLoader);
// 获取插件中的类
Class<?> clazz = classLoader.loadClass("com.plugin.Test");
// 调用类中的方法
Method method = clazz.getMethod("test", Context.class)
method.invoke(clazz.newInstance(), this)

dex中加载类很简单,但是我们需要的是将插件中的dex加载到宿主里面,又该怎么做呢?其实原理还是跟热修复一样,下面就以API 26 Android 8.0举例,通过源码,看一下DexClassLoader类加载器是怎么加载一个apk中的dex文件的。

通过查找发现,DexClassLoader并没有加载类的方法,继续看它的父类,最后在ClassLoader类中找到了一个loadClass方法,看来就是通过这个方法来加载类了:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // 1. 检测这个类是否已经被加载,如果已经被加载了就可以直接返回了
            Class<?> c = findLoadedClass(name);
            // 如果类未被加载
            if (c == null) {
                try {
                    // 2. 判断是否有上级加载器,使用上级加载器的loadClass方法去加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 正常情况下是不会走到这里的,因为最终ClassLoader都会走到BootClassLoader,重写了loadClass方法结束掉了递归
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
                // 3. 如果所有的上级都没找到,就调用findClass方法去查找
                if (c == null) {
                    c = findClass(name);
                }
            }
            return c;
    }

上面类加载分为了3个步骤

1、 检测这个类是否已经被加载,最终会调用到native方法实现查找,这里就不深入了:

protected final Class<?> findLoadedClass(String name) {
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        //native方法
        return VMClassLoader.findLoadedClass(loader, name);
}

2、如果没被找到,就会从parent中调用loadClass方法去查找,依次递归,如果找到了就返回,如果所有的上级都没有找到,又会调用到findClass一级一级的去查找。这个过程就是双亲委托机制

3、 findClass

// -->2 加载器一般都会重写这个方法,定义自己的加载规则
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

根据前面的打印结果我们可以看懂,ClassLoader的最上级是BootClassLoader,来 看下它是如何重写的loadClass方法,结束递归的:

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;
    }
}

从上面可以看到 BootClassLoader 重写了 findClass 和 loadClass 方法,并且在 loadClass 方法中,不再获取 parent,从而结束了递归。

接着往下走,如果所有的parent都没找到,DexClassLoader是如何加载的,通过查找,其实现方法在它的父类BaseDexClassLoader中:

// /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    // 在 pathList 中查找指定的 Class
    Class c = pathList.findClass(name, suppressedExceptions);
    return c;
}
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                          String librarySearchPath, ClassLoader parent) {
    super(parent);
    // 初始化 pathList
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
}

findClass中有调用了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;
        }
    }
    return null;
}

到这里一目了然,class对象就是从Element中获得的,而每一个Element就对应了一个dex文件,因为一个apk中dex文件可能有多个,所以就使用了数组来盛放Element。到这里加载apk中的类大家是不是就有思路了?

创建插件的ClassLoader加载器(PathClassLoader或DexClassLoader),然后通过反射,获取插件的dexElements数组的值
获取宿主的ClassLoader加载器,通过反射获取宿主的dexElements数组的值。
合并宿主和插件的dexElements数组,生成一个新的数组
通过反射将新的数组重新赋值给宿主的dexElements
实现方法
废话不多说,直接上代码:(我这里使用了kotlin,写起来感觉方便一些)

fun load(context: Context) {
        // 获取 pathList
        val systemClassLoader = Class.forName("dalvik.system.BaseDexClassLoader")
        val pathListField = systemClassLoader.getDeclaredField("pathList")
        pathListField.isAccessible = true

        // 获取 dexElements
        val dexPathListClass = Class.forName("dalvik.system.DexPathList")
        val dexElementsField = dexPathListClass.getDeclaredField("dexElements")
        dexElementsField.isAccessible = true


        // 获取宿主的Elements
        val hostClassLoader = context.classLoader
        val hostPathList = pathListField.get(hostClassLoader)
        val hostElements = dexElementsField.get(hostPathList) as kotlin.Array<Any>

        // 获取插件的Elements
        val pluginClassLoader = PathClassLoader("sdcard/plugin-debug.apk", context.classLoader)
        val pluginPathList = pathListField.get(pluginClassLoader)
        val pluginElements = dexElementsField.get(pluginPathList) as kotlin.Array<Any>

        // 创建数组
        val newElements =
        Array.newInstance(
            pluginElements.javaClass.componentType!!,
            hostElements.size + pluginElements.size
        ) as kotlin.Array<Any>

        // 给新数组赋值
        // 先用宿主的,再用插件的
        System.arraycopy(hostElements, 0, newElements, 0, hostElements.size)
        System.arraycopy(pluginElements, 0, newElements, hostElements.size, pluginElements.size)
        // 将生成的新值赋给 "dexElements" 属性
        dexElementsField.set(hostPathList, newElements)

    }

这样就合并了两个dex文件的类,宿主中就可以直接加载插件中的类了

private fun loadApk() {
        try {
            val clazz = Class.forName("com.kangf.plugin.Test")
            val method = clazz.getMethod("test", Context::class.java)
            method.invoke(clazz.newInstance(), this)
        } catch (e: Exception) {
            e.printStackTrace()
            // 调用上面的load方法
            Toast.makeText(this, "请先点击加载apk", Toast.LENGTH_LONG).show()
        }
}

时间关系,今天就讲到这里,还有两个问题(加载资源图片和四大组件),留到下一篇文章再讲

源码已经上传到github,有需要的可以看一下:

DynamicTest

好了,下面来看一下运行效果吧!


原文作者:Pan Geng
原文链接:https://blog.csdn.net/qq_22090073/java/article/details/103946596

Supongo que te gusta

Origin blog.csdn.net/AndroidAlvin/article/details/107532724
Recomendado
Clasificación