R8 Optimized Resource Shrinking в AGP 9: коли статичний аналіз зустрічає динамічний доступ
від Nikolay Vlasov
Перехід на Android Gradle Plugin (AGP) 9.0 змінив поведінку очищення ресурсів (resource shrinking): якщо у build type увімкнено isShrinkResources = true, тепер використовується optimized resource shrinking. Цей механізм тісніше пов'язує аналіз ресурсів з аналізом коду, видаляючи будь-які ресурси, на які посилається лише невикористовуваний код. Саме тому у проектах з динамічним доступом до ресурсів можуть виникати помилки, які раніше залишалися непоміченими. (Android Developers)
Чому ресурси зникають у Dynamic Feature Modules?
Сучасні інструменти очищення ресурсів (resource shrinkers) значною мірою покладаються на статичний аналіз. Якщо на ресурс є пряме посилання у коді (наприклад, R.raw.my_resource), shrinker може легко відстежити його використання. Однак, коли доступ до ресурсів здійснюється динамічно за допомогою строкових імен та пошуку під час виконання (runtime lookup), статичний аналіз вже не може надійно довести, що ресурс необхідний. В AGP 9 це стає ще більш помітним, оскільки optimized resource shrinking тепер є частиною єдиного пайплайну оптимізації коду та ресурсів. (Android Developers)
В архітектурі Dynamic Feature Modules (DFM) базовий модуль часто не має залежностей під час компіляції (compile-time dependencies) від R-класів фічі, особливо коли модуль доставляється окремо через Play Feature Delivery. Хоча новий resource shrinker вже деякий час підтримує динамічні фічі, саме така архітектура найчастіше призводить до того, що частина ресурсів виявляється «втраченою» через динамічний пошук. (Android Developers)
Розглянемо приклад з проекту Smart Directory Agent Center. Завантаження індексу повнотекстового пошуку реалізовано наступним чином:
private fun getFtsId(): Int {
return dynamicContextWrapper.getContextForFts().resources.getIdentifier(
"fts",
"raw",
featuresManager.ftsModulePackage,
)
}
Для інструменту очищення ресурсів такий виклик — це «чорна скринька»: ім'я ресурсу передається як рядок, а не через статичне посилання. В результаті інструменти збірки можуть помилково вважати ресурс невикористовуваним і видалити його під час релізної збірки, що призводить до помилки Resources$NotFoundException під час виконання. (Android Developers)
Еволюція Resource Shrinking в AGP
Важливо розуміти, що AGP 9.0 не винайшов очищення ресурсів заново; натомість він зробив його поведінку більш суворою та передбачуваною в межах оптимізованого пайплайну. Функція Precise resource shrinking вже була увімкнена за замовчуванням в AGP 8.3. В AGP 9.0 optimized resource shrinking запускається автоматично при isShrinkResources = true. Для версій між 8.6 та 9.0 ця поведінка зазвичай вимагала явного встановлення прапорця android.r8.optimizedResourceShrinking=true. (Android Developers)
Окремо варто пам'ятати про регресію в AGP 8.9: у примітках до релізу зафіксовано проблему очищення ресурсів, через яку в динамічних модулях функцій могли зникати ресурси. Виправлення були внесені у версії 8.9.2 та 8.10. Тому краш у релізній збірці не завжди означає помилку у вашій архітектурі — іноді це дійсно баг конкретної версії shrinker'а. (Android Developers)
Реальне рішення: keep-файли та явне збереження ресурсів
Правильний спосіб захистити ресурси, які використовуються опосередковано — визначити keep-файл у ресурсах проекту. Документація рекомендує створювати XML-файл у res/raw/ (наприклад, res/raw/my.package.keep.xml), а не покладатися на абстрактний «глобальний keep.xml» як на особливий механізм. Файл повинен мати унікальне ім'я, щоб уникнути конфліктів під час злиття ресурсів у багатомодульному проекті. (Android Developers)
У нашому кейсі виправлення виглядало так:
<!-- Шлях: app/src/main/res/raw/com.example.app.keep.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@style/Theme.StartingScreen_*,@style/AppTheme_*,@raw/fts" />
Такий підхід робить намір явним: ресурс @raw/fts не має видалятися, навіть якщо він доступний лише через динамічне розпізнавання імен. Це критично важливо для будь-яких ресурсів, що зчитуються через Resources.getIdentifier(). (Android Developers)
Best practices для AGP 9 та R8
По-перше: для ресурсів, що використовуються динамічно, краще перераховувати їх явно у tools:keep, а не сподіватися на маски на кшталт @raw/*. Чим вужче правило, тим менший ризик зберегти зайве і тим зрозумілішою буде поведінка збірки. (Android Developers)
По-друге: пам'ятайте про режим shrinkMode. За замовчуванням використовується safe mode — він намагається зберегти ресурси, які можуть бути використані динамічно через getIdentifier(). Якщо ввімкнути tools:shrinkMode="strict", буде залишено лише явно цитовані ресурси, тому точність keep-правил стає критичною. (Android Developers)
По-третє: не змішуйте причину та симптом. Проблема виникає не через те, що проект використовує DFM як такі, а через те, що частина ресурсів доступна лише через пошук під час виконання (runtime lookup) і не видна статичному аналізатору. DFM просто робить цей сценарій більш імовірним і більш болісним під час оптимізації. (Android Developers)
Висновок
Проблема ресурсів, що зникають в AGP 9 — це не магія і не випадковість. Це логічний наслідок більш суворого статичного аналізу в optimized resource shrinking. Якщо архітектура спирається на динамічний доступ до ресурсів, такі ресурси потрібно явно зберігати через keep-файли та перевіряти поведінку у відповідному режимі shrinker'а. А якщо помилка з'явилася саме після оновлення до AGP 8.9–9.0, варто додатково звірити версію плагіна: у цій області були відомі регресії, вже виправлені у новіших патчах. (Android Developers)
Корисні ресурси
- 1 Enable app optimization with R8 — офіційний посібник з налаштування R8.
- 2 Android Gradle Plugin 7.1.0 Release Notes — історія змін та підтримка Dynamic Features.
- 3 Tools attributes reference — опис атрибутів
keepтаshrinkMode. - 4 Android Gradle Plugin 8.3.0 Release Notes — подробиці про Precise Resource Shrinking.
- 5 Android Gradle Plugin 8.9.0 Release Notes — інформація про виправлення багів очищення ресурсів.
- 6 Customize which resources to keep — детальний посібник з роботи з
keep.xml.