Как оптимизировать код платформы и ресурсы с помощью R8

Система сборки платформы Android (Soong) запускает компилятор R8 для целей, которые компилируют байт-код DEX, включая приложения Android (android_app), тесты (android_test) и устанавливаемые библиотеки Java с compile_dex: true (например, services.jar), чтобы уменьшить, оптимизировать и удалить неиспользуемый код и ресурсы. В статических библиотеках (android_library и статической java_library) Soong не запускает R8 напрямую, а использует блок свойств optimize, чтобы прикреплять и распространять правила сохранения потребителей (export_proguard_flags_files: true) на целевые объекты DEX, которые статически связывают их.

Для разработчиков платформ, создающих образы систем или пакеты поставщиков, устранение слишком широких правил сохранения дает прямые преимущества для работоспособности системы:

  • Меньший размер системного раздела. R8 удаляет неиспользуемые классы, методы и ресурсы перед упаковкой APK- и JAR-файлов в /system, /system_ext, /product и /vendor.
  • Меньший размер скомпилированных артефактов. Чем меньше методов DEX, тем меньше размер файлов .odex и .vdex, создаваемых dex2oat во время компиляции на устройстве или при сборке.
  • Снижение нагрузки на оперативную память во время выполнения. Поскольку Android сопоставляет .odex, .vdex и таблицы ресурсов APK с памятью процесса, используя вызовы mmap с подкачкой страниц по запросу, меньшие по размеру двоичные файлы снижают количество ошибок отсутствия страниц при запуске приложения и уменьшают объем резидентного кода и памяти ресурсов в процессах. Подробнее о том, как скомпилированный код приложения влияет на память устройства, рассказывается в разделе Код приложения – это память.
  • Более эффективная оптимизация всей программы. Ограничения на сохранение позволяют R8 встраивать методы, девиртуализировать интерфейсы и виртуальные вызовы, удалять неиспользуемые поля и распространять константы между классами.

Как настроить оптимизацию R8 в Soong

В файлах Android.bp настройте оптимизацию R8 с помощью блока свойств optimize для целевых объектов android_app и устанавливаемых объектов java_library (или для модулей android_library и статических модулей java_library, чтобы экспортировать правила сохранения для пользователей):

android_app {
    name: "MySystemApp",
    srcs: ["src/**/*.java"],
    optimize: {
        obfuscate: true,
        shrink_resources: true,
    },
}

В таблице ниже приведены наиболее распространенные свойства optimize в Soong (определены в build/soong/java/dex.go).

Свойство Описание
enabled Определяет, запускается ли R8 на целевом устройстве. Значение по умолчанию для всех целей android_app – true.
shrink Управляет удалением недоступных классов, полей и методов. По умолчанию используется true для целей android_app (false для отдельных модулей java_library и тестовых модулей, которые передают -dontshrink, если не задано иное).
optimize Управляет оптимизацией байт-кода, например встраиванием методов, объединением классов, распространением констант и удалением неиспользуемых ветвей. По умолчанию имеет значение true для целей android_app (управляется флагом конечной сборки RELEASE_R8_OPTIMIZE_BY_DEFAULT).
obfuscate Управляет минификацией и переименованием идентификаторов. По умолчанию используется значение false для совместимости с предыдущими версиями, но по возможности задайте значение true, чтобы уменьшить размер DEX-файла и получить доступ к более глубокой оптимизации (см. раздел По возможности включите обфускацию).
shrink_resources Удаляет неиспользуемые ресурсы (res/ записей) из упакованного APK-файла после сокращения кода. Значение по умолчанию – false.
optimized_shrink_resources Запускает интегрированный конвейер R8 для сокращения кода и ресурсов, чтобы R8 отслеживал ссылки на код и ресурсы за один проход. Если задано значение shrink_resources: true, по умолчанию используется RELEASE_USE_OPTIMIZED_RESOURCE_SHRINKING_BY_DEFAULT (true в стандартных сборках платформы).
proguard_flags_files Перечисляет файлы .flags или .pro, относящиеся к модулю и содержащие специальные правила сохранения. Не добавляйте собственные файлы, если достаточно стандартных аннотаций или значений по умолчанию.
export_proguard_flags_files Передает proguard_flags_files этой библиотеки в дочерние модули, которые зависят от нее статически.
trace_references_from Перечисляет прямые зависимые цели компиляции DEX (подключенные через instrumentation_for или libs), ссылки на байт-код которых в этой цели R8 автоматически отслеживает и сохраняет (также распространяется на варианты override_android_app и override_android_test и поддерживается для независимых сопутствующих библиотек Java в паре с разделением .impl).
allow_traced_references Позволяет зависимым целям, которые компилируются напрямую в DEX (подключены через instrumentation_for или libs, например тесты поставщиков или тесты, которые выполняются в разных хранилищах и не могут быть указаны в trace_references_from), включить трассировку ссылок на байт-код в этой цели, если задано значение trace_references: true.
trace_references Если для этого параметра задано значение true в зависимом модуле, который компилируется напрямую в DEX и относится к целевому модулю с параметром allow_traced_references: true, то в этом модуле запускается R8 tracereferences, а сгенерированные правила сохранения передаются в целевой модуль.
protect_api_surface Передает --protect-api-surface в R8. Если задать это свойство для java_sdk_library, оно также автоматически сгенерирует и прикрепит правила сохранения Metalava API во всех включенных областях API (общедоступной, системной, module-lib, system-server и тестовой) к библиотеке реализации.

