Práctica de tecnología de cobertura de código de alto rendimiento y alta estabilidad de Android

Prefacio

La cobertura de código es un método de medición en las pruebas de software que refleja la proporción y el alcance del código que se prueba.

En el proceso de iteración del software, además de prestar atención a la cobertura del código durante el proceso de prueba, la cobertura del código durante el uso del usuario también es un indicador muy valioso y no se puede ignorar. Porque junto con la expansión del negocio y las actualizaciones de funciones se genera una gran cantidad de código obsoleto y abandonado, que rara vez o incluso ya no se utiliza, o está "en mal estado" y carece de mantenimiento, lo que no sólo afecta al volumen de paquetes de aplicaciones. , pero también puede conllevar riesgos para la estabilidad. En este momento, es muy importante poder recopilar la cobertura del código del entorno de producción, comprender el uso del código en línea y proporcionar una base para el código inútil fuera de línea.

Objetivo

Nuestro objetivo es muy claro: de acuerdo con la configuración de la nube, recopilar el alcance y la frecuencia de uso de cada categoría en línea, subirlo a la nube, procesarlo en la plataforma y brindar capacidades de visualización de consultas y informes .

8d64ed55b5b677838bb3a6538e486e1d.jpeg

Como se muestra en la figura anterior, esperamos que los datos de cobertura del código se puedan consultar y mostrar intuitivamente en la plataforma, y ​​se puedan ver directamente cuando sea necesario, proporcionando una base para la toma de decisiones sobre código antiguo fuera de línea, programación y asignación de recursos, etc. y, en última instancia, proporcionar a los usuarios un paquete de instalación de aplicaciones más pequeño para una mejor experiencia funcional.

A través del centro de control de la nube, podemos controlar si habilitamos la recopilación de cobertura y también podemos ajustar dinámicamente la estrategia de programación y asignación de recursos, como posiciones de diamantes e hilos en la aplicación, según la cobertura (frecuencia de uso de clase). Entre ellos, la solución de recopilación de cobertura es la parte más importante. Hay muchas soluciones maduras en la industria, pero todas tienen sus propios escenarios adecuados. Nuestro objetivo es recopilar código con granularidad de clase sin afectar tanto el uso del usuario como el funcionamiento de la aplicación. posible Utilice cobertura. La solución de recolección utilizada debe ser menos complicada, simple de implementar, tener en cuenta tanto la estabilidad como el rendimiento y, al mismo tiempo, no debe invadir el proceso de empaquetado ni afectar el tamaño del paquete. Después de una exploración en profundidad, hemos desarrollado una conjunto de soluciones que cumplen perfectamente con estos requisitos Solución completamente nueva.

Comparación de esquemas

La siguiente tabla muestra la comparación de varios indicadores entre soluciones comunes y soluciones de desarrollo propio. El verde indica mejor.

8cb78dd622aecd2a96c90f661e2c0904.png

Como se puede ver en la tabla:

solución jacoco

Algunos similares incluyen Emma, ​​​​Cobertura, etc. Todos se implementan mediante instrumentación y pueden admitir la recopilación de todas las versiones y granularidades, pero la instrumentación tiene un cierto impacto en el tamaño y el rendimiento del paquete y no es adecuada para uso en línea a gran escala. .

Esquema Hook PathClassLoader

Es fácil de implementar, no intruye el código fuente y es compatible con todas las versiones de Android. Sin embargo, Hook PathClassLoader no solo afecta el rendimiento, sino que incluso puede afectar la estabilidad de la aplicación.

Hackear el esquema ClassTable de acceso

Se puede recopilar bajo demanda y casi no tiene impacto en el rendimiento de la aplicación, pero el hackeo puede causar problemas de compatibilidad y es más complejo de implementar.

Plan de autoinvestigación

  • Excelente rendimiento, admite recopilación bajo demanda sin comprometer el rendimiento de la aplicación

  • Es simple de implementar, no utiliza ninguna "tecnología negra" y tiene una excelente estabilidad y compatibilidad.

  • Admite recopilación de complementos y procesos cruzados

