
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 ->
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.