Новости о продуктах

Настройка и устранение неполадок правил сохранения R8

7 минут чтения
Ajesh Pai и Ben Weiss

В современной разработке под Android выпуск небольшого, быстрого и безопасного приложения является фундаментальным ожиданием пользователей. Основным инструментом системы сборки Android для достижения этой цели является оптимизатор R8 — компилятор, который обрабатывает неиспользуемый код и удаляет ресурсы для уменьшения размера, переименования или минификации кода, а также оптимизации приложения.

Включение R8 — критически важный шаг в подготовке приложения к выпуску, но он требует от разработчиков предоставления рекомендаций в виде «Правил сохранения».

После прочтения этой статьи посмотрите видеоролик Performance Spotlight Week на YouTube, посвященный включению, отладке и устранению неполадок оптимизатора R8.

Зачем нужны правила?

Необходимость написания правил сохранения обусловлена ​​ключевым конфликтом: R8 — это инструмент статического анализа, но приложения для Android часто полагаются на динамические шаблоны выполнения, такие как рефлексия или вызовы в нативный код и из него с использованием JNI (Java Native Interface).

R8 строит граф используемого кода, анализируя прямые вызовы. Когда доступ к коду осуществляется динамически, статический анализ R8 не может это предсказать и идентифицирует этот код как неиспользуемый , удаляя его, что приводит к сбоям во время выполнения.

Правило сохранения (keep rule) — это явная инструкция для компилятора R8, гласящая: «Этот конкретный класс, метод или поле является точкой входа, к которой будет осуществляться динамический доступ во время выполнения. Вы должны сохранить его, даже если не можете найти прямую ссылку на него».

Более подробную информацию о правилах игры Keep Rules можно найти в официальном руководстве.

Где написать правила хранения?

Пользовательские правила сохранения для приложения записываются в текстовый файл. По соглашению, этот файл называется proguard-rules.pro и находится в корневом каталоге модуля приложения или библиотеки. Затем этот файл указывается в файле build.gradle.kts вашего модуля в поле типа сборки release .

release {

    isShrinkResources = true

    isMinifyEnabled = true

    proguardFiles(

        getDefaultProguardFile("proguard-android-optimize.txt"),

        "proguard-rules.pro",

    )

}

Используйте правильный файл по умолчанию.

Метод getDefaultProguardFile импортирует набор правил по умолчанию, предоставляемый Android SDK. При использовании неправильного файла ваше приложение может быть не оптимизировано. Убедитесь, что вы используете proguard-android-optimize.txt . Этот файл предоставляет правила сохранения по умолчанию для стандартных компонентов Android и включает оптимизацию кода R8 . Устаревший файл proguard-android.txt предоставляет только правила сохранения, но не включает оптимизацию R8.

progaurd.png

Поскольку это серьёзная проблема с производительностью, мы начинаем предупреждать разработчиков об использовании неправильного файла, начиная с обновления Android Studio Narwhal 3 Feature Drop. А начиная с версии плагина Android Gradle 9.0 мы больше не поддерживаем устаревший файл proguard-android.txt . Поэтому обязательно обновитесь до оптимизированной версии.

Как писать правила Keep

Правило сохранения состоит из трех основных частей:

  1. Опция , например, -keep или -keepclassmembers
  2. Дополнительные модификаторы, такие как allowshrinking
  3. Спецификация класса , определяющая соответствующий код.

Полный синтаксис и примеры см. в руководстве по добавлению правил сохранения .

Антипаттерны Keep Rule

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

Глобальные опции

Эти флаги являются глобальными переключателями, которые никогда не следует использовать в релизной сборке. Они предназначены только для временной отладки с целью локализации проблемы.

Использование -dontotptimize фактически отключает оптимизации производительности R8, что приводит к замедлению работы приложения.

Использование -dontobfuscate отключает все переименования, а использование -dontshrink отключает удаление мертвого кода. Оба этих глобальных правила увеличивают размер приложения.

По возможности избегайте использования этих глобальных флагов в производственной среде для повышения производительности и удобства использования приложения.

Слишком общие правила хранения

Самый простой способ свести на нет преимущества R8 — это написать слишком общие правила сохранения (Keep Rules) . Правила сохранения, подобные приведенному ниже, указывают оптимизатору R8 не уменьшать размер, не обфусцировать и не оптимизировать ни один класс в этом пакете или любом из его подпакетов. Это полностью сводит на нет преимущества R8 для всего этого пакета. Попробуйте вместо этого написать узкие и конкретные правила сохранения.

-keep class com.example.package.** { *;} // WIDE KEEP RULES CAUSE PROBLEMS

