Code-Transformation in Android 2. AST-Analyse



In diesem Artikel werde ich darüber sprechen, wie ich die Probleme gelöst habe, auf die ich im vorherigen Teil während der Implementierung des Projekts gestoßen bin.


Erstens müssen Sie bei der Analyse einer transformierbaren Klasse irgendwie verstehen, ob diese Klasse der Nachfolger von Activity oder Fragment , damit wir sicher sagen können, dass die Klasse für unsere Transformation geeignet ist.


Zweitens müssen Sie in der transformierten .class Datei für alle Felder mit der Annotation @State den Typ explizit bestimmen, um die entsprechende Methode im Bundle aufzurufen, um den Status zu speichern / wiederherzustellen, und Sie können den Typ genau bestimmen, indem Sie alle @State der Klasse und die von ihnen implementierten Schnittstellen analysieren.


Sie müssen also nur in der Lage sein, den abstrakten Syntaxbaum der transformierten Dateien zu analysieren.


AST-Analyse


Um die Klasse auf Vererbung von einer Basisklasse zu analysieren (in unserem Fall ist es Activity/Fragment ), reicht es aus, den vollständigen Pfad zur untersuchten .class Datei zu haben. Darüber hinaus hängt alles von der Implementierung des Transformators ab: Laden Sie die Klasse entweder über ClassLoader oder analysieren Sie sie über ASM mit ClassReader und ClassVisitor , um alle erforderlichen Informationen über die Klasse zu erhalten.


Dateizugriff


Beachten Sie, dass sich die benötigte Klasse außerhalb des Projektbereichs befinden kann, jedoch in einigen Bibliotheken (z. B. Activity befindet sich im Android SDK). Daher müssen Sie vor dem Start der Umwandlung eine Liste der Pfade zu allen verfügbaren .class Dateien .class .


Nehmen Sie dazu kleine Änderungen am Transformator vor :


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

Mit der Methode getReferencedScopes können Sie auf Dateien aus den angegebenen Bereichen zugreifen. Dies ist einfach ein Lesezugriff ohne die Möglichkeit einer Transformation. Genau das, was wir brauchen. Bei der transform können diese Dateien auf die gleiche Weise wie in den Hauptbereichen abgerufen werden:


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

Und noch etwas: Dateien aus dem Andoid SDK müssen separat empfangen werden:


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

Danke Google, sehr praktisch.


ClassPool Fill


Das Ausfüllen der Liste aller uns zur Verfügung stehenden .class Dateien mit Ihren Händen ist ziemlich trostlos: Da wir Verzeichnisse oder jar Dateien als Eingabe erhalten, müssen Sie alle .class und die .class Dateien korrekt .class . Hier habe ich die zuvor erwähnte Javassist- Bibliothek benutzt. Sie macht alles unter der Haube und hat außerdem eine praktische API für die Arbeit mit den erhaltenen Klassen. Am Ende müssen Sie nur den Pfad zu den Dateien übertragen und den ClassPool :


 ClassPool.getDefault().appendClassPath("  ") 

Vor dem Start der Umwandlung wird ClassPool aus allen möglichen ClassPool gefüllt:


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

Details im Transformator .


Klassenanalyse


ClassPool der ClassPool voll ist, muss die Annotation @Stater . Entfernen Sie dazu das Häkchen in der visitAnnotation Methode unseres Besuchers und untersuchen Sie einfach die Oberklasse jeder Klasse auf das Vorhandensein von Activity/Fragment in der Vererbungshierarchie. Es ist sehr einfach, eine Klasse mit Namen aus der Javassist-Pool-Klasse abzurufen:


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

Und bereits mit CtClass können Sie currentClass.superclass oder currentClass.interfaces . Durch Vergleich der Oberklasse habe ich eine Aktivitäts- / Fragmentprüfung durchgeführt.


Und schließlich habe ich ungefähr das Gleiche getan, um StateType loszuwerden und nicht den Typ des Feldes anzugeben, das explizit StateType . Der Einfachheit halber wurde ein Mapper (mit Tests ) geschrieben, der den aktuellen Deskriptor in den vom Bundle unterstützten Typ analysiert.


Infolgedessen hat sich die Codetransformation nicht geändert, sondern nur der Mechanismus zum Bestimmen des Typs einer Variablen hat sich geändert.


Durch die Kombination von zwei Ansätzen für die Arbeit mit .class Dateien konnte ich die ursprüngliche Idee des Speicherns von Variablen in einem Bundle mit nur einer Anmerkung umsetzen.


Leistung


Um die Leistung zu testen, habe ich dieses Mal das Plug-In mit einem echten Arbeitsprojekt verbunden, da das Füllen der Poolklasse von der Anzahl der Dateien im Projekt und verschiedenen Bibliotheken abhängt.
./gradlew clean build --scan dies alles durch ./gradlew clean build --scan . Die Transformationsaufgabe transformClassesWithStaterTransformForDebug dauert ungefähr 2,5 s. Ich habe mit einer Activity mit 50 @State Feldern gemessen und mit 10 solchen Activity ändert sich die Geschwindigkeit nicht viel.

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


All Articles