Transformación de código en Android 2. Análisis AST



En este artículo hablaré sobre cómo resolví los problemas que encontré en la parte anterior durante la implementación del proyecto .


En primer lugar, al analizar una clase transformable, debe comprender de alguna manera si esta clase es la sucesora de Activity o Fragment , para que podamos decir con confianza que la clase es adecuada para nuestra transformación.


En segundo lugar, en el archivo .class transformado para todos los campos con la anotación @State , debe determinar explícitamente el tipo para llamar al método correspondiente en el paquete para guardar / restaurar el estado, y puede determinar el tipo exactamente analizando todos los padres de la clase y las interfaces que implementan.


Por lo tanto, solo necesita poder analizar el árbol de sintaxis abstracta de los archivos transformados.


Análisis AST


Para analizar la clase de herencia de alguna clase base (en nuestro caso es Activity/Fragment ), es suficiente tener la ruta completa al archivo .class en estudio. Además, todo depende de la implementación del transformador: cargue la clase a través de ClassLoader o analice a través de ASM usando ClassReader y ClassVisitor , obteniendo toda la información necesaria sobre la clase.


Acceso a archivos


Tenga en cuenta que la clase que necesitamos puede ubicarse fuera del alcance del proyecto, pero en alguna biblioteca (por ejemplo, Activity está en el SDK de Android). Por lo tanto, antes de comenzar la transformación, debe obtener una lista de rutas a todos los archivos .class disponibles.


Para hacer esto, realice pequeños cambios en el Transformador :


 @Override Set<? super QualifiedContent.Scope> getReferencedScopes() { return ImmutableSet.of( QualifiedContent.Scope.EXTERNAL_LIBRARIES, QualifiedContent.Scope.SUB_PROJECTS ) } 

El método getReferencedScopes permite acceder a archivos desde los ámbitos especificados, y esto será simplemente acceso de lectura sin la posibilidad de transformación. Justo lo que necesitamos. En el método de transform , estos archivos se pueden obtener de la misma manera que desde los ámbitos principales:


 transformInvocation.referencedInputs.each { transformInput -> transformInput.directoryInputs.each { directoryInput -> // .  directoryInput.file.absolutePath } transformInput.jarInputs.each { jarInput -> // .  jarInput.file.absolutePath } } 

Y una cosa más, los archivos del SDK de Andoid deben recibirse por separado:


 project.extensions.findByType(BaseExtension.class).bootClasspath[0].toString() 

Gracias Google, muy conveniente.


ClassPool Fill


Completar la lista de todos los archivos .class disponibles para nosotros con sus manos es bastante triste: dado que obtenemos directorios o archivos jar como entrada, debe revisarlos todos y obtener los archivos .class correctamente. Aquí utilicé la biblioteca javassist mencionada anteriormente. Ella lo hace todo bajo el capó y además tiene una API conveniente para trabajar con las clases recibidas. Al final, solo necesita transferir la ruta a los archivos y completar el ClassPool :


 ClassPool.getDefault().appendClassPath("  ") 

Antes de comenzar la transformación, ClassPool se llena de todas las fuentes de archivos posibles:


 fillPoolAndroidInputs(classPool) fillPoolReferencedInputs(transformInvocation, classPool) fillPoolInputs(transformInvocation, classPool) 

Detalles en el transformador .


Análisis de clase


Ahora que el ClassPool lleno, queda por deshacerse de la anotación @Stater . Para hacer esto, elimine la visitAnnotation en el método visitAnnotation de nuestro visitante y simplemente examine la superclase de cada clase para visitAnnotation la presencia de Activity/Fragment en la jerarquía de herencia. Obtener cualquier clase por nombre de la clase de grupo javassist es muy simple:


 CtClass currentClass = ClassPool.getDefault().get(className.replace("/", ".")) 

Y ya con CtClass puede obtener currentClass.superclass o currentClass.interfaces . A través de la comparación de la superclase, hice una verificación de actividad / fragmento.


Y finalmente, para deshacerme de StateType y no especificar el tipo de campo para guardar explícitamente, hice lo mismo. Por conveniencia, se escribió un mapeador (con pruebas ) que analiza el descriptor actual en el tipo admitido por el paquete.


Como resultado, la transformación del código no ha cambiado; solo ha cambiado el mecanismo para determinar el tipo de variable.


Entonces, combinando 2 enfoques para trabajar con archivos .class , pude implementar la idea original de guardar variables en paquetes usando solo una anotación.


Rendimiento


Esta vez, para probar el rendimiento, conecté el complemento a un proyecto de trabajo real, ya que el llenado de la clase de grupo depende de la cantidad de archivos en el proyecto y varias bibliotecas.
./gradlew clean build --scan todo esto a través de ./gradlew clean build --scan . La transformClassesWithStaterTransformForDebug toma aproximadamente 2.5 s. @State con una Activity con 50 campos @State y con 10 de estas Activity , la velocidad no cambia mucho.

Source: https://habr.com/ru/post/470209/


All Articles