Статические библиотеки (android_library, java_library и импортированные файлы AAR) также могут задавать required_downstream_optimization: { enabled: true, optimize: true } в Android.bp, чтобы требовать от любой последующей цели компиляции DEX, которая статически связывает их (напрямую или транзитивно), включать сокращение R8 и оптимизацию байт-кода. Soong автоматически включает эту функцию в androidx.compose.runtime_runtime, поэтому цели, связанные с Jetpack Compose, оптимизируются с помощью R8.

Включите обфускацию везде, где это возможно.

В Soong для obfuscate по умолчанию задано значение false для обратной совместимости, но вы должны явно задать obfuscate: true для android_app и отдельных целей DEX, где это возможно:

  • Уменьшение размера DEX и .vdex. Переименование пакетов, классов, полей и методов в короткие идентификаторы (a, b) уменьшает пул строк DEX (string_ids и string_data_item) и дескрипторы типов. Поскольку Android сопоставляет файлы .vdex и .odex с памятью процесса, уменьшение таблиц символов напрямую сокращает как размер системного раздела, так и объем потребляемой памяти во время выполнения.
  • Более глубокая оптимизация всей программы. Если разрешить R8 переименовывать идентификаторы, это позволит объединять классы, выравнивать пакеты и дедуплицировать синтетические классы в пакетах, которые R8 в противном случае пришлось бы пропустить, чтобы избежать конфликтов имен.
  • Полная символизация трассировки стека. Включение обфускации в Soong не снижает возможности отладки. Для каждой цели, скомпилированной с помощью R8, Soong создает файл сопоставления proguard_dictionary в промежуточном каталоге модуля, объединяет все словари модулей в артефакт сборки proguard-dict.zip, встраивает хеш сопоставления (--map-id-template) в заголовок DEX и перезаписывает атрибуты SourceFile класса (--source-file-template), чтобы retrace мог автоматически добавлять символы в трассировки стека.

