Трансформация кода в Android 2. Анализ AST

bhmsf6jsnzyimbf_0-g6plwe57m.png

В этой статье расскажу о том, как решал проблемы, с которыми столкнулся в предыдущей части при реализации проекта.

Во-первых, при анализе трансформируемого класса, нужно как-то понять, является ли этот класс наследником Activity или Fragment, чтобы с уверенностью сказать, что класс подходит для нашей трансформации.

Во-вторых, в трансформируемом .class файле для всех полей с аннотацией @State нужно явно определить тип, чтобы вызвать соответствующий метод у бандла для сохранения/восстановления состояния, а точно определить тип можно лишь проанализировав всех родителей класса и реализуемые ими интерфейсы.

Таким образом, нужно просто иметь возможность анализировать абстрактно синтаксическое дерево трансформируемых файлов.


Анализ AST

Для того, чтобы проанализировать класс на предмет наследования от какого-нибудь базового класса (в нашем случае это Activity/Fragment), достаточно иметь полный путь к исследуемому .class файлу. Далее все зависит от реализации трансформатора: либо загружать класс через ClassLoader, либо анализировать через ASM, используя ClassReader и ClassVisitor, доставая всю необходимую информацию о классе.


Доступ к файлам

Нужно учитывать, что необходимый нам класс может находиться вне скоупа проекта, а в какой-нибудь библиотеке (например, Activity находится в Android SDK). Поэтому перед началом трансформации необходимо получить список путей ко всем доступным .class файлам.

Для этого внесем небольшие изменения в Трансформатор:

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

Метод getReferencedScopes позволяет получить доступ к файлам из указанных скоупов, причем это будет просто доступ на чтение без возможности трансформации. Как раз то, что нам нужно. В методе transform эти файлы можно получить почти также, как из основных скоупов:

transformInvocation.referencedInputs.each { transformInput ->
  transformInput.directoryInputs.each { directoryInput ->
    // доп. директории directoryInput.file.absolutePath
  }
  transformInput.jarInputs.each { jarInput ->
    // доп. джарники jarInput.file.absolutePath
  }
}

И еще одно, файлы из Andoid SDK нужно получать отдельно:

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

Спасибо Google, очень удобно.


Заполнение ClassPool

Заполнять список всех доступных нам .class файлов руками довольно муторно: так как на вход мы получаем директории или jar файлы, надо обойти их все и правильно достать именно .class файлы. Здесь я воспользовался ранее упомянутой библиотекой javassist. Она делает это все под капотом и плюс имеет удобное апи для работы с полученными классами. В итоге нужно лишь передать путь к файлам и заполнить ClassPool:

ClassPool.getDefault().appendClassPath("путь к файлам")

Перед началом трансформации происходит заполнение ClassPool из всех возможных источников файлов:

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

Подробности в трансформаторе.


Анализ классов

Теперь, когда ClassPool заполнен, осталось избавиться от аннотации @Stater. Для этого убираем проверку в методе visitAnnotation нашего визитора и просто исследуем суперкласс каждого класса на наличие Activity/Fragment в иерархии наследования. Получить любой класс по имени из класс пула javassist очень просто:

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

И уже у CtClass можно получить currentClass.superclass или currentClass.interfaces. Через сравнение суперкласса я и сделал проверку на активити/фрагмент.

Ну и наконец, чтобы избавиться от StateType и не указывать тип сохраняемого поля явно, я делал примерно то же самое. Для удобства был написан маппер (с тестами), который парсит текущий дескриптор в тип, поддерживаемый бандлом.

Трансформация кода в итоге не изменилась, поменялся лишь механизм определения типа переменной.

Так, совместив 2 подхода к работе с .class файлами, мне удалось реализовать изначальную идею по сохранению переменных в бандл, используя всего одну аннотацию.


Производительность

На этот раз для проверки производительности, подключил плагин к реальному рабочему проекту, так как заполнение класс пула зависит от количества файлов в проекте и различных библиотеках.
Проверял все это через ./gradlew clean build --scan. Таска трансформации transformClassesWithStaterTransformForDebug занимает примерно 2,5 с. Производил замер с одной Activity с 50 @State полями и с 10 такими Activity, скорость особо не меняется.

© Habrahabr.ru