Code Transformation in Android 2. AST Analysis



In this article I will talk about how I solved the problems that I encountered in the previous part during the implementation of the project .


Firstly, when analyzing a transformable class, you need to somehow understand whether this class is the successor of the Activity or Fragment , in order to say with confidence that the class is suitable for our transformation.


Secondly, in the transformed .class file for all fields with the @State annotation, @State need to explicitly determine the type in order to call the corresponding method on the bundle for saving / restoring the state, and you can determine the type exactly by analyzing all the parents of the class and the interfaces they implement.


Thus, you just need to be able to analyze the abstract syntax tree of the transformed files.


AST analysis


In order to analyze the class for inheritance from some base class (in our case, it is Activity/Fragment ), it is enough to have the full path to the .class file under study. Further, it all depends on the implementation of the transformer: either load the class through ClassLoader , or analyze through ASM using ClassReader and ClassVisitor , getting all the necessary information about the class.


File access


Keep in mind that the class we need can be located outside the project scope, and in some library (for example, Activity is in the Android SDK). Therefore, before starting the transformation, you need to get a list of paths to all available .class files.


To do this, make small changes to the Transformer :


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

The getReferencedScopes method allows you to access files from the specified scopes, and this will simply be read access without the possibility of transformation. Just what we need. In the transform method, these files can be obtained in much the same way as from the main scopes:


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

And one more thing, files from the Andoid SDK need to be received separately:


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

Thanks Google, very convenient.


ClassPool Fill


Filling the list of all .class files available to us with your hands is rather dreary: since we get directories or jar files as an input, you need to go around all of them and get the .class files correctly. Here, I used the previously mentioned javassist library. She does it all under the hood and plus has a convenient api for working with the classes received. In the end, you just need to transfer the path to the files and fill in ClassPool :


 ClassPool.getDefault().appendClassPath("  ") 

Before starting the transformation, ClassPool from all possible file sources:


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

Details in the transformer .


Class Analysis


Now that the ClassPool full, it remains to get rid of the @Stater annotation. To do this, remove the check in the visitAnnotation method of our visitor and simply examine the superclass of each class for the presence of Activity/Fragment in the inheritance hierarchy. Getting any class by name from the javassist pool class is very simple:


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

And already with CtClass you can get currentClass.superclass or currentClass.interfaces . Through comparison of the superclass, I did an activity / fragment check.


And finally, to get rid of StateType and not specify the type of field to save explicitly, I did about the same. For convenience, a mapper (with tests ) was written that parses the current descriptor into the type supported by the bundle.


As a result, the code transformation has not changed; only the mechanism for determining the type of a variable has changed.


So, combining 2 approaches to working with .class files, I managed to implement the original idea of ​​saving variables in a bundle using just one annotation.


Performance


This time, to test the performance, I connected the plug-in to a real working project, since the filling of the pool class depends on the number of files in the project and various libraries.
Checked all this through ./gradlew clean build --scan . The transformation transformClassesWithStaterTransformForDebug takes approximately 2.5 s. I measured with one Activity with 50 @State fields and with 10 such Activity , the speed does not change much.



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


All Articles