Transformação de código no Android 2. Análise AST



Neste artigo, falarei sobre como resolvi os problemas que encontrei na parte anterior durante a implementação do projeto .


Em primeiro lugar, ao analisar uma classe transformável, você precisa entender de alguma forma se essa classe é a sucessora de Activity ou Fragment , para que possamos dizer com segurança que a classe é adequada para a nossa transformação.


Em segundo lugar, no arquivo .class transformado para todos os campos com a anotação @State , @State precisa determinar explicitamente o tipo para chamar o método correspondente no pacote configurável para salvar / restaurar o estado e pode determinar o tipo exatamente analisando todos os pais da classe e as interfaces que eles implementam.


Portanto, você só precisa analisar a árvore de sintaxe abstrata dos arquivos transformados.


Análise AST


Para analisar a classe quanto à herança de alguma classe base (no nosso caso, é Activity/Fragment ), basta ter o caminho completo para o arquivo .class em estudo. Além disso, tudo depende da implementação do transformador: carregue a classe através do ClassLoader ou analise através do ASM usando ClassReader e ClassVisitor , obtendo todas as informações necessárias sobre a classe.


Acesso a arquivos


Lembre-se de que a classe de que precisamos pode estar localizada fora do escopo do projeto, mas em algumas bibliotecas (por exemplo, Activity está no Android SDK). Portanto, antes de iniciar a transformação, você precisa obter uma lista de caminhos para todos .class arquivos .class disponíveis.


Para fazer isso, faça pequenas alterações no Transformer :


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

O método getReferencedScopes permite acessar arquivos dos escopos especificados, e isso será simplesmente acesso de leitura sem a possibilidade de transformação. Apenas o que precisamos. No método de transform , esses arquivos podem ser obtidos da mesma maneira que nos escopos principais:


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

E mais uma coisa, os arquivos do Andoid SDK precisam ser recebidos separadamente:


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

Obrigado Google, muito conveniente.


Preenchimento de ClassPool


Preencher a lista de todos .class arquivos .class disponíveis para nós com as mãos é bastante sombrio: como obtemos diretórios ou arquivos jar como uma entrada, você precisa contorná-los e obter corretamente exatamente .class arquivos .class . Aqui eu usei a biblioteca javassist mencionada anteriormente. Ela faz tudo sob o capô e tem uma API conveniente para trabalhar com as aulas recebidas. No final, você só precisa transferir o caminho para os arquivos e preencher o ClassPool :


 ClassPool.getDefault().appendClassPath("  ") 

Antes de iniciar a transformação, o ClassPool é preenchido de todas as fontes de arquivos possíveis:


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

Detalhes no transformador .


Análise de classe


Agora que o ClassPool cheio, resta eliminar a anotação @Stater . Para fazer isso, remova a verificação no método visitAnnotation do nosso visitante e simplesmente examine a superclasse de cada classe quanto à presença de Activity/Fragment na hierarquia de herança. Obter qualquer classe por nome da classe javassist pool é muito simples:


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

E já com o CtClass você pode obter currentClass.superclass ou currentClass.interfaces . Através da comparação da superclasse, fiz uma verificação de atividade / fragmento.


E, finalmente, para me livrar do StateType e não especificar o tipo de campo a ser salvo explicitamente, fiz o mesmo. Por conveniência, foi escrito um mapeador (com testes ) que analisa o descritor atual no tipo suportado pelo pacote configurável.


Como resultado, a transformação do código não foi alterada; apenas o mecanismo para determinar o tipo de uma variável foi alterado.


Assim, combinando duas abordagens para trabalhar com arquivos .class , pude implementar a ideia original de salvar variáveis ​​em pacotes configuráveis ​​usando apenas uma anotação.


Desempenho


Dessa vez, para testar o desempenho, conectei o plug-in a um projeto de trabalho real, pois o preenchimento da classe de pool depende do número de arquivos no projeto e de várias bibliotecas.
./gradlew clean build --scan tudo isso através do ./gradlew clean build --scan . A transformClassesWithStaterTransformForDebug leva aproximadamente 2,5 s. Eu medi com uma Activity com 50 campos @State e com 10 dessas Activity , a velocidade não muda muito.

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


All Articles