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

prefacio

La cobertura de código (cobertura de código) es un método de medición en las pruebas de software, que se utiliza para reflejar la proporción y el grado del código que se prueba.

En el proceso de iteración del software, además de prestar atención a la tasa de cobertura del código durante el proceso de prueba, la tasa de cobertura del código durante el uso del usuario también es un indicador muy valioso que no se puede ignorar. Porque junto con la expansión del negocio y las actualizaciones de funciones se genera una gran cantidad de códigos obsoletos y obsoletos que rara vez se utilizan, o incluso ya no se utilizan, o están "en mal estado". La falta de mantenimiento no sólo afecta al tamaño del paquete de la aplicación. , pero también puede contribuir a estabilizar el riesgo. 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 la frecuencia de uso y contacto de cada categoría en línea, cargarla en la nube, procesarla en la plataforma y brindar capacidades de visualización de consultas y informes .

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, a fin de proporcionar una base para la toma de decisiones para el código antiguo fuera de línea, la 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 y 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 ajustar dinámicamente las estrategias de programación y asignación de brocas, hilos y otros recursos de diamante en la aplicación de acuerdo con 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 atractivo es recopilar código de granularidad de clase bajo la premisa de no afectar el uso del usuario y la aplicación. operación tanto como sea posible. Utilice cobertura. El esquema de recolección utilizado debe ser menos complicado, simple de implementar, teniendo en cuenta la estabilidad y el rendimiento, y no invadir el proceso de empaquetado ni afectar el volumen del paquete. Después de una exploración en profundidad, hemos desarrollado un conjunto de soluciones que cumplen perfectamente estos requisitos Solución completamente nueva.

Comparación de esquemas

La siguiente tabla muestra la comparación de varios indicadores entre el régimen común y el régimen de desarrollo propio: Verde significa mejor.

De la tabla se puede observar que:

esquema jacoco

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

Esquema Hook PathClassLoader

La implementación es simple, no intruye el código fuente y es compatible con todas las versiones de Android, pero Hook PathClassLoader no solo afecta el rendimiento, sino que incluso puede afectar la estabilidad de la aplicación.

Hackear el acceso a la solución ClassTable

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 la implementación es más complicada.

Programa de desarrollo propio

  • Excelente rendimiento, admite recopilación bajo demanda sin comprometer el rendimiento de la aplicación
  • Implementación simple, sin "tecnología negra", excelente estabilidad y compatibilidad
  • Admite recopilación de complementos y procesos cruzados

En comparación, sabemos que la solución de desarrollo propio puede satisfacer mejor nuestras demandas de recopilación de cobertura de código en línea, porque no solo tiene buena estabilidad, sino que también tiene un rendimiento excelente y apenas tiene 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 la cobertura del código con granularidad de clase, en realidad es necesario saber qué clases se cargan y utilizan durante la ejecución de la aplicación. En una aplicación Java, esto se puede consultar directamente llamando al método findLoadedClass de ClassLoader, pero no es tan simple en una aplicación de Android. La razón es que el sistema Android ha realizado la siguiente optimización:

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

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 ¿Se solucionará el problema? En esta línea de pensamiento, propusimos de manera innovadora una solución para copiar el puntero de ClassTable y acceder indirectamente al estado de carga de clases a través de API estándar .

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

  • La velocidad de adquisición es más de 5 veces mayor que la de la solución común y el rendimiento es excelente.
  • Utilice API estándar para acceder a ClassTable, excelente compatibilidad y estabilidad
  • Sólo se utiliza un reflejo, sin ninguna "tecnología negra", simple y estable.
  • No afecta la carga de clases ni la ejecución de la aplicación.
  • Soporte perfecto para multiproceso y colección de complementos

Pero hay una cosa a tener en cuenta:

El campo ClassTable se introduce desde Android N, por lo que este método solo es aplicable a Android N y superiores. Por razones de necesidad y retorno de la inversión, no nos adaptamos 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. El proceso clave es el siguiente:

Se puede ver que todo el proceso de adquisición del lado del terminal es en serie, lo cual es muy conveniente para el control del proceso y la integración de datos. Expliquemos la idea de diseño:

  • Durante la recopilación, la aplicación se divide en dos partes: una parte son los datos de la clase de host utilizados por el proceso principal y los subprocesos, y la otra parte son los datos de la clase de complemento.
  • Según la recopilación de consultas, el proceso principal, los subprocesos y los complementos proporcionan respectivamente interfaces para consultar el estado de carga de la clase.
  • El proceso se basa en el método serial, controlado por el proceso principal, que a su vez llama a la interfaz correspondiente 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 Cuanto más recopile, más clases necesitará consultar.
  • El proceso principal y los subprocesos consultan a su vez, y las consultas son todas las clases descargadas que quedan después de la consulta anterior como entrada, por lo que la cantidad de consultas requeridas para los subprocesos posteriores es menor y la consulta del mismo complemento La instancia en diferentes procesos es similar a esta.

Como se muestra abajo:

  • Al final de la recopilación, se generará una copia de los datos del host y N copias de los datos del 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.

Cabe resaltar que:

  • Las clases utilizadas por el proceso principal y los subprocesos pertenecen al host y los resultados de la recopilación deben combinarse en un solo dato; de manera similar, no importa en cuántos procesos se cargue un complemento, solo un dato del complemento debería 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, es más significativo fusionarlos y mostrarlos.

gestión de versiones

La mayoría de los códigos de aplicaciones de Android están ofuscados y los nombres de clases ofuscados varían según las versiones, lo que requiere la gestión de 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 las colecciones posteriores de esta versión no consultarán repetidamente su condición de uso.

Cuando cada versión se recopila por primera vez, debe usar el conjunto completo de nombres de clases de aplicaciones como entrada, y 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, utilizando 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 al mismo tiempo y, según nuestra prueba, cada 10.000 clases ocupan aproximadamente 0,8 MB de memoria, para aplicaciones grandes con decenas de miles de clases. En todo momento dicho esto, habría una sobrecarga de memoria no trivial. Por tanto, también se puede considerar la segunda vía.

2. Descarga en la nube

Obtenga datos de nombres de clases de la plataforma de construcción, cárguelos en la plataforma en la nube y descargue y use la aplicación cuando sea necesario.

En cuanto a qué método elegir, simplemente elija según el número de clases. Cuando hay muchas clases, como escenarios de aplicaciones a gran escala, se recomienda utilizar el método de la nube; las aplicaciones o complementos normales se pueden obtener 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 proceso secundario proporcione una interfaz de consulta que admita llamadas entre procesos. Elegimos una solución AIDL simple, confiable y fácil de reutilizar para lograr esto.

El método específico es:

Defina la interfaz de consulta a través de AIDL, defina la Acción correspondiente y devuelva la clase de implementación Binder de la interfaz de consulta de acuerdo con la Acción en el método onBind del Servicio 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: use el archivo como soporte de datos, escriba las clases cargadas y descargadas en el archivo y pase la ruta del archivo entre las interfaces. Las operaciones de archivos también pueden utilizar BufferedReader y BufferedWriter para mejorar el rendimiento.

El proceso de llamada se muestra en la figura:

Los beneficios de hacerlo también son obvios:

  • Solo se requiere una llamada entre procesos para recopilar un proceso y el costo es extremadamente bajo
  • Evite la sobrecarga de memoria de la serialización de datos
  • Evite el problema de que grandes datos no se puedan transferir directamente entre procesos.
  • El proceso de recolección es más simple y el proceso requerido se puede recolectar a pedido.
  • Facilite el filtrado de datos, evite consultas repetidas de clases cargadas y mejore el rendimiento de la recopilación

Colección de complementos

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

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

Para los complementos utilizados en el proceso secundario, solo hay más llamadas a la interfaz entre procesos y la operación de devolver la clase cargada y la clase restante al proceso principal para su procesamiento.

Los pasos de recolección son los siguientes:

  • Al consultar clases de subproceso, las clases de complemento que se ejecutan en el proceso se consultarán al mismo tiempo y los datos se escribirán en archivos divididos por nombres de complemento.
  • La colección de complementos en el proceso principal es el último enlace de todo el proceso, en este momento se detectarán, fusionarán y procesarán los archivos de datos correspondientes a cada complemento (generados por subprocesos), y finalmente el Los archivos de datos se eliminarán.
  • Finalmente, se procesan los archivos de datos restantes del complemento. Esta parte del archivo pertenece al complemento que solo se ejecuta en el proceso secundario.