Сохранять obfuscate: false (или явно сохранять имена общедоступных API) только для:

  • Общие библиотеки в bootclasspath или system_server classpath. Модули (java_library или java_sdk_library), которые предоставляют внешний API, динамически связанный с другими модулями во время выполнения (например, цели framework.jar, services.jar или <uses-library>), должны либо сохранять obfuscate: false, либо явно сохранять свой API с помощью правил protect_api_surface: true, @KeepForApi или -keep. Это позволит вызывающим объектам, скомпилированным с использованием заглушек, разрешать имена классов и элементов во время выполнения. В модулях java_sdk_library настройка protect_api_surface: true автоматически генерирует и прикрепляет правила сохранения Metalava ProGuard ко всем включенным областям API (общедоступным, системным, module-lib, system-server и тестовым) в библиотеке реализации. Когда обобщенная оптимизация R8 включена для необновляемых целей bootclasspath (framework-minus-apex, ext и framework-location), они также включают framework-hiddenapi-proguard-rules (созданный hiddenapi_to_proguard), чтобы сохранить элементы, перечисленные в активных файлах boot/hiddenapi/*.txt (без hiddenapi-max-target-o.txt).

Прежде чем включать obfuscate: true в приложении, убедитесь, что выполнены следующие условия:

  • Зависимости внешних тестовых APK. Если внешний APK-файл android_test задает instrumentation_for: "MyApp" и напрямую вызывает внутренние классы или методы MyApp, переименование этих символов приводит к ошибке NoSuchMethodError или NoClassDefFoundError во время выполнения теста. Настройте trace_references_from (или allow_traced_references с trace_references: true), структурируйте тест как самоинструментируемый APK, статически связывающий MyApp.impl, или добавьте аннотации к тестовым хукам с помощью @VisibleForTesting (см. Шаг 2. Перенесите правила сохранения, управляемые тестами), чтобы внутренние символы, необходимые для тестов, сохранялись при обфускации остальной части приложения.
  • Рефлексия на основе строк и JNI. Если модуль ищет классы, методы или поля по строковым литералам (Class.forName, getDeclaredMethod или JNI FindClass и GetMethodID) без правил сохранения или аннотаций, обфускация переименовывает эти цели и нарушает поиск во время выполнения. (Обратите внимание, что неаннотированная рефлексия также не работает в shrink: true и optimize: true). Добавьте к этим точкам входа аннотации, следуя руководству по сохранению аннотаций (@UsesReflection, @UsedByReflection или @UsedByNative), чтобы R8 сохранил их имена и обфусцировал остальную часть модуля.

Принцип нулевого количества специальных пометок

В идеальном модуле платформы Android нет пользовательских файлов proguard.flags или keep.xml. В сборке платформы Android подавляющее большинство пользовательских правил сохранения избыточны:

  • Базовые правила платформы и AAPT2. Большинство точек входа уже автоматически сохраняются базовыми правилами платформы (например, @Keep, методы JNI native и @VisibleForTesting) или генерируются AAPT2 из AndroidManifest.xml и ресурсов макета (см. раздел Правила сохранения платформы по умолчанию).
  • Целевые альтернативы. Если точки входа необходимо сохранить, используйте аннотации на уровне объявления (keepanno, @VisibleForTesting), экспортированные правила библиотеки (export_proguard_flags_files: true) или статическое связывание тестов вместо отдельных файлов .flags (см. раздел Проверка и перенос существующих правил сохранения).

Прежде чем добавлять или сохранять собственный файл proguard.flags или keep.xml, проверьте, не обрабатывается ли правило базовыми конфигурациями по умолчанию и можно ли перенести его в аннотации кода.

Правила хранения по умолчанию для платформы

Soong автоматически передает следующие глобальные базовые правила каждому вызову R8 для целевых объектов компиляции DEX (android_app, android_test и устанавливаемые модули java_library), настроенных в build/soong/java/dex.go:

  • build/make/core/proguard.flags. Сохраняет классы и элементы, аннотированные с помощью @com.android.internal.annotations.VisibleForTesting во всех пакетах и с помощью @VisibleForTesting (androidx.annotation.VisibleForTesting или com.google.common.annotations.VisibleForTesting) в пакетах android.**, com.android.** и com.google.android.**. Также сохраняет: @TestApi, @Keep (androidx.annotation, android.support.annotation, com.android.internal.annotations), @KeepForWeakReference, @WeaklyReferencedCallback и @dalvik.annotation.optimization.**.
  • build/make/core/proguard_basic_keeps.flags: сохраняет атрибуты SourceFile для трассировки стека, аннотации видимости среды выполнения (RuntimeVisible*Annotations), атрибуты Exceptions и AnnotationDefault, методы native, элементы Serializable, методы @JavascriptInterface, конструкторы Throwable(String), поля Parcelable$CREATOR и поля protobuf MessageLite.
  • build/make/core/proguard/kotlin.flags. Отключает безвредные предупреждения для определенных метааннотаций Kotlin (kotlin.Metadata и kotlin.annotation.{AnnotationRetention,AnnotationTarget,Retention,Target}) и удаляет аннотации Kotlin DebugMetadata в конечных сборках.
  • build/make/core/proguard/checknotnull.flags. Заменяет распространенные вызовы вспомогательных функций для проверки на null (com.google.common.base.Preconditions.checkNotNull и dagger.internal.Preconditions.checkNotNull*) на краткие проверки на null в байт-коде. В этом файле намеренно отсутствует Objects.requireNonNull, чтобы сохранить явные сообщения об исключениях в границах API фреймворка.
  • build/make/core/proguard/enumvalues.flags. Сохраняет методы values и valueOf для типов enum, если они не отключены в конфигурации сборки.
  • Автоматически созданные правила AAPT2. AAPT2 проверяет объединенные файлы AndroidManifest.xml, XML-файлы макетов и XML-файлы настроек, чтобы создать точные правила сохранения для каждого зарегистрированного класса Activity, Service, BroadcastReceiver, ContentProvider, BackupAgent, Application, пользовательского подкласса View, подкласса Preference и метода XML android:onClick.

Проверьте и перенесите существующие правила хранения

При аудите существующих файлов proguard.flags или keep.xml в репозитории платформы оценивайте каждое правило по порядку в соответствии со следующей четырехуровневой иерархией:

Шаг 1. Удалите лишние или устаревшие правила

Удалите правила, которые уже охвачены глобальными базовыми показателями, манифестом AAPT2 и правилами макета или настройками R8 по умолчанию, а также правила, ссылающиеся на классы или пакеты, которых больше не существует.

Если все правила в файле .flags избыточны, удалите его и уберите proguard_flags_files из Android.bp. Если оставшийся блок optimize только повторяет настройки по умолчанию, удалите его и отформатируйте файл сборки с помощью bpfmt -w Android.bp. При очистке пакета проверьте, ссылаются ли сопутствующие модули в подкаталогах (например, целевые варианты Kotlin) на один и тот же файл флагов, и обновите их вместе.

Шаг 2. Перенесите правила хранения, созданные в тестовом режиме

Перенесите правила сохранения, основанные на тестах, из рабочих файлов .flags, используя один из следующих шаблонов:

  • Отслеживание ссылок из тестовых или сопутствующих целей (trace_references_from или allow_traced_references). Если цель и напрямую зависимый модуль компилируют собственный DEX (android_app, android_test или java_library с DEX) и связаны через instrumentation_for или libs, Soong может запустить R8 tracereferences в зависимом модуле и передать сгенерированные правила сохранения в компиляцию R8 цели, не создавая цикл зависимостей и не требуя разделения библиотеки .impl вручную.

    • Тесты в том же хранилище (trace_references_from). Перечислите прямые зависимости instrumentation_for или libs от целевого модуля. Это также автоматически распространяется на варианты override_android_app и override_android_test и продолжает поддерживать независимые сопутствующие библиотеки Java в паре с разделением .impl.

      android_app {
          name: "MyApp",
          srcs: ["src/**/*.java"],
          optimize: {
              obfuscate: true,
              shrink_resources: true,
              trace_references_from: ["MyAppTests"],
          },
      }
      
      android_test {
          name: "MyAppTests",
          instrumentation_for: "MyApp",
          srcs: ["tests/**/*.java"],
      }
      
    • Тесты, относящиеся к разным хранилищам или поставщикам (allow_traced_references и trace_references). Если целевой модуль не может указать зависимый модуль непосредственно в trace_references_from (например, тесты находятся в другом хранилище или дереве поставщика), задайте allow_traced_references: true в целевом модуле и включите его из каждого зависимого модуля с помощью trace_references: true:

      android_app {
          name: "MyApp",
          srcs: ["src/**/*.java"],
          optimize: {
              allow_traced_references: true,
          },
      }
      
      android_test {
          name: "MyAppVendorTests",
          instrumentation_for: "MyApp",
          srcs: ["tests/**/*.java"],
          optimize: {
              trace_references: true,
          },
      }
      
    • Рефлексия на основе строк в тестах. tracereferences проверяет ссылки на пул констант байт-кода и не может обнаружить рефлексию на основе строк в тестах (Class.forName, getDeclaredField, getDeclaredMethod или вспомогательные средства внедрения через поле). Добавьте аннотацию @VisibleForTesting (или @UsedByReflection) к полям и методам рабочей версии, к которым осуществляется доступ через рефлексию.

  • Свяжите библиотеку реализации с тестами (static_libs). Если вы не хотите, чтобы в рабочем APK-файле оставались точки входа, предназначенные только для тестирования, и чтобы изменения в тестах повторно запускали R8 для рабочей цели, поместите исходные файлы приложения в android_library (MyApp.impl) и статически свяжите MyApp.impl с MyApp и MyAppTests.

    android_library {
        name: "MyApp.impl",
        srcs: ["src/**/*.java"],
        manifest: "AndroidManifest.xml",
    }
    
    android_app {
        name: "MyApp",
        static_libs: ["MyApp.impl"],
        optimize: {
            obfuscate: true,
            shrink_resources: true,
        },
    }
    
    android_test {
        name: "MyAppTests",
        srcs: ["tests/**/*.java"],
        static_libs: ["MyApp.impl"],
    }
    

    Такая схема из трех модулей позволяет запускать MyAppTests как самоинструментируемый тест с полным доступом к внутренним классам, а MyApp – сжимать и обфусцировать только точки входа рабочей версии.

  • Добавьте к тестовым хукам аннотацию @VisibleForTesting. Если внешний тестовый APK-файл вызывает только несколько внутренних методов или конструкторов в целевом объекте android_app, вы можете добавить к этим объявлениям аннотацию @VisibleForTesting. Глобальный базовый план build/make/core/proguard.flags автоматически сохраняет @VisibleForTesting объектов в пакетах android.**, com.android.** и com.google.android.** без специальных файлов .flags. Для модулей поставщиков за пределами этих пространств имен используйте правила trace_references_from, @UsedByReflection или экспортируемой библиотеки. Если вы вручную добавляете аннотации @VisibleForTesting вместо trace_references_from или trace_references, также добавляйте аннотации к классам, переданным в Mockito mock(...) или spy(...) (а не только к методам-заглушкам), потому что mockito-target (dexmaker-mockito) на устройстве генерирует подклассы среды выполнения, а R8 может помечать классы без подклассов как final (MockitoException: Can't mock/spy class ... - final class).

  • Избегайте пересекающихся статических библиотек или перепаковывайте их (jarjar_rules). Если и android_app, и тестовый APK-файл instrumentation_for статически связывают одну и ту же библиотеку (напрямую, например guava, или транзитивно), tracereferences видит эти классы, определенные в тесте, и не выдает правил сохранения для приложения. Во время выполнения родительский загрузчик классов приложения затеняет копию теста своими классами, сжатыми с помощью R8, что приводит к ошибке IllegalAccessError или NoSuchMethodError. Если для теста не требуется собственная копия, удалите дубликат из static_libs теста (или свяжите его через libs), чтобы избежать конфликта. В противном случае перепакуйте пересекающиеся пакеты статических библиотек приложения с префиксом модуля, используя jarjar_rules.

Шаг 3. Экспортируйте правила из библиотеки-владельца

Если для общей библиотеки java_library или android_library требуются правила сохранения для собственной внутренней рефлексии или обратных вызовов JNI, определите правила для целевой библиотеки и задайте export_proguard_flags_files: true:

java_library {
    name: "my-shared-library",
    srcs: ["src/**/*.java"],
    optimize: {
        proguard_flags_files: ["proguard.flags"],
        export_proguard_flags_files: true,
    },
}

Все последующие цели android_app и библиотеки, которые статически связывают my-shared-library, автоматически наследуют эти правила, поэтому последующим приложениям не нужно их дублировать. Если несколько модулей в разных каталогах должны использовать один набор правил, не привязанный к одной библиотеке кода, опубликуйте файл .flags с помощью явного filegroup в Android.bp, чтобы модули могли ссылаться на :my-shared-flags за пределами пакета. Общие рекомендации по созданию правил сохранения для потребителей библиотек, не ограничивающих оптимизацию приложений, приведены в разделе Оптимизация для авторов библиотек.

Шаг 4. Добавьте аннотации к объявлениям в исходном коде

Если доступ к классу, методу, конструктору или полю осуществляется через рефлексию или JNI и не охватывается AAPT2 или глобальными базовыми показателями, замените отдельные правила -keep в файлах .flags аннотациями источника из руководства по сохранению аннотаций (см. справочник по Javadoc для keepanno):

  • @UsesReflection. Используйте эту аннотацию, если вы управляете кодом, выполняющим рефлексию. Разместите ее в месте вызова рефлексии, чтобы объявить, к каким целевым классам, методам или полям осуществляется динамический доступ. Поскольку @UsesReflection автоматически кодирует предварительное условие, согласно которому само аннотированное место вызова доступно, R8 удаляет как вызывающий объект, так и целевой объект отражения, если место вызова не используется.
  • @UsedByReflection. Используйте для классов, методов, полей или конструкторов, которые создаются или вызываются рефлексивно внешним кодом или библиотеками, например для классов, загруженных с помощью Class.forName из ключей Bundle или Settings, или классов плагинов, загруженных через динамические загрузчики классов. Укажите kind (например, KeepItemKind.CLASS_AND_METHODS), preconditions (@KeepCondition) и ограничения параметров, чтобы R8 сохранял только точный контракт рефлексии и мог оптимизировать или удалять неиспользуемые элементы класса. Не добавляйте @UsedByReflection в компоненты, зарегистрированные в манифесте, такие как JobService или BroadcastReceiver, потому что AAPT2 уже сохраняет их.
  • @UsedByNative – применяется к методам или полям, к которым обращается код JNI на C или C++ с помощью GetMethodID, GetStaticMethodID или GetFieldID.
  • @KeepForApi. Применяется к классам или элементам API библиотеки, которые должны оставаться неизменными, когда библиотека сжимается перед распространением.
  • @Keep (androidx.annotation.Keep): используйте как резервный вариант, если keepanno не подходит. Применение @Keep к классу сохраняет класс и все его элементы без условий. Применение @Keep к методу или полю действует как безусловная точка входа, которая сохраняет элемент и содержащий его класс, даже если экземпляр класса никогда не создается, тогда как keepanno выражает условную доступность.

Чтобы использовать аннотации R8 keepanno в модуле Soong:

  1. Добавить "keepanno-annotations" в libs (Android.bp):

    libs: [
        "keepanno-annotations",
    ],
    
  2. Импортируйте com.android.tools.r8.keepanno.annotations.* и добавьте аннотацию к объявлению или месту вызова в исходном коде Java или Kotlin:

    import com.android.tools.r8.keepanno.annotations.KeepItemKind;
    import com.android.tools.r8.keepanno.annotations.UsedByReflection;
    
    public final class CustomPluginController {
        @UsedByReflection(
            description = "Instantiated via Class.forName from plugin config",
            kind = KeepItemKind.CLASS_AND_METHODS)
        public CustomPluginController(Context context) {
            // ...
        }
    }
    

    R8 преобразует аннотации keepanno непосредственно во внутреннюю модель правил сохранения и удаляет аннотации из конечного вывода DEX, не добавляя накладных расходов на байт-код во время выполнения.

Как избежать распространенных ошибок

В следующих разделах описаны распространенные ошибки, связанные с proguard.flags и keep.xml в коде платформы, и способы их устранения.

Общие правила сохранения компонентов

# Don't do this:
-keep class * extends android.app.Activity
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
  • Почему это снижает производительность. Это правило полностью дублирует AAPT2. Поскольку в нем используется подстановочный знак без проверки того, зарегистрирован ли компонент в окончательном объединенном файле AndroidManifest.xml, R8 сохраняет каждый подкласс Activity, Service или BroadcastReceiver, найденный в пути к классам, включая неиспользуемые компоненты библиотеки и отключенные действия отладки.
  • Рекомендуемое решение. Удалите правило. AAPT2 проверяет объединенный манифест и генерирует точные правила -keep для зарегистрированных компонентов.

Правила сохранения обработчика кликов Broad XML

# Don't do this:
-keepclassmembers class * {
    public void *(android.view.View);
}
  • Почему это снижает производительность. Сохранение каждого метода public void *(View) во всех классах модуля не позволяет R8 удалять или встраивать методы, которые принимают один параметр View.
  • Рекомендуемое решение. Удалите правило. AAPT2 сканирует XML-файлы макета и генерирует целевые правила сохранения для методов, на которые ссылаются атрибуты android:onClick.

Правила сохранения методов получения и установки Broad View

# Don't do this:
-keep public class * extends android.view.View {
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    public void set*(...);
    public *** get*();
}
  • Почему это снижает производительность. Это правило заставляет R8 сохранять все методы получения и установки для каждого подкласса View в приложении и всех связанных библиотеках (включая библиотеки AndroidX и Material), блокируя удаление неиспользуемых методов и встраивание в код пользовательского интерфейса.
  • Рекомендуемое решение. Удалите правило. AAPT2 уже сохраняет конструкторы для пользовательских классов View, созданных из XML-файлов макета. Если код анимирует свойство представления, используя рефлексивные строковые имена, например ObjectAnimator.ofFloat(view, "translationZ", ...), замените строковое имя ссылкой на типизированное свойство (View.TRANSLATION_Z или пользовательская реализация FloatProperty или IntProperty), чтобы полностью избежать рефлексии. Если избежать рефлексивного доступа к свойству невозможно, добавьте к соответствующему методу получения или установки аннотацию @UsedByReflection.

Полное отключение оптимизации или обфускации

# Don't do this in proguard.flags:
-dontoptimize
-dontshrink
-dontobfuscate
  • Почему это снижает производительность. Размещение -dontoptimize или -dontshrink внутри файла .flags автоматически переопределяет настройки Android.bp модуля и отключает оптимизацию для всей цели. Хуже того, если библиотека экспортирует файл .flags, содержащий -dontoptimize, это отключает оптимизацию R8 для каждого последующего android_app, который ссылается на библиотеку.
  • Рекомендуемое исправление. Удалите -dontoptimize, -dontshrink и -dontobfuscate из файлов .flags. Явно задайте поведение оптимизации в Android.bp, используя блок optimize (shrink, optimize, obfuscate) в конечной цели.

Флаги диагностики в зафиксированных правилах

# Don't do this in proguard.flags:
-verbose
-printmapping proguard.map
-printusage usage.txt
-printconfiguration config.txt
  • Почему это мешает сборке. Флаги диагностики переполняют журналы сборки или пытаются записать выходные файлы в локальные пути во время изолированных сборок Soong.
  • Рекомендуемое исправление. Удалите эти флаги из зафиксированных файлов .flags. Soong автоматически записывает выходные данные сопоставления и использования R8, такие как proguard_dictionary и proguard_usage.zip, в промежуточный каталог модуля в out/soong/.intermediates/.

Правила сохранения рабочей версии для тестового кода

  • Почему это снижает производительность. Если вы добавите специальные правила -keep только для доступа к тестам, код останется необфусцированным и будет сохранен в рабочих сборках. Настройка trace_references_from (или allow_traced_references с trace_references: true) или аннотирование методов с помощью @VisibleForTesting позволяет избежать поддержки правил -keep вручную и сохранить только то, на что ссылаются тесты. Однако эти символы по-прежнему поставляются в рабочем двоичном файле.
  • Рекомендуемое исправление. Удалите специальные правила -keep для инструментального теста, включив trace_references_from (или allow_traced_references и trace_references: true), или структурируйте приложение в библиотеку .impl, связанную с самоинструментируемым тестовым APK-файлом, используя static_libs, чтобы полностью исключить точки входа, предназначенные только для тестирования, из рабочего APK-файла, как описано в разделе Шаг 2. Перенесите правила сохранения, управляемые тестированием.

Общие подстановочные знаки ресурсов в файле keep.xml

<!-- Don't do this in res/raw/keep.xml: -->
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@raw/*,@drawable/*,@string/*" />
  • Почему это снижает производительность. Подстановочные знаки сохраняют все ресурсы соответствующего типа в модуле и его зависимостях, что мешает удалению неиспользуемых ресурсов (shrink_resources: true) и увеличивает размер APK и сопоставленной таблицы resources.arsc.
  • Рекомендуемое исправление:
    • Используйте ссылки на статические ресурсы вместо keep.xml. Не применяйте метод Resources.getIdentifier для динамического поиска ограниченного набора ресурсов, например пронумерованных строк эксперимента или тематических графических объектов. Вместо этого используйте оператор switch времени компиляции или сопоставьте со статическими константами R.id, R.string или R.drawable. Статические ссылки позволяют R8 и AAPT2 отслеживать точные активные ресурсы, устранять накладные расходы на поиск строк во время выполнения и полностью исключать необходимость в keep.xml.
    • Укажите названия конкретных ресурсов. Если требуется динамический поиск, например сторонним средством чтения лицензий, укажите точные идентификаторы ресурсов в tools:keep (например, tools:keep="@raw/third_party_licenses").
    • Удалите лишние файлы keep.xml. Если на перечисленные ресурсы уже есть статические ссылки в коде (R.raw.foo) или XML (@raw/foo), инструмент сжатия сохранит их автоматически, поэтому вы можете удалить res/raw/keep.xml.

Как проверить и проанализировать изменения правил

Каждый раз, когда вы удаляете или сужаете правила сохранения в proguard.flags или keep.xml, убедитесь, что модуль собирается без ошибок, проходит модульные и инструментальные тесты и сохраняет все необходимые точки входа.

Создание и тестирование модулей

  1. Выполните чистую сборку модуля, чтобы убедиться, что R8 и AAPT2 завершают работу без предупреждений об отсутствующих ссылках:

    m <MODULE_NAME>
    
  2. Запустите модульные и инструментальные тесты для модуля с помощью atest:

    atest <TEST_MODULE_NAME>
    
  3. Для системных приложений, привилегированных сервисов и модулей, зависящих от аппаратного обеспечения, запустите инструментальные тесты и тесты пользовательского интерфейса на целевых физических устройствах или в лаборатории тестирования устройств. Это позволит проверить пути рефлексии во время выполнения, привязки IPC и инфляцию ресурсов.

Просматривайте различия в DEX-файлах и ресурсах.

Сравните скомпилированный APK- или JAR-файл до и после изменения правил, чтобы убедиться, что R8 удаляет неиспользуемый код и ресурсы, не затрагивая ожидаемые точки входа.

  • Чтобы проверить, какие классы, методы и поля остались в выходных файлах APK или DEX, используйте apkanalyzer или dexdump.

    apkanalyzer dex packages $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • При изменении keep.xml или включении shrink_resources используйте aapt2 dump resources, чтобы убедиться, что сборка удаляет неиспользуемые ресурсы и сохраняет необходимые:

    aapt2 dump resources $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • При запуске prebuilts/r8/r8.jar непосредственно в файле classes.jar модуля до R8 для проверки изменений флагов в офлайн-режиме без полной сборки платформы передайте JAR-файлы пути к классам --lib модуля (android.jar, framework.jar и общий libs) и правила proguard.options, сгенерированные AAPT2, из AndroidManifest.xml и XML-файлов макета. Без этих входных данных R8 не может разрешить супертипы фреймворка (Activity, Service, Phone, Handler) и недостаточно сохраняет методы иерархии.

Анализ радиуса сохранения и поглощения правил с помощью R8

В компилятор R8 с открытым исходным кодом встроен анализатор Keep Radius (в Android Studio и Gradle он называется анализатором конфигурации R8), который оценивает точное влияние каждого правила сохранения во время компиляции. Soong интегрирует этот анализатор непосредственно в сборку платформы Android (настраивается в build/soong/java/dex.go).

Чтобы проанализировать правила сохранения для модуля платформы или всей сборки:

  1. Запустите сборку с переменной среды R8_DUMP_KEEP_RADIUS=true:

    R8_DUMP_KEEP_RADIUS=true m <MODULE_NAME>
    

    Если задано значение R8_DUMP_KEEP_RADIUS=true, Soong указывает R8 записывать показатели правил сохранения в промежуточный файл r8keepradius.pb для каждого скомпилированного модуля в out/soong/.intermediates/. Для каждого правила сохранения и аннотации keepanno R8 записывает:

    • Непосредственный радиус сохранения. Точные классы, поля и методы, сохраняемые правилом, а также конкретные ограничения, которые оно накладывает на сжатие, оптимизацию или обфускацию.
    • Поглощение правил. Какие другие правила хранения или аннотации уже применяются к тем же объектам. Если пользовательское правило полностью заменяется правилом AAPT2 или глобальным базовым правилом, его можно удалить.
    • Правила, действующие на уровне пакета или глобально. Правила, в которых используются подстановочные знаки на уровне пакета или глобальные директивы конфигурации.
  2. Преобразуйте выходные данные r8keepradius.pb в интерактивный HTML-отчет с помощью KeepRadiusHtmlReportGenerator (входит в состав prebuilts/r8/r8.jar):

    # Generate an HTML report for a single module
    INTERMEDIATES=out/soong/.intermediates/packages/apps/<APP_NAME>/<APP_NAME>
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        $INTERMEDIATES/android_common/r8keepradius.pb \
        keep_radius_report.html
    
    # Scan all built modules under out/soong/.intermediates and generate
    # per-module HTML reports plus an aggregate keepradius.html summary
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        out/soong/.intermediates \
        out/keep_radius_reports
    

    Если указан каталог, KeepRadiusHtmlReportGenerator обходит все файлы *keepradius*.pb, создает отчет в формате HTML для каждого модуля и файл out/keep_radius_reports/keepradius.html, в котором суммируется количество активных и сохраненных элементов, а также правила сохранения с наибольшим радиусом для всей сборки. Подробнее о том, как интерпретировать оценки сжатия, оптимизации и обфускации в созданном отчете…

Soong также поддерживает дополнительные переменные среды для диагностики R8 и внутреннего цикла в build/soong/java/dex.go:

  • R8_DUMP_INPUT=true – записывает r8inputs.zip в промежуточный каталог модуля, содержащий все входные JAR-файлы, JAR-файлы библиотек и объединенные конфигурации ProGuard для автономного воспроизведения R8.
  • R8_DUMP_PERFETTO_TRACE=true – записывает r8trace.ptrace в промежуточный каталог модуля для проверки проходов компиляции R8 в Perfetto.
  • SOONG_SKIP_TRACE_REFERENCES=true – пропускает выполнение правил tracereferences, настроенных с помощью trace_references_from или trace_references, чтобы ускорить локальные сборки внутреннего цикла при итерации тестового кода.

Проверка правил для потребителей библиотеки

Когда java_library или android_library экспортирует правила сохранения в последующие потребители (export_proguard_flags_files: true), эти правила не должны включать глобальные флаги, которые изменяют или отключают оптимизацию для приложения-потребителя.

В R8 есть анализатор правил сохранения с открытым исходным кодом (ProcessKeepRules), который представлен в сборке платформы Android как инструмент хоста process-keep-rules (определен в prebuilts/r8/Android.bp):

# Build the R8 keep rules validator host binary
m process-keep-rules

# Validate one or more ProGuard configuration files
out/host/linux-x86/bin/process-keep-rules <PROGUARD_FLAGS_PATH>

Инструмент process-keep-rules анализирует каждый файл конфигурации и сообщает об ошибке, если обнаруживает директивы, запрещенные в правилах для потребителей библиотек. К запрещенным директивам относятся глобальная оптимизация, сжатие, отключение обфускации (например, -dontoptimize или -dontshrink), перепаковка пакетов, флаги изменения доступа, флаги диагностики или сопоставления, а также -keepattributes на уровне приложения.

Как проверить деревья источников с помощью pgaudit.py

Чтобы проверять каталоги с исходными файлами, которые не были собраны, вместе с анализаторами времени сборки R8, в дерево платформы Android включен скрипт pgaudit.py, который находится в каталоге build/make/core/proguard/tools/. Этот инструмент статического анализа сканирует файлы Android.bp, proguard.flags и keep.xml в репозитории, чтобы выявить правила, которые уже охвачены AAPT2 или базовыми показателями глобальной платформы, широкие подстановочные знаки, переопределения -dontoptimize и правила сохранения только для тестирования.

# Audit a single package directory
./build/make/core/proguard/tools/pgaudit.py packages/apps/Provision/

# Scan a subsystem tree and write a structured JSON report
./build/make/core/proguard/tools/pgaudit.py packages/ --json=audit_report.json

Команды разработчиков платформы и OEM-производителей могут использовать pgaudit.py для быстрой сортировки дерева исходного кода, process-keep-rules для проверки экспортированных правил библиотеки и R8_DUMP_KEEP_RADIUS=true с KeepRadiusHtmlReportGenerator для измерения точного радиуса удержания класса, поля и метода для каждого правила в сборке.