De la comparación, aprendimos que la solución de desarrollo propio puede satisfacer mejor nuestros requisitos para recopilar cobertura de código en línea, porque no solo tiene buena estabilidad, sino que también tiene un rendimiento excelente y apenas tendrá ningún impacto en los usuarios. Entonces, ¿cómo logra un alto rendimiento y una alta estabilidad? Consulte la introducción a continuación.

Una introducción

principio

Para recopilar cobertura de código granular de clases, en realidad necesita saber qué clases se cargan y utilizan durante la ejecución de la aplicación. En las aplicaciones Java, esto se puede consultar directamente llamando al método findLoadedClass de ClassLoader, pero en la aplicación de Android no es tan simple. La razón es que el sistema Android ha realizado la siguiente optimización:

Para mejorar el rendimiento de inicio, para las clases personalizadas de la aplicación, es decir, las clases cargadas por PathClassLoader, si llama directamente a findLoadedClass para realizar consultas, la operación de carga se realizará incluso si la clase no está cargada.

Esto no es lo que esperábamos.

Aunque no podemos llamar directamente al método FindLoadedClass para consultar el estado de carga de la clase, después de una investigación y un análisis en profundidad, descubrimos que ClassLoader finalmente obtiene el estado de carga de la clase consultando su campo ClassTable. Si también podemos acceder a ClassTable, el problema se solucionará.? En esta línea de pensamiento, propusimos de manera innovadora una solución para copiar el puntero ClassTable y acceder indirectamente al estado de carga de clases a través de la API estándar .

Esta solución logra inteligentemente el acceso libre de hacks a ClassTable y al mismo tiempo evita perfectamente la optimización de carga de clases que no necesitamos, logra la adquisición del estado de carga de clases en solo unas pocas líneas de código, lo cual es inteligente y conciso. Además tiene las siguientes ventajas:

  • La velocidad de recolección es más de 5 veces mayor que la de las soluciones ordinarias , con un rendimiento excelente.

  • Utilice API estándar para acceder a ClassTable, con excelente compatibilidad y estabilidad

  • Sólo se utiliza un reflejo, sin "tecnología negra", simple y estable

  • No afecta la carga de clases ni la ejecución de la aplicación.

  • Admite perfectamente la colección de múltiples procesos y complementos

Pero hay una cosa a tener en cuenta:

El campo ClassTable se introdujo a partir de Android N, por lo que este método solo es aplicable a Android N y superiores. Por necesidad y consideraciones de ROI, no nos hemos adaptado a versiones inferiores a Android N.

Proceso de cobranza

Basándonos en la solución anterior, diseñamos una función de recopilación de cobertura de código completa. Los procesos clave son los siguientes:

cd9255384372a8dec3894020860fde49.jpeg

Se puede ver que todo el proceso de recopilación final es en serie, lo cual es muy conveniente para el control del proceso y la integración de datos. A continuación se explican las ideas de diseño:

  • Al recopilar, la aplicación se divide en dos partes: una parte son los datos del host utilizados por el proceso principal y los subprocesos, y la otra parte son los datos del complemento.

  • Según la recopilación del modo de consulta, el proceso principal, el subproceso y el complemento proporcionan respectivamente interfaces para consultar el estado de carga de clases.

  • El proceso se basa en el método en serie y está controlado por el proceso principal. Las interfaces correspondientes se llaman en secuencia para recopilar datos del proceso principal, subprocesos y complementos.

  • Cada versión solo recopila y reporta datos de clases descargadas. Cuando se recopila por primera vez, se utiliza como entrada el conjunto completo de clases; para cada colección posterior, se utilizan como entrada las clases que no se han cargado en la versión anterior. Cuantas más veces cuantas más clases sea necesario consultar.

  • El proceso principal y los subprocesos se consultan en secuencia. Todas las consultas se basan en las clases descargadas restantes después de la última consulta. Por lo tanto, los subprocesos posteriores requieren menos consultas. El mismo complemento también se consulta en instancias de diferentes procesos. .

  • Como se muestra abajo:

