Трансформация кода в Android 2. Анализ AST
В этой статье расскажу о том, как решал проблемы, с которыми столкнулся в предыдущей части при реализации проекта.
Во-первых, при анализе трансформируемого класса, нужно как-то понять, является ли этот класс наследником Activity
или Fragment
, чтобы с уверенностью сказать, что класс подходит для нашей трансформации.
Во-вторых, в трансформируемом .class
файле для всех полей с аннотацией @State
нужно явно определить тип, чтобы вызвать соответствующий метод у бандла для сохранения/восстановления состояния, а точно определить тип можно лишь проанализировав всех родителей класса и реализуемые ими интерфейсы.
Таким образом, нужно просто иметь возможность анализировать абстрактно синтаксическое дерево трансформируемых файлов.
Анализ AST
Для того, чтобы проанализировать класс на предмет наследования от какого-нибудь базового класса (в нашем случае это Activity/Fragment
), достаточно иметь полный путь к исследуемому .class
файлу. Далее все зависит от реализации трансформатора: либо загружать класс через ClassLoader
, либо анализировать через ASM, используя ClassReader
и ClassVisitor
, доставая всю необходимую информацию о классе.
Доступ к файлам
Нужно учитывать, что необходимый нам класс может находиться вне скоупа проекта, а в какой-нибудь библиотеке (например, Activity
находится в Android SDK). Поэтому перед началом трансформации необходимо получить список путей ко всем доступным .class
файлам.
Для этого внесем небольшие изменения в Трансформатор:
@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. Она делает это все под капотом и плюс имеет удобное апи для работы с полученными классами. В итоге нужно лишь передать путь к файлам и заполнить 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
, скорость особо не меняется.