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