
В этой статье расскажу о том, как решал проблемы, с которыми столкнулся в предыдущей части при реализации проекта.
Во-первых, при анализе трансформируемого класса, нужно как-то понять, является ли этот класс наследником 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, скорость особо не меняется.