Инженер по производительности Android
Инженер по связям с разработчиками
Приложения для Android часто недостаточно оптимизированы из-за слишком консервативных правил сохранения, которые обычно используются в сторонних библиотеках. Это приводит к увеличению размера APK-файлов, замедлению холодного запуска и снижению производительности во время работы.
Новые пороговые значения производительности Google Play для оптимизации DEX-кода означают, что видимость приложения теперь напрямую зависит от того, насколько хорошо оно оптимизировано.
В этом докладе рассказывается о современном рабочем процессе R8, который позволяет справиться с раздуванием правил. Вы научитесь:
Расшифровка
Здравствуйте!
Меня зовут Бриана, я работаю в команде Android DevRel.
А я Крис Крейк из команды Android Performance.
Представляем типобезопасные правила сохранения и оптимизацию DEX.
Сегодня мы поговорим о том, что такое оптимизация цифрового опыта, почему она важна и какие обновления мы недавно выпустили.
Долгое время оптимизация DEX считалась чем-то необязательным.
Возможно, вы настроите его один раз и больше не будете трогать, а может, уделите этому время, когда будете свободны.
Однако сейчас индустрия мобильных устройств сталкивается со значительными ограничениями в поставках оборудования, что влияет на доступность памяти. Поэтому хорошо оптимизированный DEX-файл необходим, чтобы ваше приложение использовало меньше памяти, быстрее запускалось, реже выдавало ошибки ANR и в целом работало эффективнее.
Чтобы поддерживать здоровую экосистему Android и помогать разработчикам создавать качественные приложения, Google Play объявил о новых требованиях к качеству приложений и пороговых значениях производительности. Они начнут действовать в феврале.
в том числе пороговые значения для оптимизации DEX-кода.
Для сжатия, оптимизации и обфускации приложений необходимо достичь показателя выше 25 %.
Что это значит для вас как разработчика Android?
Неоптимизированный DEX-код не только ухудшает удобство использования, но и может повлиять на видимость приложения в Google Play и возможность его публикации.
Однако вы обратились по адресу, и к концу этой презентации у вас будут инструменты, которые позволят не только выполнить, но и превзойти эти требования.
Оптимизировать DEX-код можно с помощью R8 – оптимизатора приложений Android.
Использовать R8 необязательно, если вы можете достичь нужных показателей с помощью другого оптимизатора. Однако мы рекомендуем именно R8, поскольку он встроен в тулчейн Android.
Хотя оптимизированные DEX-файлы и наборы App Bundle используют меньше памяти, основные преимущества все же связаны с производительностью: меньше ошибок ANR, более быстрый запуск и улучшенная производительность во время выполнения.
Оптимизация приложения приносит реальные, доказанные и положительные результаты как для вас, так и для пользователей.
Например, перейдя на полный режим R8 и уточнив правила сохранения, разработчики Grab смогли оптимизировать приложение на 78 %, уменьшить его размер на 79 % и обфусцировать на 82 %.
Вы можете посмотреть на этот слайд и подумать: "Хорошо, эти цифры довольно высоки, но что это на самом деле означает?"
В результате этих улучшений, а также использования профилей базовой конфигурации, количество ошибок ANR снизилось на 32 %, а время запуска сократилось на 35 %.
Когда ведущие приложения впервые используют R8, они видят среднее уменьшение размера DEX на 38 % и уменьшение количества ошибок ANR в активном режиме на 16 %.
А если вы уже используете R8, у нас для вас отличные новости.
Если разработчики приложений, в которых уже включен R8, предпринимают дополнительные шаги по оптимизации, сжатию и обфускации, чтобы повысить их уровень более чем на 50 %, то они видят в среднем дополнительное уменьшение размера DEX на 25 %, сокращение времени запуска на 4 % и дополнительное снижение количества ошибок ANR в активном режиме на 17 % (в дополнение к 16 %, которые мы показали на предыдущем слайде).
Сегодня мы расскажем, как добиться таких результатов.
Мы расскажем об инструментах, которые помогут оптимизировать код DEX и перейти от слишком общих и неправильно настроенных правил сохранения к конкретным и узким.
Прежде всего отметим, что R8 существует уже много лет.
Мы понимаем, что, хотя эта функция и позволяет добиться высокой эффективности, ее настройка требует значительных усилий.
Рефлексивные вызовы означают, что входные данные непредсказуемы и могут привести к сбоям во время выполнения.
Поэтому раньше разработчики приложений и библиотек часто добавляли слишком широкие правила сохранения, чтобы перестраховаться.
Мы знаем, что 41 % ведущих приложений не достигает уровня оптимизации в 25 %, определенного Google Play, но также знаем, что 13 % ведущих приложений уже в три раза превышают этот порог.
Причем такого результата добиваются приложения из самых разных отраслей и с разными путями пользователей.
Высоких показателей оптимизации можно достичь, если у вас есть подходящие инструменты и вы понимаете, как управлять правилами сохранения, которые вы создали сами или унаследовали из библиотек.
Прежде чем перейти к новым инструментам и улучшениям, которые помогут вам в этом, мы хотели бы выделить несколько других улучшений R8 за последний год, которые также немного упрощают эту оптимизацию.
Во-первых, новые шаблоны приложений включают область действия пакета, что позволяет обеспечить минимальный уровень оптимизации с самого начала.
Анализатор конфигурации R8, о котором мы поговорим далее, теперь поддерживается в более ранних версиях AGP.
и неофициально поддерживается для Bazel.
А в AGP 9.2 примитивы сопрограмм стали в два раза быстрее.
Благодаря улучшениям R8 в AGP 9.2 после обновления с версии 9.0.1 в Tinder количество ошибок ANR снизилось на 13 %.
В AGP 9.3 также обновлен R8 API: названия стали понятнее, а правила по умолчанию и удаление неиспользуемых ресурсов – проще. Кроме того, добавлена поддержка каталога с исходными файлами AGP для правил сохранения.
android {
buildTypes {
release {
optimization {
// no need to specify default
// rules and resource shrinking
enabled = true
}
}
}
...
}android {
buildTypes {
release {
// Enables code optimizations.
isMinifyEnabled = true
// Enables resource shrinking.
isShrinkResources = true
proguardFiles(
// Default rules.
getDefaultProguardFile(
"proguard-android-optimize.txt"
),
... // list out each file with keep rules
)
...
}
}
...
}Обновления, о которых мы сегодня расскажем, также улучшают работу разработчиков.
Сначала мы расскажем, как с помощью анализатора конфигурации R8 и навыка R8 выявить устаревшие правила и определить, как их улучшить.
Затем Крис расскажет, как перейти от широких правил сохранения с подстановочными знаками к новым точным, типобезопасным, автоматически генерируемым аннотациям.
Наконец, мы рассмотрим, как использовать новые инструменты проверки CLI, чтобы обеспечить агрессивную оптимизацию без сбоев.
Анализатор конфигурации R8 – идеальный инструмент для начала оптимизации DEX.
Если вы ещё не пользовались этим инструментом, вот как он выглядит.
Это HTML-отчет, в котором визуализируется информация о конфигурации R8.
Этот отчет относится к моему приложению, в котором есть библиотека Analytics Bloat.
Как вы понимаете, это не настоящее приложение и не настоящая библиотека, но оно выделено синим цветом вверху.
Анализатор конфигурации измеряет следующие показатели: сокращение – процент классов, методов и полей, сокращению которых не препятствуют правила сохранения.
Это значение рассчитывается после того, как R8 выполнит первый проход сжатия.
Также измеряется оптимизация – процент кода, который можно оптимизировать на уровне всей программы, например за счет встраивания методов и объединения классов.
и обфускации – процента участников, которых можно обфусцировать или переименовать в более короткие идентификаторы.
Также хочу отметить, что R8 Config Analyzer рассчитывает показатели до окончательной оптимизации и компиляции DEX-файлов, поэтому значения оптимизации, сжатия и обфускации в отчете не будут точно совпадать с данными в Play Console.
Это связано с тем, что Google Play оценивает конечный DEX-файл.
Причина этого несоответствия заключается в том, чтобы при разработке у вас был доступ к быстрой локальной обратной связи.
Однако оценки, которые вы видите в анализаторе конфигурации R8, по-прежнему очень полезны для проверки потенциала оптимизации DEX перед загрузкой в Google Play.
Отчет также включает таблицу "Анализ правил хранения", которая является основным представлением, которое вы видите при входе в отчет.
Вы можете нажать на любое правило и посмотреть точное количество классов, методов и полей, которые оно сохраняет. Это поможет вам понять, какие правила сохраняют больше, чем нужно.
В нем также есть поглощенные правила, которые показаны здесь крупным планом.
и правила, которые пересекаются с более общими правилами.
Они выделены, чтобы вы могли удалить более широкое и оставить более узкое.
и неиспользуемые правила, которые можно удалить без риска для стабильности приложения.
Анализатор конфигурации был разработан для решения нескольких проблем, начиная с накопления устаревших правил.
Раньше, когда нельзя было посмотреть, какие правила применяются к конкретному объявлению, удалять старые правила было рискованно.
Поэтому команды просто оставляли их в покое или добавляли более широкие подстановочные знаки.
Во-вторых, это помогает обнаружить слепые зоны в правилах библиотеки.
Поэтому, когда вы добавляете SDK, исторически было трудно сказать, является ли правило, связанное с этой библиотекой, слишком широким или оно вообще необходимо, скажем, через 10 или около того версий.
О библиотеках мы тоже поговорим чуть позже.
В-третьих, вы сможете оценить влияние оптимизации.
Раньше было сложно определить, сколько классов сохраняет правило – два или две тысячи.
Давайте посмотрим, как R8 работает в контексте моего приложения и библиотеки, которую мы рассматривали ранее.
Итак, у меня есть модуль приложения и три библиотеки.
И обратите пристальное внимание на библиотеку три справа.
Это библиотека Analytics Bloat, которая включает правило сохранения на уровне пакета.
Сначала R8 определяет корневую точку входа из всех объединенных правил сохранения, которые выделены желтым цветом.
И, как правило, это должны быть основные точки входа в приложение, например классы в манифесте.
А из-за правила сохранения с подстановочным знаком в библиотеке Analytics Bloat каждый класс в ней считается точкой входа.
Затем R8 отслеживает доступность по графу вызовов, начиная с этих корней.
Все классы, доступные из корневого, отмечены зеленым цветом, а неиспользуемые классы классифицируются как неработающий код и отмечены красным.
Затем R8 выполняет оптимизацию и удаляет неиспользуемый код.
Итак, после этого встряхивания дерева или первого прохода сжатия вот что осталось.
А у нашего приложения показатель уменьшения составляет 42,5 %, что выше порогового значения Google Play в 25 %, но все же не совсем то, что нам нужно.
И это в основном связано с правилом сохранения на уровне пакета из библиотеки аналитики.
И это делает так, что только 17 из оставшихся 40 классов могут быть дополнительно сокращены.
Я хочу подчеркнуть, что одно плохое правило сохранения может вызвать серьезные проблемы с оптимизацией приложения.
Теперь давайте поговорим о библиотеках.
В прошлом году наша команда много работала над тем, чтобы понять, как правила хранения потребителей библиотек влияют на оптимизацию экосистемы.
Многие библиотеки поставляются со слишком широкими правилами во имя стабильности.
или указывают в документации общие правила, которые разработчики приложений должны добавить в свои приложения.
Однако для стабильности не обязательно использовать широкие правила, а на производительность они влияют негативно, и в первую очередь это сказывается на разработчиках приложений.
Мы видели случаи, когда вся библиотека сохранялась с помощью правила сохранения на уровне пакета, как в случае с библиотекой аналитики, и это имело далеко идущие последствия для потенциала оптимизации моего приложения.
Добавлю, что даже наши собственные SDK в Google не были застрахованы от этого, и за последний год более десятка библиотек Google исправили слишком широкие правила сохранения.
Анализатор конфигурации интегрирован в AGP 9.3, но его можно использовать и в более ранних версиях, если обновить R8.
Выполнение этой команды создаст HTML-отчет, который мы показывали. В нем будут ваши правила сохранения, влияние на сжатие, оптимизацию и обфускацию для отдельных правил и приложения в целом.
Если вы используете полную конечную сборку R8, отчет создается автоматически и его можно найти по этому пути.
Преимущество этой автономной задачи заключается в том, что на ее создание уходит меньше времени, чем на полную сборку.
Команда WHOOP, которая, возможно, сейчас здесь, запустила анализатор конфигурации и получила следующие результаты: сжатие – 74 %, обфускация – 74 %, оптимизация – 0 %.
Как вы можете себе представить, команда была очень удивлена, увидев это значение оптимизации.
Благодаря этому разработчики обнаружили, что в их сборке использовался устаревший файл ProGuard, который отключал оптимизацию. Перейдя на обновленную версию, они смогли включить оптимизацию для своего приложения.
Это позволило оптимизировать приложение на 55 % и уменьшить размер APK-файла на 4 %.
В сочетании с базовыми профилями это позволило сократить время холодного запуска на 38 % и уменьшить количество зависших кадров на 28 %.
На экране слева показано приложение без профилей базовой конфигурации, а на экране справа – с ними. Как видите, во втором случае приложение запускается быстрее.
Давайте посмотрим ещё раз.
то идем дальше.
По этой ссылке вы перейдете к руководству по анализатору конфигурации. Проверять отчет вручную – это здорово, но, как вы понимаете, в зависимости от состояния конфигурации R8 вашего приложения это может занять много времени.
Чтобы ускорить процесс, мы создали навык R8 Analyzer.
$ android skills list
$ android skills add r8-analyzerВы можете попросить агента проанализировать конфигурацию R8, и он не только создаст отчет, но и укажет на самые важные правила.
Вот пример выходных данных. Обратите внимание, что показатель оптимизации составляет 42 %. Это означает, что 58 % базы кода не может быть оптимизировано с помощью R8.
Если мы посмотрим на раздел правил сохранения, то увидим, что правило сохранения на уровне пакета для библиотеки аналитики предотвращает оптимизацию 22 % базы кода. Навык также рекомендует уточнить это правило сохранения, поскольку, как мы уже знаем, оно слишком широкое.
Теперь вы можете диагностировать, что вызывает раздувание приложения и блокирует оптимизацию DEX, используя анализатор конфигурации R8 и навык.
Но как заменить слишком общее правило, не вызвав при этом сбой?
Передаю слово Крису, который покажет, как перейти от устаревших правил сохранения к новым типобезопасным автоматически созданным аннотациям сохранения.
Спасибо, Бриана.
Давайте поговорим о том, как мы к этому пришли.
С этой раздутой библиотекой аналитики, со всеми этими правилами сохранения, которые... с этим правилом сохранения, которое сохраняет практически всё, основная проблема, с которой мы сталкиваемся, – это рефлексия.
Давайте посмотрим на код, который мы написали в прошлый раз.
fun createWithReflection(className: String): MyObject {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as MyObject
}Это функция создания экземпляра объекта, которая просто раздувает определенный объект и делает это рефлексивно.
Недостаток такого подхода в контексте оптимизации DEX заключается в том, что все эти статические вызовы откладываются до времени выполнения.
Просто анализируя код, в зависимости от того, откуда берется это имя класса, наш оптимизатор R8 не может увидеть, что здесь происходит.
В таких ситуациях оптимизация Dex по умолчанию не выполняется, поэтому обычно приходится добавлять правило Keep.
В идеале, вы просто избегаете этого, верно?
В идеале мы просто полностью избегаем рефлексии, особенно сейчас, в 2026 году, когда у нас есть потрясающие инструменты генерации кода для сериализации, для внедрения зависимостей, которые могут полностью устранить эту категорию проблем.
Но это не всё, верно?
В некоторых случаях рефлексия все ещё необходима.
Давайте посмотрим, как это произошло в этой раздутой библиотеке.
Вот один конкретный пример, и это обычный пример, это необязательная зависимость.
// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}
// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}
// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}Здесь мы раздуваем определенный тип, который недоступен в пути к классам библиотеки, поэтому мы должны сделать это со строкой.
Здесь мы ссылаемся на определенный класс, который может присутствовать или отсутствовать в момент сборки библиотеки. Поэтому вполне логично, что эта библиотека использует рефлексию для поиска элемента внутри связанной библиотеки.
Ещё один случай, который мы здесь имеем, – это динамическое создание экземпляров подклассов.
Поэтому, особенно если это что-то, поступающее из файла манифеста, или что-то, отправляемое со стороны сервера, мы сообщаем локальному приложению: "Эй, пожалуйста, раздуй эту штуку, создай эту штуку позже".
И третья категория, которую мы здесь видим, – это сериализация.
Я имею в виду, что у меня есть сериализация JSON, которая происходит путем итерации по всем элементам.
Это, конечно, отвратительно, но, знаете, такие вещи иногда задерживаются.
Не всегда удается исправить проблему сразу.
Но давайте поговорим о том, к чему это приводит.
По мере того, как эти рефлексивные вызовы накапливаются, у вас появляется больше мотивации делать то, что проще, и оптимизировать работу разработчиков, а не пользователей.
Это не очень хорошо, поэтому я добавлю задачу, и все будет в порядке.
# TODO: clean this up
-keep com.example.analytics.bloat.** { * };Когда-нибудь я вернусь и все исправлю, верно?
Давайте поговорим о том, что мы должны сделать, что мы должны были сделать, по крайней мере, до сегодняшнего дня, как бы вы написали здесь идеальное правило Keep.
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}Пристегните ремни.
Итак, первая часть здесь заключается в том, что мы хотим сохранить конкретный класс, который мы возвращаем из этой рефлексивной функции.
### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
*
}Хотя нет, подождите.
Знаешь что?
Это не так.
Это класс Kotlin, мне нужно убедиться, что он строго типизирован, позвольте мне добавить туда KT.
### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
*
}На самом деле, важно то, что я возвращаю не EventFilter, а подкласс EventFilter. Поэтому мне нужно сохранить классы, расширяющие EventFilter, чтобы правильно выполнить десериализацию.
### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
*
}И на самом деле, если я хочу сделать это более эффективно. Теперь это сработало бы, но чтобы сделать это более эффективным, как хороший разработчик библиотеки, что мне нужно сделать дальше?
Точнее, -keepclasseswithmembers, и оставить только те классы, у которых есть конструктор <init>, используемый для создания объекта.
### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}И если я хочу быть по-настоящему хорошим, я добавлю allowaccessmodification, чтобы разрешить дальнейшую оптимизацию всех API, на которые нацелено это правило Keep, потому что я, знаете ли, не хочу препятствовать дальнейшей оптимизации со стороны R8, я хочу постараться быть хорошим гражданином.
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}И последний шаг, который почти никто не делает, потому что это дополнительная работа, но это отлично подходит для библиотеки, – сделать ее условной.
### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}Теперь, концептуально, вам не нужно сохранять все эти подклассы, если вы на самом деле не вызываете эту функцию, и это то, что делает этот оператор if в верхней части правила Keep.
Я мог бы попросить вас поднять руки, но знаю, что почти никто этого не делает.
То есть, фактически, написание его, делая его условным.
Что, если мы сможем сделать это немного лучше?
Давайте сначала посмотрим, как, хорошо.
Может, есть другой способ?
Итак, мы обсудили, что произойдет, если я напишу узкое, написанное вручную правило Keep.
Технически это возможно, но такой код сложно писать и читать, а также поддерживать, потому что в нем много мелких строковых элементов, которые легко сломать, если что-то пойдет не так.
Под чем-то, под чем-то, что меняется.
И у нас есть альтернативы.
У нас есть аннотации, такие как @Keep.
Аннотация @Keep уже давно входит в пакет аннотаций AndroidX, но сейчас ее использование не рекомендуется.
@androidx.annotation.Keep
class EventFilter { … }
@androidx.annotation.Keep
class SizeBasedEventFilter { … }
@androidx.annotation.Keep
class DropLogsEventFilter { … }Это не лучшее решение для разработчиков библиотек, и, по сути, оно запрещено внутри компании, потому что, хотя его проще поддерживать, оно очень негибкое и не имеет некоторых из тех оптимизаций, о которых я говорил ранее.
Это невозможно.
Она не позволяет анализировать звонки.
Теперь, если мы посмотрим на тот конкретный случай, который у нас был раньше: "О, ну, я могу просто поставить @Keep на свой EventFilter, у меня все хорошо, верно?"
@Keep
class EventFilter { … }Вообще-то нет.
@Keep doesn't actually target subclasses, it only targets that class.
@Keep
class EventFilter { … }
@Keep
class SizeBasedEventFilter { … }
@Keep
class DropLogsEventFilter { … }На самом деле мне нужно указать все подклассы, и теперь я пишу больше аннотаций @Keep, чем если бы я использовал одно правило Keep, которое было сложно написать.
Итак, у обоих этих подходов есть проблемы, верно?
О них легко забыть, вы узнаете о них поздно, и они не зависят от звонящего по умолчанию.
Особенно для библиотек, мы хотим лучший способ.
Мы хотим, чтобы у вас была возможность сразу получить готовый результат.
И одна из больших проблем, которую мы здесь видим, заключается в том, что эти вещи живут далеко от отражения, от которого мы на самом деле защищаемся.
В конечном счете, решение, которое мы хотим, это то, что живет прямо против, вы знаете, непослушный код отражения здесь, что на самом деле раздувает целевые вещи.
Давайте поговорим о Jetpack Keep, новой библиотеке и плагине от AndroidX, которые предназначены для решения именно этой проблемы.
Итак, цель Jetpack Keep – сначала сгенерировать правила сохранения из аннотаций, а затем сгенерировать аннотации непосредственно из рефлексии, которая уже есть в вашей базе кода.
Настройка очень проста.
plugins {
id("com.android.library")
id("androidx.annotation.keep")
}
dependencies {
implementation("androidx.annotation.keep:keep")
...
}Это плагин Gradle и библиотека аннотаций.
Но давайте посмотрим, как это на самом деле работает во время сборки.
Итак, если у меня есть предыдущий пример, где я раздуваю подкласс известного типа, что мне нужно сделать?
Я просто бегаю lintFix.
./gradlew analyticsbloat:lintFixLint анализирует все отражения, происходящие внутри этого метода, и говорит: "Эй, вам нужна эта аннотация".
@UsesReflectionToConstruct(
classConstant = EventFilter::class,
parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}Эта аннотация точно выражает, что делает рефлексия внутри этого вызова.
Это означает, что я создаю класс этого типа.
В данном случае EventFilter – это известный класс.
Я не использую строку для ссылки на него, потому что мне это не нужно.
Это дополнительная безопасность, и я даже кодирую parameterTypes, чтобы точно указать, какой конструктор сохраняется.
Но что это дает?
Например, как это влияет на конечную сборку R8?
Он генерирует правило сохранения, и, как вы видели ранее, это точно такое же идеальное правило сохранения, которое у нас было раньше.
# Generated Keep Rule
-if class com.example.analytics.bloat.EventTools {
class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
void <init>();
}Она нацелена на те же классы, подклассы и зависит от этой точки отражения.
И мне не нужно поддерживать... Мне не нужно заботиться об аннотировании всех подклассов.
Мне не нужно хранить отдельный файл с правилами хранения.
Это гораздо более простой способ управления правилами хранения.
Давайте вернёмся к примеру с необязательной зависимостью.
@UsesReflectionToConstruct(
className = "com.example.analytics.bloat.crash.CrashReporterTransport",
parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}Теперь, там, класс не был на пути класса, но я все еще могу ссылаться на него.
Я просто делаю это, используя строку, но все остальное, типы параметров, на них можно ссылаться по ссылкам на классы, поэтому я так и делаю.
И снова мы создаем идеальное правило хранения для этого случая.
# Generated Keep Rule
-if class com.example.analytics.bloat.CrashConnection {
class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
void <init>(android.content.Context);
}Она условная, конкретная и целевая, и позволяет оптимизировать целевые классы везде, где это возможно.
Вот рефлексия по полям. В этом конкретном случае я перебирал все поля внутри объекта, но и здесь мы прекрасно справляемся, говоря, что нужно нацелиться на все поля с помощью этого оператора со звездочкой.
// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
classConstant = UserActionEvent::class,
fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}Это позволяет мне нацеливаться на все поля, и посмотрите, это генерирует точное правило сохранения, которое вы ожидаете от этого, знаете ли, невероятно неудачного отражения, которого не должно быть в моем приложении, но, эй, я вернусь и уберу это, я полностью обещаю.
# Generated Keep Rule
-if class com.example.analytics.bloat.serialization.EventSerializer {
class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
<fields>;
}Что делает Jetpack Keep?
Это значительно упрощает обслуживание правил хранения.
Все, что можно сгенерировать автоматически, просто удаляется из вашей зоны ответственности и зоны риска, связанной с соблюдением правил хранения.
Это также дает вам детерминизм линта.
Она не просто исправляет ошибки, когда вы итеративно пытаетесь выполнить миграцию, а обнаруживает новые отражения и отражающие изменения при любом изменении кода. И эта безопасность абсолютно критична.
Можно ли использовать Offerwall?
Итак, каковы здесь зависимости?
Для плагина и lint требуется AGP 8.7.
Это должно быть почти у всех.
Но это также очень гибкий инструмент, который можно использовать в разных местах.
Вы можете использовать его в приложениях.
Вы можете использовать его в библиотеках Android, AAR, KMP и JAR, но в этом случае потребуется немного ручной настройки.
Но ключевая особенность здесь в том, что это безопасно использовать в качестве разработчика библиотеки.
Даже если кто-то использует древнюю версию R8 или даже сторонний инструмент, который использует вашу библиотеку, мы все равно экспортируем фактические стандартные потребительские правила, чтобы любой мог по-прежнему использовать вас и не беспокоиться о том, что он использует какой-то новый несовместимый формат.
Итак, я рассказал о том, как Jetpack Keep помогает оптимизировать код и защищать вызовы рефлексии. Теперь давайте поговорим о новых функциях, которые мы недавно запустили в Android CLI для перепроверки на последнем этапе выпуска.
Бриана уже упоминала, что в Play Findings есть инструменты, которые предоставляют информацию о размерах, размере DEX и процентах оптимизации.
Теперь мы начали делать это доступным и через Android CLI.
Он использует ту же логику, что и Google Play, поэтому вам не нужно загружать набор в Google Play, чтобы проанализировать его с помощью CLI.
$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>и вы сразу же получаете ту же информацию в виде локального анализа.
В данном случае приложение получает предупреждение о том, что у него очень низкий уровень оптимизации. Также в нем указан размер DEX-файла и процент оптимизации, как и в Google Play.
Также здесь есть выходные данные JSON. Если вы хотите написать скрипт для автоматизации регрессионного тестирования, чтобы отслеживать резкое падение любого из этих значений, поскольку, опять же, это цифры, которые применяет Google Play, вы можете легко сделать это с помощью этих отчетов JSON.
{
"optimization": {
"bundleAnalysisResult": {
"agpVersion": "9.3.2",
"r8Json": {
"version": "9.3.16",
"options": {
"isRepackageClassesEnabled": true,
"isProguardCompatibilityModeEnabled": false
},
"stats": {
"shrinkingRatio": 0.2306,
"optimizationRatio": 0.2351,
"obfuscationRatio": 0.2322
},
...
}
"mappingObfuscationRatio": 0.21631891,
"dexHeuristicObfuscationRatio": 0.25118626,
"uncompressedDexSizeBytes": "16198152"
}Здесь мы видим точно такие же цифры.
Но это не единственная функция android analyze.
android analyze также можно запускать в библиотеках.
$ android analyze <library.aar>
$ android analyze <library.jar>И, как упомянула Бриана, существует множество проблем с библиотеками, работающими с доставкой, с неудачными правилами хранения внутри них.
Давайте рассмотрим некоторые из этих проблем.
Итак, когда я запускаю это на конкретном AAR, который у меня есть, я вижу, что в этой библиотеке есть ряд проблем, выделенных в самом верху.
У этой библиотеки высокий коэффициент сохранения. Это означает, что она сохраняет слишком много данных из-за неограниченных правил сохранения.
Он содержит широкие правила сохранения, которые не позволяют его оптимизировать, и ниже приведены некоторые цифры, которые точно показывают, насколько это плохо, а также правила сохранения, которые, что, пожалуй, самое страшное из всего этого, ни на что не нацелены, что, вероятно, означает, что они нацелены на кого-то другого каким-то неограниченным образом.
Очень опасные запахи кода.
Таким образом, вы можете проверить все это в AAR, особенно в качестве издателя библиотеки, это отличный способ убедиться, что вы не поставляете плохие правила хранения и не позволяете им просачиваться, когда кто-то, возможно, не такой эксперт в написании правил хранения, работает над библиотекой.
Здесь вы видите, что мы также выводим коэффициент удержания.
Это хороший способ оценить общее качество вашей библиотеки.
Если я просто оптимизирую отдельную библиотеку полностью автономно, без ссылок, с помощью R8, я должен увидеть, что она равна нулю байт, верно?
То есть он должен быть пустым, потому что в нем ничего не хранится.
Это было бы идеально.
В данном случае это не так.
Вы можете сразу увидеть размеры R8 и D8.
Мы также выделяем некоторые из худших правил хранения, чтобы сделать их немного более заметными для вас.
Он также имеет формат JSON, поэтому независимо от того, являетесь ли вы разработчиком приложений, пытающимся заблокировать обновления библиотеки, или разработчиком библиотеки, пытающимся предотвратить серьезные регрессии в качестве вашей библиотеки, вы можете выполнить тот же анализ и предотвратить возникновение этих проблем с помощью простого скрипта, который упаковывает Android CLI.
{
"optimization": {
"libraryAnalysisResult": {
"dex": {
"d8CompileResult": {
"dexSizeBytes": 4523352
},
"r8CompileResult": {
"dexSizeBytes": 3266596,
"configuration": {
"usedPackageWideKeepRules": [
{
"source": "-keep class com.example.analytics.bloat.** { *; }",
"keptItemCount": 125579,
},
...
],
"unusedPackageWideKeepRules": [
{
"source": "-keep class some.other.library.** { *; }",
},
...
]
},
...
}Поэтому, если вы обнаружите проблему с библиотекой, сначала попробуйте ее обновить.
Как уже говорила Бриана, мы исправили более десятка библиотек Google, чтобы устранить некоторые неудачные правила сохранения. Есть и другие библиотеки, в которых были внесены исправления, но они не обновляются приложениями, которые их используют.
Это отличный первый шаг.
Во-вторых, вы можете найти проблему и связаться с разработчиком библиотеки и, возможно, прикрепить отчет анализатора, в котором говорится: "Эй, вы слишком много храните.
Помогите мне оптимизировать приложение".
Итак, подведем итоги. Мы поговорили об R8 Config Analyzer, который помогает проанализировать все, что происходит в сборке R8, и понять, что можно оптимизировать, а что нет.
Мы уже говорили о Jetpack Keep – новой библиотеке, которая помогает автоматически создавать правила сохранения.
И Android CLI – новый инструмент, который поможет вам проверить и убедиться, что вы, ваш агент или младший инженер в вашей команде ничего не испортили для ваших потребителей.
Если вы хотите узнать больше, ознакомьтесь с документацией по R8, анализатору конфигурации и Jetpack Keep.
В документации по Android CLI рассказывается о команде analyze и о том, как ее использовать.
Большое спасибо.
android {
buildTypes {
release {
optimization {
// no need to specify default
// rules and resource shrinking
enabled = true
}
}
}
...
}android {
buildTypes {
release {
// Enables code optimizations.
isMinifyEnabled = true
// Enables resource shrinking.
isShrinkResources = true
proguardFiles(
// Default rules.
getDefaultProguardFile(
"proguard-android-optimize.txt"
),
... // list out each file with keep rules
)
...
}
}
...
}$ android skills list
$ android skills add r8-analyzerfun createWithReflection(className: String): MyObject {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as MyObject
}// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}
// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}
// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}# TODO: clean this up
-keep com.example.analytics.bloat.** { * };fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
*
}### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
*
}### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
*
}### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}@androidx.annotation.Keep
class EventFilter { … }
@androidx.annotation.Keep
class SizeBasedEventFilter { … }
@androidx.annotation.Keep
class DropLogsEventFilter { … }@Keep
class EventFilter { … }@Keep
class EventFilter { … }
@Keep
class SizeBasedEventFilter { … }
@Keep
class DropLogsEventFilter { … }plugins {
id("com.android.library")
id("androidx.annotation.keep")
}
dependencies {
implementation("androidx.annotation.keep:keep")
...
}./gradlew analyticsbloat:lintFix@UsesReflectionToConstruct(
classConstant = EventFilter::class,
parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}# Generated Keep Rule
-if class com.example.analytics.bloat.EventTools {
class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
void <init>();
}@UsesReflectionToConstruct(
className = "com.example.analytics.bloat.crash.CrashReporterTransport",
parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}# Generated Keep Rule
-if class com.example.analytics.bloat.CrashConnection {
class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
void <init>(android.content.Context);
}// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
classConstant = UserActionEvent::class,
fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}# Generated Keep Rule
-if class com.example.analytics.bloat.serialization.EventSerializer {
class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
<fields>;
}$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>{
"optimization": {
"bundleAnalysisResult": {
"agpVersion": "9.3.2",
"r8Json": {
"version": "9.3.16",
"options": {
"isRepackageClassesEnabled": true,
"isProguardCompatibilityModeEnabled": false
},
"stats": {
"shrinkingRatio": 0.2306,
"optimizationRatio": 0.2351,
"obfuscationRatio": 0.2322
},
...
}
"mappingObfuscationRatio": 0.21631891,
"dexHeuristicObfuscationRatio": 0.25118626,
"uncompressedDexSizeBytes": "16198152"
}$ android analyze <library.aar>
$ android analyze <library.jar>{
"optimization": {
"libraryAnalysisResult": {
"dex": {
"d8CompileResult": {
"dexSizeBytes": 4523352
},
"r8CompileResult": {
"dexSizeBytes": 3266596,
"configuration": {
"usedPackageWideKeepRules": [
{
"source": "-keep class com.example.analytics.bloat.** { *; }",
"keptItemCount": 125579,
},
...
],
"unusedPackageWideKeepRules": [
{
"source": "-keep class some.other.library.** { *; }",
},
...
]
},
...
}