exterior_default.png

  • Al final de la recopilación, se generará una copia de los datos de la clase de host y N copias de los datos de la clase de complemento (si hay N complementos). Estos datos se diferenciarán de los resultados de la recopilación anterior y los datos incrementales se cargarán en el servicio.

  • La plataforma de servicio realiza almacenamiento, desasignación, asociación de módulos y otros procesos, y finalmente los agrega y muestra en forma de informes.

Vale la pena señalar:

  • Las clases utilizadas por el proceso principal y los subprocesos pertenecen al host y los resultados de la recopilación deben fusionarse en un solo dato. De manera similar, no importa en cuántos procesos se cargue un complemento, solo un dato para el complemento debe generarse al final.

  • Al recopilar, dividimos los datos en dos partes, lo que puede mejorar la eficiencia de la recopilación y facilitar la desofuscación posterior; cuando se muestran en la plataforma, la visualización combinada es más significativa.

Gestión de versiones

La mayoría de los códigos de aplicaciones de Android se han ofuscado y los nombres de las clases ofuscadas variarán según la versión, lo que requiere administrar los datos de cobertura según la versión de la aplicación.

Después de administrar los datos por versión, cada versión borrará los datos de la versión anterior para evitar confusión de datos, se registrará una clase específica después de que se haya utilizado la versión actual y la recopilación posterior de esta versión no consultará repetidamente su uso.

Cuando se recopila cada versión por primera vez, se debe utilizar como entrada el conjunto completo de nombres de clases de la aplicación. Cada colección generará una colección de clases no utilizadas como entrada para la siguiente colección. De esta manera, la cantidad de clases a las que se debe prestar atención en cada colección en una versión se reducirá gradualmente, lo que puede evitar consultas sin sentido y mejorar el rendimiento de la colección.

Adquisición de datos de nombre de clase

Los datos del nombre de la clase se pueden obtener de dos maneras:

1. Obtener del paquete de instalación

Los datos del nombre de la clase en el paquete de instalación se pueden obtener de PathClassLoader y el complemento se puede obtener del BaseDexClassLoader correspondiente. Utilice el siguiente método:

public static List<String> getClassesFromClassLoader(BaseDexClassLoader classLoader) throws ClassNotFoundException, IllegalAccessException {
    //类名数据位于BaseDexClassLoader.pathList.dexElements.dexFile中,可以通过反射获取


    //先获取pathList字段
    Field pathListF = ReflectUtils.getField("pathList", BaseDexClassLoader.class);
    pathListF.setAccessible(true);
    Object pathList = pathListF.get(classLoader);


    //获取pathList中的dexElements字段
    Field dexElementsF = ReflectUtils.getField("dexElements", Class.forName("dalvik.system.DexPathList"));
    dexElementsF.setAccessible(true);
    Object[] array = (Object[]) dexElementsF.get(pathList);


    //获取dexElements中的dexFile字段
    Field dexFileF = ReflectUtils.getField("dexFile", Class.forName("dalvik.system.DexPathList$Element"));
    dexFileF.setAccessible(true);
    ArrayList<String> classes = new ArrayList<>(256);
    for (int i = 0; i < array.length; i++) {
        //获取dexFile
        DexFile dexFile = (DexFile) dexFileF.get(array[i]);
        //遍历DexFile获取类名数据
        Enumeration<String> enumeration = dexFile.entries();
        while (enumeration.hasMoreElements()) {
            classes.add(enumeration.nextElement());
        }
    }
    return classes;
}