En este punto, se obtienen los datos de carga de clases de todos los complementos.

Mapeo de soluciones

Al ver los datos de cobertura del código, esperamos ver los nombres de las clases originales, por lo que desasignar es el único camino a seguir.

La operación de desasignación se puede realizar en el lado del cliente 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 y cada paquete de instalación corresponde a una copia. Nuestro enfoque es generar un archivo de mapeo entre la clase ofuscada y la clase de texto sin formato a través de scripts al crear el paquete oficial en la plataforma. 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, descifra el nombre de la clase original y lo compara con el módulo a asociar.

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

Almacenamiento de datos y computación 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 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, que se declara como clave principal y no acepta valores nulos ni duplicados.
  • Antes de cada recopilación, se obtiene el número de filas que contiene. Durante el proceso de recopilación, los datos del nombre de la clase cargada se actualizan en la tabla, para que la base de datos pueda completar automáticamente la deduplicación. Una vez completada la recopilación, obtenga nuevamente el número de filas de datos y reste el desplazamiento del número de filas antes de que la recopilación sea la parte incremental. Solo necesitamos cargar esta parte de datos al servicio.

rendimiento y estabilidad

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

Al mismo tiempo, también ha sido verificado por múltiples versiones del mapa de Gaode en línea y no se han encontrado fallas relacionadas ni ANR.

otro

Omitir la lista negra y gris

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

Momento y frecuencia de recolección

Aunque el proceso de recopilación es breve e insensible, para minimizar el impacto en la ejecución de la aplicación, colocamos el trabajo de recopilación en el subproceso y elegimos iniciar la ejecución después de que la aplicación salga 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 necesitamos 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 los datos de frecuencia de uso de cada clase, también puede obtener estadísticas agregadas en el lado del servidor.

escribe al final

Como método de medición, la cobertura del código no solo puede proporcionarnos una base para desconectar códigos antiguos, sino que también refleja la popularidad de una determinada función, que puede proporcionar una base para la asignación de recursos, decisiones de programación, etc., que es una parte indispensable. del desarrollo de software, falta una herramienta importante.

Nuestra nueva solución es concisa pero no simple. Realiza inteligentemente una recopilación libre de hacks. Con la premisa de garantizar una alta estabilidad y no intrusión en el código fuente, realiza elegantemente una recopilación de alto rendimiento de la cobertura del código del entorno de producción, que ya es demasiado alta. La verificación de múltiples versiones de mapas alemanes es una solución madura, estable y eficiente. Lo comparto aquí, con la esperanza de brindar algunas referencias e ideas para estudiantes que tienen el mismo atractivo.

notas de estudio de Android

Optimización del rendimiento de Android: https://qr18.cn/FVlo89
Android Vehículo: https://qr18.cn/F05ZCM
Android Notas de estudio de seguridad inversa: https://qr18.cn/CQ5TcL
Principios del marco de trabajo de Android: https://qr18.cn/AQpN4J
Android Audio y video: https://qr18.cn/Ei3VPD
Jetpack (incluido Compose): https://qr18.cn/A0gajp
Kotlin: https://qr18.cn/CdjtAF
Gradle: https://qr18.cn/DzrmMB
OkHttp Notas de análisis del código fuente: https://qr18.cn/Cw0pBD
Flutter: https://qr18.cn/DIvKma
Android Eight Knowledge Body: https://qr18.cn/CyxarU
Android Core Notes: https://qr21.cn/CaZQLo
Android Preguntas de entrevistas anteriores: https://qr18.cn/CKV8OZ
Última colección de preguntas de entrevistas de Android de 2023: https://qr18.cn/CgxrRy
Ejercicios de entrevistas de trabajo de desarrollo de vehículos Android: https://qr18.cn/FTlyCJ
Preguntas de entrevistas de audio y video:https://qr18.cn/AcV6Ap

Supongo que te gusta

Origin blog.csdn.net/weixin_61845324/article/details/132691460
Recomendado
Clasificación