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

Система сборки платформы Android (Soong) запускает компилятор R8 для целей, связанных с компиляцией байт-кода DEX, включая приложения Android (android_app), тесты (android_test) и устанавливаемые библиотеки Java с compile_dex: true (например, services.jar), чтобы уменьшить размер, оптимизировать и удалить неиспользуемый код и ресурсы. В случае со статическими библиотеками (android_library и static 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 выполняться в целевом объекте. Значение по умолчанию – true для всех целевых значений android_app.
shrink Управляет удалением недостижимых классов, полей и методов. По умолчанию для целей android_app используется значение true (false для отдельных модулей java_library и тестовых модулей, которые передают -dontshrink, если не задано иное).
optimize Управляет оптимизацией байт-кода, например встраиванием методов, объединением классов, распространением констант и удалением неиспользуемых ветвей. По умолчанию для целевых объектов android_app используется значение true (управляется флагом сборки 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 Содержит список целевых объектов, связанных с библиотекой Java, байт-код которых 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 с помощью правил @KeepForApi или -keep (а также protect_api_surface: true для целевых объектов bootclasspath), чтобы вызывающие объекты, скомпилированные с использованием их заглушек, могли разрешать имена классов и членов во время выполнения.

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

  • Внешние зависимости тестового APK. Если внешний APK-файл android_test задает instrumentation_for: "MyApp" и напрямую вызывает внутренние классы или методы MyApp, переименование этих символов приводит к ошибке NoSuchMethodError или NoClassDefFoundError во время выполнения теста. Рекомендуем создать самоинструментируемый APK, статически связав MyApp.impl, добавить к точкам тестирования аннотации @VisibleForTesting или настроить trace_references_from (см. шаг 2: перенос правил сохранения, управляемых тестами), чтобы внутренние символы, необходимые для тестов, сохранялись, а остальная часть приложения была обфусцирована.
  • Отражение на основе строк и JNI. Если модуль ищет классы, методы или поля по строковым именам (Class.forName, getDeclaredMethod или JNI FindClass и GetMethodID) без правил сохранения или аннотаций, обфускация переименовывает эти объекты и нарушает поиск во время выполнения. (Обратите внимание, что рефлексия без аннотаций также не работает в shrink: true и optimize: true.) Добавьте к этим точкам входа аннотации Guide to Keep Annotations (@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, используя один из следующих шаблонов:

  • Добавьте библиотеку реализации в тесты (static_libs). Если android_test использует закрытые или внутренние детали реализации приложения, рекомендуется разместить исходные файлы приложения в 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 свободно включать obfuscate: true без нарушения работы тестов, позволяет избежать включения точек входа, предназначенных только для тестирования, в производственный APK и устраняет необходимость перестраивать MyApp при изменении тестового кода.

  • Добавьте аннотацию @VisibleForTesting к тестовым точкам. Если внешний тестовый APK вызывает небольшое количество внутренних методов или конструкторов в целевом объекте android_app и реструктуризация в библиотеку .impl нецелесообразна, добавьте аннотацию @VisibleForTesting к этим объявлениям. Глобальный базовый план build/make/core/proguard.flagsсохраняет@VisibleForTesting объекты в пакетах android.**, com.android.** и com.google.android.** автоматически без специальных файлов .flags. (Для модулей поставщиков вне этих пространств имен используйте @UsedByReflection или правила экспортируемой библиотеки.)

  • Используйте trace_references_from для разных библиотек платформы. Если тесты не могут статически связать целевую реализацию (например, тесты, использующие сервисы системного сервера или JAR-файлы платформы, такие как framework-connectivity), настройте trace_references_from в целевом модуле, указав на дополнительный модуль библиотеки Java, содержащий исходный код теста. R8 отслеживает и сохраняет все классы и элементы, на которые ссылается дополнительный байт-код.

Шаг 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 аннотациями источника из руководства по аннотациям Keep (см. справочник 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 правила для зарегистрированных компонентов.

Правила для обработчика кликов XML с широким соответствием

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

Правила для геттеров и сеттеров в широком представлении

# 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 только для тестирования, код останется незапутанным и будет сохранен в производственных сборках. Хотя аннотирование методов с помощью @VisibleForTesting или настройка trace_references_from позволяет избежать ручного управления правилами -keep, эти символы все равно будут присутствовать в производственном двоичном файле.
  • Рекомендуемое исправление. Чтобы точки входа, предназначенные только для тестирования, не попадали в производственный APK, разделите приложение на библиотеку .impl и свяжите ее с самоинструментирующим тестовым APK с помощью static_libs, как описано в шаге 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 для динамического поиска ограниченного набора ресурсов, например нумерованных строк эксперимента или тематических объектов drawable. Вместо этого используйте оператор 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. Для системных приложений, привилегированных сервисов или модулей, зависящих от оборудования, запустите инструментальные и UI-тесты на целевых физических устройствах или в тестовой лаборатории, чтобы проверить пути отражения во время выполнения, привязки IPC и развертывание ресурсов.

Проверка различий в файлах DEX и ресурсах

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

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

    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
    

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

Компилятор R8 с открытым исходным кодом включает анализатор радиуса сохранения (также доступный в 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 записывать показатели правила keep в промежуточный файл 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 с информацией о количестве активных объектов, количестве сохраненных объектов и правилах хранения с наибольшим радиусом для всей сборки. Подробнее о том, как интерпретировать оценки сжатия, оптимизации и обфускации в сгенерированном отчете, рассказывается в статье Как использовать анализатор конфигурации R8.

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.

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

Когда при экспорте 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 для измерения точного радиуса сохранения класса, поля и метода для каждого правила в сборке.