Este método es simple y directo, pero cargará todos los nombres de clases en DexFile en la memoria a la vez. Según nuestras pruebas, cada 10.000 clases ocupan aproximadamente 0,8 MB de memoria. Para aplicaciones grandes con decenas de miles de clases, dicho esto , habrá mucha memoria sobrecargada. Entonces también puedes considerar la segunda forma.

2. Descarga en la nube

Obtenga los datos del nombre de la clase de la plataforma de construcción y cárguelos en la plataforma en la nube. La aplicación se puede descargar y utilizar cuando sea necesario.

En cuanto a qué método elegir, simplemente elija según la cantidad de clases. Cuando el número de clases es particularmente grande, como en escenarios de aplicaciones a gran escala, se recomienda utilizar el método de la nube; para aplicaciones o complementos normales, puede obtenerlos directamente desde la clase del paquete de instalación.

Colección de subprocesos

Para las clases que no están cargadas por el proceso principal, las entregaremos al proceso secundario para que las consulte nuevamente. Esto requiere que el subproceso proporcione una interfaz de consulta que admita llamadas entre procesos. Elegimos la solución AIDL que es simple, confiable y fácil de reutilizar para implementar esto.

Los pasos específicos son:

Defina la interfaz de consulta a través de AIDL y defina la Acción correspondiente. En el método onBind del Servicio, la clase de implementación Binder de la interfaz de consulta se devuelve de acuerdo con la Acción para llamadas remotas.

Al mismo tiempo, considerando el alto costo del proceso cruzado, sin duda es inaceptable llamar a la interfaz de consulta una vez para cada clase. Entonces pensamos en el método de consulta de archivo + lote: usar archivos como soportes de datos, escribir clases cargadas y descargadas en archivos y pasar rutas de archivos entre interfaces. Las operaciones de archivos también pueden utilizar BufferedReader y BufferedWriter para mejorar el rendimiento.

El proceso de llamada es como se muestra en la figura:

6fdc20333382ec192af61b60ec951954.jpeg

Los beneficios de hacer esto también son obvios:

  • La recopilación de un proceso solo requiere una llamada entre procesos y el costo es extremadamente bajo

  • Evite la sobrecarga de memoria de la serialización de datos

  • Evitar el problema de que los big data no se pueden transferir directamente entre procesos

  • El proceso de recolección es más simple y los procesos requeridos se pueden recolectar bajo demanda.

  • Facilita el filtrado de datos, evita consultas repetidas de clases cargadas y mejora el rendimiento de la recopilación.

Colección de complementos

Para la clase de host, simplemente consulte la ClassTable correspondiente a PathClassLoader.

Los complementos generalmente se cargan a través de BaseDexClassLoader o sus clases derivadas, y es necesario consultar la ClassTable del ClassLoader correspondiente.

Para los complementos utilizados en procesos secundarios, solo hay una llamada de interfaz entre procesos adicional para devolver las clases cargadas y las clases restantes al proceso principal para su procesamiento.

Los pasos de recolección son los siguientes:

  • Al consultar la clase de subproceso, también se consultará la clase de complemento que se ejecuta en el proceso y los datos se escribirán en archivos divididos por nombres de complemento.

  • La recopilación de complementos del proceso principal es el último paso de todo el proceso, en este momento se detectarán, fusionarán los archivos de datos correspondientes a cada complemento (generados por subprocesos) y finalmente se almacenarán los archivos de datos. eliminado.

  • Finalmente, se procesan los archivos de datos de complementos restantes, que pertenecen a complementos que solo se ejecutan en el proceso secundario.

En este punto, ha obtenido los datos de carga de clases de todos los complementos.

Resolver mapeo

Al observar los datos de cobertura del código, esperamos ver el nombre de la clase original, por lo que desasignar es el único camino a seguir.

La operación de desmapeo se puede realizar al final o en el lado del servicio, por razones de seguridad elegimos el lado del servicio.

