Actualización en caliente del principio de carga de clases de Tinker


El lenguaje básico de Android es Java, y la máquina virtual Java carga las clases y recursos correspondientes en tiempo de ejecución a través de ClassLoader. ClassLoader en sí es una abstracción. Android usa la clase PathClassLoader como cargador de clases predeterminado de Android.

Android中的虚拟机不是JVM,而是Dalvik/ART VM。不同于JVM中加载.class,Dalvik/ART加载的是.dex文件。

Mecanismo de carga de clases

Después de compilar cada clase, se genera un objeto Class y se almacena en el archivo .class. La JVM utiliza el cargador de clases Class Loader para cargar el archivo de código byte .class de la clase. El cargador de clases es esencialmente una cadena de cargadores de clases. Generalmente, Solo usaremos un cargador de clases nativo, que solo carga clases confiables como la API de Java, generalmente solo cargadas en el disco local, estas clases generalmente son suficientes para que las usemos.

Si necesitamos descargar archivos de código de bytes .class desde una red o base de datos remota, necesitamos montar un cargador de clases adicional. Es decir, hay varios cargadores de clases en el cargador de clases, generalmente solo se usa un PathClassLoader predeterminado y se requiere DexClassPath en situaciones adicionales.

Estrategia de carga

La estrategia de prioridad de la clase principal es una situación relativamente general (como JDK adopta este método). Bajo esta estrategia, antes de cargar una clase Java, la clase intentará proxy a su cargador de clases principal, solo cuando la clase principal esté cargada. Cuando no se puede encontrar el dispositivo, intenta cargarlo por sí mismo.
La estrategia de autoprimera prioridad es lo opuesto a la prioridad principal. Primero intentará cargarlo por sí mismo. Cuando no pueda encontrarlo, el cargador de clases principal lo cargará. Esto es más común en contenedores web (como tomcat).

Carga e inicialización de clases

El cargador de clases carga el archivo .class de una clase, lo que no significa que el objeto Class esté inicializado. La inicialización de una clase consta de 3 pasos:

  1. Loading (Loading), ejecutado por el cargador de clases, busca el bytecode y crea un objeto Class (solo creación);
  2. Vincular, verificar el código de bytes, asignar espacio de almacenamiento para dominios estáticos (solo asignar, pero no inicializar el espacio de almacenamiento) y analizar aplicaciones para otras clases necesarias para la creación de esta clase;
  3. Inicialización (inicialización), primero ejecute el bloque de inicialización estático static {}, inicialice las variables estáticas y ejecute métodos estáticos (como los métodos de construcción).

Nota:类与接口的初始化不同,如果一个类被初始化,则其父类或父接口也会被初始化,但如果一个接口初始化,则不会引起其父接口的初始化。

Carga dinámica

No importa qué cargador de clases se use, la clase se carga dinámicamente en la JVM cuando se usa por primera vez. Esta función es la función de carga dinámica de Java :

  1. El programa Java no necesariamente se carga completamente cuando se está ejecutando, solo cuando se encuentra que la clase no ha sido cargada, el archivo .class de la clase se encuentra local o remotamente y se verifica y carga;
  2. Cuando el programa crea la primera referencia a los miembros estáticos de la clase (como variables estáticas de clase, métodos estáticos, métodos de construcción, el método de construcción también es estático), la clase se cargará.

Enlace de clase

La vinculación de clases Java se refiere al proceso de fusionar el código binario de la clase Java en el estado de ejecución de la JVM. Es un paso necesario para garantizar que la clase cargada pueda ejecutarse normalmente en la máquina virtual. El enlace tiene 3 pasos:

  1. Verificación, verificación es para asegurar la corrección estructural del código de bytes binario, incluida la verificación de la corrección del tipo, la corrección de los
    atributos de acceso (público, privado), la verificación de que la clase final no se hereda y la verificación de la corrección de las variables estáticas, etc. .

  2. Preparación. La fase de preparación es principalmente para crear dominios estáticos, asignar espacio y establecer valores predeterminados para estos dominios
    . Hay dos puntos a tener en cuenta: uno es que no se ejecutará ningún código en la fase de preparación, pero el valor predeterminado está establecido y el otro es Estos valores predeterminados se asignan de esta manera, todos los
    tipos primitivos se establecen en 0, como: float: 0f, int 0, long 0L, boolean: 0 (el tipo booleano también es 0) y otros tipos de referencia son nulos.

  3. Resolución: el proceso de análisis es analizar y ubicar las referencias simbólicas de interfaces, clases, métodos y variables en la clase, y
    resolverlas en referencias directas (una referencia simbólica es un código que usa una cadena para representar la ubicación de una variable o interfaz. Referencia es la dirección traducida de acuerdo con la referencia del símbolo),
    y para asegurar que estas clases se encuentran correctamente.
    Estrategia de
    resolución : resolución temprana : requiere que existan todas las referencias, por lo que todas las referencias se resuelven de forma recursiva durante el análisis.
    Resolución tardía : la estrategia adoptada por el JDK de Oracle, que no procede cuando la clase solo se hace referencia pero no se utiliza realmente Al analizar, esta clase se cargará y analizará solo cuando se use realmente.

