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

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

Время на чтение: 7 минут
Посмотреть профиль Ajesh Pai Посмотреть профиль Бена Вайса
Ajesh Pai и Ben Weiss

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

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

Прочитав эту статью, посмотрите видео о включении, отладке и устранении неполадок оптимизатора R8 на YouTube.

 

 

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

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

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

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

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

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

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

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. Начиная с версии 9.0 плагина Android Gradle мы больше не поддерживаем устаревший файл proguard-android.txt. Поэтому рекомендуем перейти на оптимизированную версию.

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

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

  1. Вариант , например -keep или -keepclassmembers.
  2. Необязательные модификаторы, например allowshrinking.
  3. Спецификация класса, определяющая код для сопоставления.

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

Антипаттерны правил хранения

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

Общие настройки

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

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

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

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

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

Самый простой способ нивелировать преимущества R8 – написать слишком общие правила хранения. Правила сохранения, подобные приведенному ниже, указывают оптимизатору 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

Ещё одна распространенная ошибка – вручную добавлять правила сохранения для 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Что это дает
-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. Эти правила помещаются в файл 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 – избавиться от необходимости писать правила сохранения. Для многих приложений этого можно достичь, выбрав современные библиотеки, которые предпочитают генерацию кода рефлексии. При генерации кода оптимизатору проще определить, какой код действительно используется во время выполнения, а какой можно удалить. Если вы не используете динамическое отражение, то у вас нет "скрытых" точек входа, а значит, правила сохранения не нужны. При выборе новой библиотеки всегда отдавайте предпочтение решению, в котором используется генерация кода, а не рефлексия.

Подробнее о том, как выбрать библиотеку…

Отладка и устранение неполадок в конфигурации 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) до библиотеки, в которую оно включено.

Ask R8: Why are you keeping this? (Спросите у R8: почему вы сохраняете это?)

Если класс, который должен был быть удален, все ещё находится в приложении, R8 может сообщить вам причину. Добавьте следующее правило:

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

class com.example.MyUnusedClass

-whyareyoukeeping 

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

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

Дальнейшие действия

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

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

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

Пора узнать, какие преимущества дает подписка.

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

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

Мы уверены, что это поможет вам повысить эффективность приложения.

Если у вас есть вопросы, задавайте их в социальных сетях с хештегом #AskAndroid. Наши специалисты будут следить за обсуждением и отвечать на вопросы в течение недели.

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

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