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.








All Articles