Implementar un marco de complementos desde cero

Autor: Pan Geng

¿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, 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 así lograr una conexión en caliente. Basado en este principio, el software hot plug es plug-in

Problemas resueltos por plug-in

  • Cada vez hay más módulos funcionales de APP y el volumen es cada vez mayor, de esta manera algunos módulos de negocio pueden ser plug-in y cargados bajo demanda, reduciendo así el tamaño del paquete de instalación.
  • El acoplamiento entre módulos es alto y el costo del desarrollo colaborativo y la comunicación está aumentando
  • La cantidad de métodos puede superar los 65535 y la memoria ocupada por la aplicación es demasiado grande
  • Llamarnos entre aplicaciones

La diferencia entre componentización y plug-inización

El desarrollo basado en componentes consiste en dividir una aplicación en varios módulos. Cada módulo es un componente. Durante el proceso de desarrollo, podemos hacer que estos componentes dependan unos de otros o depurar algunos componentes por separado, pero la versión final es fusionar estos componentes en un unificado Un apk, esto es desarrollo de componentes.

El desarrollo de complementos es ligeramente diferente del desarrollo basado en componentes. 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 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?

Cuando elegimos un marco de código abierto, debemos hacerlo de acuerdo con nuestras propias necesidades. Si el complemento cargado no necesita estar acoplado con el host, ni necesita comunicarse con el host, como cargar un tercer -party app, entonces se recomienda RePlugin, y se recomiendan otras situaciones. VirtualApk.

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 se deben considerar los siguientes problemas:

  • ¿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 están registrados de antemano en el host, entonces, ¿cómo llamarlo?

Resolvamos estos problemas paso a paso

ClassLoader

Cuando hablé antes de Hot Fix, presenté brevemente el mecanismo de carga de ClassLoader. El archivo de código fuente de Java generará un archivo de clase después de la compilación. En Android, el código se compilará para generar un archivo apk. Después de descomprimir el archivo apk, puede ver que hay uno o más archivos classes.dex en él, que es Android Combinar todos los archivos de clase y generarlos después de la optimización.

El mecanismo de carga de ClassLoader:

https://blog.csdn.net/qq_22090073/article/details/103369591

JVM en java carga archivos de clase, mientras que en Android DVM y ART carga archivos dex. Aunque ambos se cargan con ClassLoader, todavía hay algunas diferencias debido a los diferentes tipos de archivos cargados, así que a continuación presentamos principalmente cómo ClassLoader de Android carga archivos dex.

Clase de implementación ClassLoader

En Android, ClassLoader es una clase abstracta y sus clases de implementación se dividen principalmente en dos tipos: cargador de clases del sistema (BootClassLoader) y cargador de clases personalizado (PathClassLoader | DexClassLoader)

Primer vistazo al diagrama de flujo de carga de ClassLoader:

  • BootClassLoader

Se utiliza para cargar los archivos de clase de la capa de Android Framework, como Activity y Fragment, pero debe tenerse en cuenta que, aunque AppCompatActivity también es una clase proporcionada por los ingenieros de Google, pero una clase en un paquete de terceros no ingresa al Capa de estructura, por lo que AppCompatActivity no usa BootClassLoader Loaded

  • PathClassLoader

Cargador de clases para aplicaciones de Android. Puede cargar el dex especificado y classes.dex en jar, zip y apk

  • DexClassLoader

En la API posterior a Android 8.0, no hay diferencia entre PathClassLoader y PathClassLoader. En la API anterior, las dos solo tienen una diferencia en la configuración de la ruta de carga (algunos artículos dicen que PathClassLoader solo admite la manipulación directa de archivos de formato dex, mientras que DexClassLoader Puede admitir archivos .apk, .jar y .dex, y lanzará el archivo dex en la ruta de salida especificada. De hecho, no lo es, incluso se puede decir que no hay diferencia entre los dos)

Primero ponga un diagrama de herencia de clases ClassLoader, creo que se puede entender, no hablaré de eso, veamos el código fuente de PathClassLoader y 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);
}

De acuerdo con el código fuente, puede saber que PathClassLoader y DexClassLoader se heredan de BaseDexClassLoader, y solo hay métodos de construcción en la clase, y su lógica de carga de clases está completamente escrita en BaseDexClassLoader.

Vale la pena señalar que antes de 8.0, la única diferencia entre los dos es el segundo parámetro OptimDirectory, que significa la ruta donde se almacena el odex generado (dex optimizado). PathClassLoader es directamente nulo, mientras que DexClassLoader se usa La ruta que el usuario pasó , y después de 8.0, los dos son exactamente iguales.

Echemos un vistazo a la relación entre BootClassLoader y PathClassLoader:

// 在 onCreate 中执行下面代码
ClassLoader classLoader = getClassLoader();
while (classLoader != null) {
    Log.e("leo", "classLoader:" + classLoader);
    classLoader = classLoader.getParent();
}
Log.e("leo", "classLoader:" + Activity.class.getClassLoader());