ClassLoader

Cargador de clases, Cargador de clases.
La estructura jerárquica de ClassLoader:
Inserte la descripción de la imagen aquí
el proceso de carga de clases en la máquina virtual Dalvik en
Inserte la descripción de la imagen aquí
el cargador de clases de Android Android se divide en dos tipos de PathClassLoader y DexClassLoader , ambos heredan de BaseDexClassLoader, y BaseDexClassLoader hereda de ClassLoader.
PathClassLoader: se usa para cargar clases de sistema y clases de aplicación. El dex en caché, como todos los .dex en ART, es el cargador predeterminado en la máquina virtual de Android.
DexClassLoader: se usa para cargar archivos jar.apk y dex, o desde Cargue en la tarjeta SD. La esencia máxima de cargar jar y apk es extraer los archivos Dex que están dentro para cargarlos.

Modelo de delegación de padres

Tanto DexClassLoader como PathClassLoader pertenecen a cargadores de clases que se ajustan al modelo de delegación parental (porque no sobrecargan el método loadClass).
La característica
es que cuando se solicita a un cargador que cargue una determinada clase, primero le confía a su cargador principal que lo cargue y sigue mirando hacia arriba. Si el cargador superior (preferido) o el cargador de clases principal puede cargar, regresa a la clase correspondiente Si el objeto Class no se puede cargar, el iniciador de la solicitud finalmente cargará la clase.
Si ya se ha cargado, se devolverá directamente sin carga repetida.

【Nota:】这就是为什么采用类加载方案的热修复需要冷启动生效的原因:补丁合成好之前类已加载,想要替换bug类,需要重新启动软件,重新加载修复好的类 .
Cuando
se carga la clase de rendimiento , atravesará el archivo dex y cargará primero el dex anterior. La actualización en caliente de carga de clases significa que el archivo dex con el problema solucionado se carga cuando se reinicia la aplicación.
Ventajas La ventaja de
este método es que la carga de clases se realiza en un cierto orden de reglas, cuanto más básicas son las clases, más carga el cargador de clases superiores, garantizando así la seguridad del programa.

Código fuente clave del cargador de clases

1. ClassLoader
puede verse en el código fuente que el SystemClassLoader predeterminado en la máquina virtual es PathClassLoader.

    /**
     * Encapsulates the set of parallel capable loader types.
     * 封装一组并行的加载器类型。
     */
    private static ClassLoader createSystemClassLoader() {
    
    
        String classPath = System.getProperty("java.class.path", ".");
        String librarySearchPath = System.getProperty("java.library.path", "");
        // 看见了吧 PathClassLoader 是默认的类加载器
        return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
    }

2. BaseDexClassLoader

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
    
    
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

Además de llamar al método de construcción de la clase principal ClassLoader, BaseDexClassLoader también inicializa un objeto DexPathList en el constructor, que es una lista de entradas que describen archivos de recursos relacionados con DEX.

3. DexPathList

   final class DexPathList {
    
    
  ...
  
    public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
    
    

        .......
        this.definingContext = definingContext;
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
        if (suppressedExceptions.size() > 0) {
    
    
            this.dexElementsSuppressedExceptions =
                    suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
    
    
            dexElementsSuppressedExceptions = null;
        }
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }

    public Class findClass(String name, List<Throwable> suppressed) {
    
    
            //遍历该数组
            for (Element element : dexElements) {
    
    
                //初始化DexFile
                DexFile dex = element.dexFile;

                if (dex != null) {
    
    
                    //调用DexFile类的loadClassBinaryName方法返回Class实例
                    Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                    if (clazz != null) {
    
    
                        return clazz;
                    }
                }
            }
            return null;
        }
    
...}

Se puede ver en el código que lo más importante para dexElementsel hotfix es inicializar en la inicialización de BaseDexClassLoader. En el constructor de DexPathList, se llama a makeDexElements para analizar los parámetros relacionados con dex y guardarlos en la variable miembro dexElements. El orden de los miembros dexElements determina el orden de carga de .dex.

