- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

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

Анализ AST

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

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

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

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

@Override
Set<? super QualifiedContent.Scope> 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 [4]. Она делает это все под капотом и плюс имеет удобное апи для работы с полученными классами. В итоге нужно лишь передать путь к файлам и заполнить ClassPool:

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

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

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

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

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

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

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

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

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

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

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

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

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

Автор: panman

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/332206

Ссылки в тексте:

[1] Первая часть: https://habr.com/ru/post/469237/

[2] Пример на Github: https://github.com/AlexeyPanchenko/stater

[3] Трансформатор: https://github.com/AlexeyPanchenko/stater/blob/master/stater/src/main/groovy/ru/alexpanchenko/stater/plugin/StaterTransform.groovy

[4] javassist: https://www.javassist.org/

[5] трансформаторе: https://github.com/AlexeyPanchenko/stater/blob/master/stater/src/main/groovy/ru/alexpanchenko/stater/plugin/StaterTransform.groovy#L60

[6] маппер: https://github.com/AlexeyPanchenko/stater/blob/master/stater/src/main/groovy/ru/alexpanchenko/stater/plugin/utils/StateTypeDeterminator.groovy

[7] тестами: https://github.com/AlexeyPanchenko/stater/blob/master/stater/src/test/java/ru/alexpanchenko/stater/plugin/utils/StateTypeDeterminatorTest.java

[8] Источник: https://habr.com/ru/post/470209/?utm_source=habrahabr&utm_medium=rss&utm_campaign=470209