Resultado de la impresión:

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

Se puede ver en el resultado de impresión que la clase de aplicación es cargada por PathClassLoader, la clase Activity es cargada por BootClassLoader y BootClassLoader es el padre de PathClassLoader Aquí debemos prestar atención a la diferencia entre el padre y la clase padre. Este resultado de impresión se mencionará a continuación.

Principio de carga

Entonces, ¿cómo usar el cargador de clases para cargar una clase de complemento desde dex? Muy simple

Por ejemplo, si hay un archivo apk con una ruta de apkPath y una clase com.plugin.Test en él, puede cargar una clase a través de la reflexión:

// 初始化一个类加载器
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)

Cargar clases en dex es muy simple, pero lo que necesitamos es cargar el dex en el complemento en el host, ¿qué debemos hacer? De hecho, el principio sigue siendo el mismo que el de la solución urgente. Tomemos como ejemplo la API 26 de Android 8.0. A través del código fuente, observe cómo el cargador de clases DexClassLoader carga el archivo dex en un apk.

A través de la búsqueda, se encuentra que DexClassLoader no tiene un método para cargar la clase. Continúe mirando su clase principal. Finalmente, encontré un método loadClass en la clase ClassLoader. Parece que este método se usa para cargar la clase:

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

La carga de clases anterior se divide en 3 pasos

1. Verifique si esta clase ha sido cargada, y finalmente llame al método nativo para encontrarla. No entraré aquí:

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

2. Si no se encuentra, llamará al método loadClass del padre para encontrarlo, de forma recursiva sucesivamente, y volverá si se encuentra. Si no se encuentran todos los superiores, llamará a findClass para encontrarlo nivel por nivel . Este proceso es el mecanismo de delegación principal

3 、 findClass

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

De acuerdo con los resultados de impresión anteriores, podemos entender que el nivel más alto de ClassLoader es BootClassLoader. Veamos cómo reescribe el método loadClass para finalizar la recursividad:

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

Puede ver en lo anterior que BootClassLoader reescribe los métodos findClass y loadClass, y en el método loadClass, el padre ya no se obtiene, terminando así la recursividad.

Luego baje, si no se encuentran todos los padres, ¿cómo se carga DexClassLoader? Mediante la búsqueda, su método de implementación está en su clase padre 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);
}

El método findClass en DexPathList se llama en findClass, continúe:

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

Aquí está claro que el objeto de clase se obtiene del Elemento, y cada Elemento corresponde a un archivo dex. Debido a que puede haber varios archivos dex en un apk, se usa una matriz para contener el Elemento. ¿Tienes una idea cuando cargas las clases en el apk aquí?

  1. Cree el cargador ClassLoader (PathClassLoader o DexClassLoader) del complemento y luego obtenga el valor de la matriz dexElements del complemento a través de la reflexión.
  2. Obtenga el cargador ClassLoader del host y obtenga el valor de la matriz dexElements del host a través de la reflexión.
  3. Combine las matrices dexElements del host y los complementos para generar una nueva matriz
  4. Reasigne la nueva matriz a los dexElements del host a través de la reflexión

Implementación

No hay muchas tonterías, solo ve al código: (Yo uso kotlin aquí, se siente más conveniente de escribir)

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)

    }

De esta manera, las clases de los dos archivos dex se fusionan y las clases del complemento se pueden cargar directamente en el host.

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()
        }
}

Bueno, ¡echemos un vistazo al efecto de ejecución!

Limitaciones de espacio, hablaré de ello hoy, y hay dos problemas más (cargar imágenes de recursos y cuatro componentes principales). Mira los dos artículos siguientes.

Implementar un marco de complementos desde cero (2):

https://blog.csdn.net/qq_22090073/article/details/104053249

Implementar un marco de complementos desde cero (3):

https://blog.csdn.net/qq_22090073/article/details/104063781

Dirección de origen de este artículo:

https://github.com/plumcookingwine/DynamicTest

Al final

Además, también comparto un PDF de aprendizaje de Android + video de arquitectura + documento de entrevista + notas de origen , mapa mental avanzado de tecnología de arquitectura avanzada, materiales de temas de entrevistas de desarrollo de Android, materiales de arquitectura avanzada avanzada recopilados y organizados por los grandes.

Estos son los materiales nobles que leeré una y otra vez en mi tiempo libre. Hay explicaciones detalladas de los puntos de conocimiento de alta frecuencia de las entrevistas con las principales fábricas en los últimos años. Creo que puede ayudar eficazmente a todos a dominar el conocimiento y a comprender los principios.

Por supuesto, también puede utilizarlo para comprobar si hay omisiones y mejorar su competitividad.

Si lo necesitas, puedes  conseguirlo aquí

Si te gusta este artículo, también puedes darme un me gusta, dejar un mensaje en el área de comentarios o reenviarlo y apoyarlo ~

Supongo que te gusta

Origin blog.csdn.net/River_ly/article/details/107520870
Recomendado
Clasificación