El método findClass de DexPathList es detectar la clase escaneada. El método atravesará dexElements y luego obtendrá el DexFile de cada elemento. Si DexFile no está vacío, llame a su loadClassBinaryName y devolverá la instancia de la clase. Una vez que se carga ClassLoader en la clase correcta, ya no se cargará la misma clase.
findClass方法是ClassLoader的核心

5. El
método loadDexFile de makeDexElements carga el archivo dex en el método makeDexElements y devuelve el objeto DexFile.

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                             ArrayList<IOException> suppressedExceptions) {
    
    
        ArrayList<Element> elements = new ArrayList<Element>();
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
    
    
            File zip = null;
            DexFile dex = null;
            String name = file.getName();

            if (name.endsWith(DEX_SUFFIX)) {
    
    
                // Raw dex file (not inside a zip/jar).
                try {
    
    
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
    
    
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
    
    
                zip = file;

                try {
    
    
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
    
    
                    /*
                     * IOException might get thrown "legitimately" by the DexFile constructor if the
                     * zip file turns out to be resource-only (that is, no classes.dex file in it).
                     * Let dex == null and hang on to the exception to add to the tea-leaves for
                     * when findClass returns null.
                     */
                    suppressedExceptions.add(suppressed);
                }
            } else if (file.isDirectory()) {
    
    
                // We support directories for looking up resources.
                // This is only useful for running libcore tests.
                elements.add(new Element(file, true, null, null));
            } else {
    
    
                System.logW("Unknown file type for: " + file);
            }

            if ((zip != null) || (dex != null)) {
    
    
                elements.add(new Element(file, false, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }

6. dexElements

    List of dex/resource (class path) elements.Should be called pathElements,
    but the Facebook app uses reflection to modify 'dexElements'
    dex / resource(类路径)元素的列表。应称为pathElements,但Facebook应用程序使用反射来修改“ dexElements”

dexElements es un Element [], que es el núcleo de la actualización en caliente del modo de carga de clases.
Su función es mantener todos los archivos dex (la representación binaria de la clase que escribimos, utilizada para cargar la máquina virtual de Android), almacenados en el programa de Android.

La máquina virtual de Android carga los archivos de clase correspondientes de la matriz en orden descendente según sea necesario. Incluso si hay varios archivos dex correspondientes a la misma clase en la matriz, una vez que la máquina virtual encuentre los archivos dex correspondientes, dejará de buscar y carga. De acuerdo con esta regla, solo necesitamos insertar los archivos de clase involucrados en la reparación del error en la parte frontal de la matriz para lograr el propósito de la reparación.

Por ejemplo: cuando un patch.dex se coloca en el primer lugar de dexElements, luego cuando se carga una clase de error A, y la clase A reparada se encuentra en patch.dex, esta clase se cargará directamente. La clase A no reparada también se escaneará cuando se cargue class.dex, pero la clase A se cargó y A no se recargará, es decir, se logra el efecto de reparación.

7. DexClassLoader

   public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath,ClassLoader parent) {
    
    
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }

dexPath : la ruta para cargar APK, DEX y JAR. Se puede usar entre múltiples rutas: Split
Esta clase se puede usar para que Android cargue DEX / JAR dinámicamente.
OptimizadoDirectory: Cuando el archivo dex se carga por primera vez, se realizará la operación dex opt. El OptimizadoDirectory es el directorio de almacenamiento del odex optimizado. El directorio no está vacío y el directorio privado de la aplicación se recomienda oficialmente, dexOutputDir = context.getDir ("dex", 0).
libraryPath : La ruta de la biblioteca dinámica lib que se debe usar al cargar DEX, libraryPath generalmente incluye / vendor / lib y / system / lib. No puede ser
padre vacío : el cargador de clases padre especificado por DEXClassLoader, tipo de parámetro ClassLoader

【Nota】

  1. El archivo cargado por este cargador de clases es un archivo .jar o .apk, y este .jar o .apk contiene el archivo de entrada classes.dex, que se utiliza principalmente para ejecutar algunos archivos ejecutables que no están instalados. Por ejemplo, el paquete de parche de Bugly en la actualización en caliente es un archivo .apk y el parche generado por Sophix es un archivo .jar.

  2. Este cargador de clases necesita un directorio privado que pertenezca a la aplicación como su propio directorio optimizado de caché. Este directorio también es el segundo parámetro del constructor (ruta de salida dex)

  3. No configure el directorio de caché mencionado en el segundo punto anterior como almacenamiento externo, porque el almacenamiento externo es vulnerable a los ataques de inyección de código.

8. PathClassLoader

public class PathClassLoader extends BaseDexClassLoader {
    
    
 
    public PathClassLoader(String dexPath, ClassLoader parent) {
    
    
        super(dexPath, null, null, parent);
    }
 
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
    
    
        super(dexPath, null, libraryPath, parent);
	}
} 

El sistema Android utiliza PathClassLoader como cargador del sistema y cargador de aplicaciones.
La diferencia entre PathClassLoader y DexClassLoader es si el parámetro OptimDirectory está vacío

9.
El método findLoadedClass está en ClassLoader y su subclase no tiene este método. Llame a findLoadedClass para averiguar si la máquina virtual actual ha cargado la clase; en caso afirmativo, devuelva la clase directamente. Si no se ha cargado, llame al método loadClass del cargador principal, donde se utiliza el modelo de delegación principal de Java.

 protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    
    
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
    
    
            ClassNotFoundException suppressed = null;
            try {
    
    
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
    
    
                suppressed = e;
            }

            if (clazz == null) {
    
    
                try {
    
    
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
    
    
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }

        return clazz;
    }

10. La diferencia entre DexClassLoader y PathClassLoader
En el método de construcción, el parámetro OptimizedDirectory de PathClassLoader puede estar vacío y el parámetro en DexClassLoader no puede estar vacío. PathClassLoader generará automáticamente un directorio de caché /data/dalvik-cache/[email protected]. DexClassLoader utiliza la ruta de caché predeterminada del sistema.

Entonces, generalmente PathDexClassLoader solo puede cargar la dex del apk instalado, mientras que DexClassLoader puede cargar el apk, dex y jar de la ruta especificada, o cargar desde la tarjeta sd.

Cuando se subcontrata dex, usamos PathClassLoader para obtener la información dex cargada almacenada en pathList, y luego usamos DexClassLoadder para cargar el archivo dex secundario especificado y fusionar la información dex en los dexElements de pathList, de modo que la aplicación pueda ejecutarse Cargue todas las clases del dex en la memoria.

Principio de actualización en caliente de carga de clases

  1. Al obtener el Classloader de la aplicación actual, es el BaseDexClassloader
  2. Obtenga su objeto de atributo DexPathList pathList mediante la reflexión
  3. Invocar el método dexElements de pathList mediante la reflexión para convertir patch.dex en Element []
  4. Dos elementos [] se combinan y el archivo fix.dex combinado se coloca en la parte superior de dexElements
  5. Elemento de carga [] para lograr el propósito de reparación

Proceso de actualización en caliente en Tinker

Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí

  1. La solución Tinker hace referencia al principio de implementación de multidex, y genera patch.dex diferencial a través del Dex de los APK antiguos y nuevos durante la compilación.
  2. El patch.apk se distribuye al terminal a través de la plataforma correspondiente, y el patch.dex y la versión anterior de .dex se fusionan y restauran en el nuevo .dex.
  3. Inserte el dex recién sintetizado en el frente de la matriz dexElements, de modo que la clase de contenido en el nuevo .dex se cargue primero
  4. El error se solucionará la próxima vez que inicie el programa

其中有些常识需要额外注意

  1. Para reducir el tamaño del parche, Tinker desarrolló por sí mismo el algoritmo DexDiff, utilizando el formato Dex para reducir la diferencia. Deje que el parche contenga solo las diferentes partes y no las mismas partes
  2. El formato del paquete de parches de Tinker es el formato .apk, además de patch.dex, también contiene paquetes de diferencia de recursos y otros archivos
  3. Dado que el proceso de fusión requiere mucho tiempo, existe un PatchService independiente para fusionar.
  4. El principio de implementación de la carga de clases implica un procesamiento como la descompresión y la fusión de archivos dex, que consume mucha memoria, lleva mucho tiempo y tiende a fallar cuando la memoria del sistema es baja. Es decir, la actualización en caliente de carga de clases tiene una cierta tasa de fallas.

La carga de clases de QZone y Tinker es un poco diferente. QZone no usa patch.dex para fusionarse con el antiguo .dex, por lo que se producirá el problema CLASS_ISPREVERIFIED. Y el patch.dex de Tinker se fusionó con el antiguo .dex en el mismo .dex, de hecho, el orden del contenido de patch.dex se clasifica antes que el de repair.dex, y las clases en el .patch se pueden cargar primero.

Para aclarar la actualización en caliente del mecanismo de carga de clases, este blog está especialmente escrito con la esperanza de ayudarlo a comprender el principio de actualización en caliente basado en el esquema de carga de clases.

Supongo que te gusta

Origin blog.csdn.net/luo_boke/article/details/106219691
Recomendado
Clasificación