- PVSM.RU - https://www.pvsm.ru -
В этой статье расскажу о том, как решал проблемы, с которыми столкнулся в предыдущей части [1] при реализации проекта [2].
Во-первых, при анализе трансформируемого класса, нужно как-то понять, является ли этот класс наследником Activity
или Fragment
, чтобы с уверенностью сказать, что класс подходит для нашей трансформации.
Во-вторых, в трансформируемом .class
файле для всех полей с аннотацией @State
нужно явно определить тип, чтобы вызвать соответствующий метод у бандла для сохранения/восстановления состояния, а точно определить тип можно лишь проанализировав всех родителей класса и реализуемые ими интерфейсы.
Таким образом, нужно просто иметь возможность анализировать абстрактно синтаксическое дерево трансформируемых файлов.
Для того, чтобы проанализировать класс на предмет наследования от какого-нибудь базового класса (в нашем случае это 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, очень удобно.
Заполнять список всех доступных нам .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
Нажмите здесь для печати.