Меню
R8 Optimized Resource Shrinking в AGP 9: когда статический анализ встречает динамический доступ

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)

R8 Optimized Resource Shrinking
Оптимизированное сжатие ресурсов тесно связывает анализ кода и доступных ресурсов.

Эволюция 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)

Полезные ресурсы