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 shrinker опирается на статические ссылки в коде. Если ресурс упоминается как R.raw.my_resource, его легко отследить. Если же доступ идёт динамически, через строковые имена и runtime lookup, статический анализ уже не может надёжно доказать, что ресурс используется. В AGP 9 это становится особенно заметно, потому что optimized resource shrinking работает как часть общего пайплайна оптимизации кода и ресурсов. (Android Developers)
В архитектуре Dynamic Feature Modules (DFM) базовый модуль часто не имеет compile-time зависимости от R-классов фичи, особенно когда модуль доставляется отдельно через Play Feature Delivery. Сами dynamic features поддерживаются новым resource shrinker’ом уже давно, но именно такая архитектура чаще всего приводит к тому, что часть ресурсов оказывается видимой только через динамический доступ. (Android Developers)
Рассмотрим пример из проекта Smart Directory Agent Center. Загрузка полнотекстового индекса реализована так:
private fun getFtsId(): Int {
return dynamicContextWrapper.getContextForFts().resources.getIdentifier(
"fts",
"raw",
featuresManager.ftsModulePackage,
)
}
Для shrinker’а такой вызов — это «чёрный ящик»: имя ресурса передаётся строкой, а не через статическую ссылку. В результате build tools могут считать ресурс неиспользуемым и удалить его при релизной сборке, если для него не задано явное правило сохранения. Именно это и приводит к Resources$NotFoundException в рантайме. (Android Developers)
Эволюция Resource Shrinking в AGP
Важно понимать, что AGP 9.0 не изобрёл resource shrinking заново, а сделал его поведение более жёстким и предсказуемым в рамках оптимизированного пайплайна. В AGP 8.3 precise resource shrinking уже был включён по умолчанию, а в AGP 9.0 optimized resource shrinking включается автоматически при isShrinkResources = true; для более старых версий между 8.6 и 9.0 требовалось явно задавать флаг android.r8.optimizedResourceShrinking=true. (Android Developers)
Отдельно стоит помнить про регрессию AGP 8.9: в release notes зафиксирована проблема resource shrinking, из-за которой в dynamic feature modules могли пропадать ресурсы; исправления были внесены в 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 — он сохраняет ресурсы, которые могут быть использованы динамически через Resources.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.