Los archivos de mapeo se generan mediante el proceso de empaquetado, uno para cada paquete de instalación. Nuestro enfoque es utilizar un script para generar archivos de mapeo para clases ofuscadas y clases de texto sin formato al construir la plataforma y el paquete oficial. Cuando sea necesario, el servidor obtiene el archivo de mapeo correspondiente a través de la información de la versión de la aplicación, decodifica el nombre de la clase original y lo compara con el módulo Hacer una asociación.

Lo que finalmente se muestra en la plataforma son los datos de cobertura del código que se han mapeado y asociado con módulos y complementos.

Almacenamiento de datos y cálculo incremental.

Los datos recopilados deben almacenarse. Para facilitar el cálculo de los datos incrementales, elegimos la base de datos como solución de almacenamiento porque tiene funciones inherentes de deduplicación y clasificación, y su rendimiento también es bueno. El método específico es:

  • Para crear una tabla de datos, solo necesita incluir una columna llamada clase, esta columna se declara como clave principal y no acepta valores nulos ni duplicados.

  • Antes de cada recopilación, se obtiene el número de filas. Durante el proceso de recopilación, los datos del nombre de la clase cargada se actualizan en la tabla, lo que permite que la base de datos complete automáticamente la deduplicación. Una vez completada la recopilación, se obtiene nuevamente el número de filas de datos. El desplazamiento obtenido restando el número de filas antes de la recopilación es la parte incremental. Solo necesitamos cargar esta parte de los datos al servicio.

Rendimiento y estabilidad

Después de nuestras repetidas pruebas y ajustes, el tiempo promedio de recopilación para las categorías 5w+ es de aproximadamente 0,5 s/tiempo. Durante el período de recopilación, la memoria aumenta a aproximadamente 500 kb y la CPU no aumenta significativamente.

Al mismo tiempo, ha sido verificado por múltiples versiones de Amap en línea y no se encontraron fallas relacionadas ni ANR.

otro

Evita las listas negras y grises

Después de Android P, las variables miembro de ClassTable se agregaron oficialmente a las listas negra y gris. Antes de usar el acceso de reflexión, se deben evitar las restricciones del SDK. Utilizamos el método de metarreflexión + exención de configuración. Para una implementación específica, consulte el proyecto de código abierto FreeReflection en GitHub. Si desea obtener más información, puede buscar en Google.

Calendario y frecuencia de recogida

Aunque el proceso de recopilación es breve e inocuo, para minimizar el impacto en la ejecución de la aplicación, colocamos el trabajo de recopilación en un subproceso y elegimos iniciar la ejecución después de que la aplicación haya salido del fondo por un período de tiempo.

Al mismo tiempo, dado que solo necesitamos conocer la proporción y la situación general del uso del código, solo podemos recopilarlo una vez después de cada inicio en frío.

Los datos después de varios arranques en frío por parte de varios usuarios son suficientes para reflejar el uso real del código. Si necesita datos de frecuencia de uso para cada categoría, también puede obtenerlos agregando estadísticas en el lado del servidor.

escribe al final

Como método de medición, la cobertura del código no solo nos proporciona una base para eliminar el código antiguo, sino que también refleja la popularidad de una determinada función, puede proporcionar una base para la asignación de recursos, decisiones de programación, etc., es un factor indispensable en el software. desarrollo Falta una herramienta importante.

Nuestra nueva solución es concisa pero no simple. Logra inteligentemente una recopilación libre de piratería. Logra elegantemente una recopilación de alto rendimiento de cobertura de código del entorno de producción al mismo tiempo que garantiza una alta estabilidad y no se inmiscuye en el código fuente. Ya es demasiado alta. La verificación de versiones es una solución madura, estable y eficiente. Lo comparto aquí con la esperanza de que pueda proporcionar algunas referencias e ideas para estudiantes que tienen el mismo atractivo.

Siga "Amap Technology" para obtener más información

Lectura recomendada

Supongo que te gusta

Origin blog.csdn.net/amap_tech/article/details/132572876
Recomendado
Clasificación