Оператор инверсии (!)

Оператор инверсии (!) кажется мощным способом исключить пакет из правила. Но всё не так просто. Рассмотрим следующий пример:

-keep class !com.example.my_package.** { *; } // USE WITH CAUTION

Вы можете подумать, что это правило означает « не храните классы в com.example.package ». Но на самом деле оно означает « храните каждый класс, метод и свойство».   во всем приложении, которое не находится в com.example.package ." Если это стало для вас неожиданностью, лучше всего проверить наличие отрицаний в вашей конфигурации R8.

Избыточные правила для компонентов Android

Ещё одна распространённая ошибка — добавление правил сохранения (Keep Rules) вручную для Activities , Services или BroadcastReceivers вашего приложения. Это излишне . Файл proguard-android-optimize.txt по умолчанию уже содержит необходимые правила для работы этих стандартных компонентов Android без дополнительных настроек.

Кроме того, многие библиотеки используют собственные правила хранения данных. Поэтому вам не нужно будет писать для них свои собственные правила. В случае возникновения проблем с правилами хранения данных из используемой вами библиотеки, лучше всего обратиться к автору правил в этой библиотеке, чтобы выяснить, в чем проблема.

Рекомендации по соблюдению правил

Теперь, когда вы знаете, чего делать не следует, давайте поговорим о передовых методах.

Напишите узкие правила соблюдения.

Правильные правила сохранения должны быть максимально узкими и конкретными . Они должны сохранять только то, что необходимо, позволяя R8 оптимизировать все остальное.

Правило Качество

-keep class com.example.** { ; }

Низкий уровень: сохраняет весь пакет и его подпакеты.

-keep class com.example.MyClass { ; }

Низкий уровень: Сохраняет целый класс, который, вероятно, все еще слишком широк.
-keepclassmembers class com.example.MyClass {

    private java.lang.String secretMessage;

    public void onNativeEvent(java.lang.String);

}
Высокий уровень: Сохраняются только релевантные методы и свойства из конкретного класса.

Используйте общих предков

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

# Keep all fields of any class that implements SerializableModel

-keepclassmembers class * implements com.example.models.SerializableModel {

    <fields>;

}

Используйте аннотации для выбора нескольких классов.

Создайте собственную аннотацию (например, @Serialize ) и используйте её для "пометки" классов, поля которых необходимо сохранить. Это ещё один чистый, декларативный и масштабируемый шаблон. Вы также можете создавать правила сохранения для уже существующих аннотаций из используемых вами фреймворков.

# Keep all fields of any class annotated with @Serialize

-keepclassmembers class * {

    @com.example.annotations.Serialize <fields>;

}

Выберите правильный вариант сохранения.

Параметр «Сохранить» — наиболее важная часть правила. Выбор неправильного параметра может без необходимости отключить оптимизацию.

Вариант "Сохранить" Что это делает
-keep Предотвращает удаление или переименование класса и его членов, упомянутых в объявлении .
-keepclassmembers Предотвращает удаление или переименование указанных членов , но позволяет удалить сам класс, однако только для тех классов, которые не удаляются иным способом.
-keepclasseswithmembers Комбинация: Сохраняет класс и его члены только в том случае, если присутствуют все указанные члены.

Более подробную информацию о параметре «Сохранить» вы найдете в нашей документации по параметрам «Сохранить».

Разрешить оптимизацию с помощью модификаторов

Модификаторы, такие как allowshrinking и allowobfuscation ослабляют широкое правило -keep , возвращая возможности оптимизации в R8. Например, если устаревшая библиотека заставляет вас использовать -keep для всего класса, вы можете вернуть себе часть возможностей оптимизации, разрешив сжатие и обфускацию:

# Keep this class, but allow R8 to remove it if it's unused and allow R8 to rename it.

-keep,allowshrinking,allowobfuscation class com.example.LegacyClass

Добавьте глобальные параметры для дополнительной оптимизации.

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

Опция -repackageclasses — это мощный инструмент, который указывает R8 переместить все обфусцированные классы в один пакет. Это значительно экономит место в DEX-файле за счет удаления избыточных строк с именами пакетов.

-allowaccessmodification позволяет R8 расширить доступ (например, private на public ), чтобы обеспечить более агрессивное встраивание. Теперь это включено по умолчанию при использовании proguard-android-optimize.txt .

Предупреждение: Авторам библиотек ни в коем случае нельзя добавлять эти глобальные флаги оптимизации в правила обработки данных, поскольку они будут принудительно применяться ко всему приложению.

И чтобы это стало еще яснее, в версии 9.0 плагина Android Gradle мы начнем полностью игнорировать глобальные флаги оптимизации из библиотек.

Передовые методы работы для библиотек

Каждое Android-приложение так или иначе использует библиотеки. Поэтому давайте поговорим о лучших практиках использования библиотек.

Для разработчиков библиотек

Если ваша библиотека использует рефлексию или JNI, вы обязаны предоставить необходимые правила сохранения (Keep Rules) её потребителям. Эти правила размещаются в файле consumer-rules.pro , который затем автоматически включается в AAR-файл библиотеки.

android {

    defaultConfig {

        consumerProguardFiles("consumer-rules.pro")

    }

    ...

}

Для читателей библиотек

Отфильтруйте проблемные правила сохранения.

Если вам необходимо использовать библиотеку, содержащую проблемные правила сохранения (Keep Rules), вы можете отфильтровать их в файле build.gradle.kts , начиная с AGP 9.0. Это указывает R8 игнорировать правила, поступающие от конкретной зависимости.

release {

    optimization.keepRules {

        // Ignore all consumer rules from this specific library

        it.ignoreFrom("com.somelibrary:somelibrary")

    }

}

Лучшее правило сохранения — это отсутствие правила сохранения.

Идеальная стратегия настройки R8 — полностью отказаться от написания правил сохранения (Keep Rules) . Для многих приложений этого можно добиться, выбрав современные библиотеки, которые отдают предпочтение генерации кода перед рефлексией. Благодаря генерации кода оптимизатор может легче определить, какой код фактически используется во время выполнения, а какой можно удалить. Кроме того, отказ от динамической рефлексии означает отсутствие «скрытых» точек входа, а значит, и правил сохранения. При выборе новой библиотеки всегда отдавайте предпочтение решению, использующему генерацию кода вместо рефлексии.

Для получения более подробной информации о том, как выбирать библиотеки, ознакомьтесь с разделом «Выбирайте библиотеку с умом» .

Отладка и устранение неполадок в вашей конфигурации R8.

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

Найти дубликаты и глобальные правила сохранения

Поскольку R8 объединяет правила из десятков источников, бывает сложно определить, какой набор правил является «окончательным». Добавление этого флага в файл proguard-rules.pro генерирует полный отчет:

# Outputs the final, merged set of rules to the specified file

-printconfiguration build/outputs/logs/configuration.txt

Вы можете выполнить поиск в этом файле, чтобы найти избыточные правила или отследить проблемное правило (например, -dontoptimize ) до конкретной библиотеки, которая его включила.

Спросите R8: Зачем вы это храните?

Если класс, который вы ожидали удалить, всё ещё присутствует в вашем приложении, R8 может подсказать причину. Просто добавьте это правило:

# Asks R8 to explain why it's keeping a specific class

class com.example.MyUnusedClass

-whyareyoukeeping 

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

Полное руководство см. в разделе «Устранение неполадок R8» .

Следующие шаги

R8 — мощный инструмент для повышения производительности Android-приложений. Его эффективность зависит от правильного понимания принципов его работы как механизма статического анализа.

Создавая конкретные правила на уровне элементов, используя предков и аннотации, а также тщательно выбирая правильные параметры сохранения, вы можете сохранить именно то, что необходимо. Наиболее передовая практика заключается в полном отказе от необходимости в правилах за счет выбора современных библиотек, основанных на генерации кода, вместо их предшественников, использующих рефлексию.

В рамках Недели повышения производительности обязательно посмотрите сегодняшнее видео на YouTube и продолжите наше задание по R8. Используйте хэштег #optimizationEnabled для любых вопросов по включению или устранению неполадок R8. Мы здесь, чтобы помочь.

Пришло время убедиться в преимуществах на собственном опыте.

Предлагаем вам сегодня же включить полнофункциональный режим R8 для вашего приложения.

  1. Для начала воспользуйтесь нашими руководствами для разработчиков: Включите оптимизацию приложения .
  2. Проверьте, используете ли вы по-прежнему файл proguard-android.txt , и замените его на proguard-android-optimize.txt .
  3. Затем измерьте эффект . Не просто почувствуйте разницу, а проверьте её. Измерьте прирост производительности, адаптировав код из нашего примера приложения Macrobenchmark на GitHub, чтобы измерить время запуска до и после обновления.

Мы уверены, что вы заметите существенное улучшение производительности вашего приложения.

Заодно используйте хэштег #AskAndroid, чтобы задать свои вопросы. В течение недели наши эксперты будут отслеживать ваши вопросы и отвечать на них.

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

    Автор:

    Продолжить чтение