Раскройте весь потенциал оптимизатора R8

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

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

Включить полный режим

Чтобы включить полный режим, удалите следующую строку из файла gradle.properties :

android.enableR8.fullMode=false // Remove this line to enable full mode

Сохранять классы, связанные с атрибутами

Атрибуты — это метаданные, хранящиеся в скомпилированных файлах классов и не являющиеся частью исполняемого кода. Однако они могут потребоваться для определённых типов рефлексии. К распространённым примерам относятся Signature (сохраняющая информацию об обобщённом типе после удаления типа), InnerClasses и EnclosingMethod (для рефлексии структуры класса), а также аннотации, видимые во время выполнения.

Следующий код показывает, как выглядит атрибут Signature для поля в байт-коде. Для поля:

List<User> users;

Скомпилированный файл класса будет содержать следующий байт-код:

.field public static final users:Ljava/util/List;
    .annotation system Ldalvik/annotation/Signature;
        value = {
            "Ljava/util/List<",
            "Lcom/example/package/User;",
            ">;"
        }
    .end annotation
.end field

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

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

Рассмотрим следующий пример, в котором мы десериализуем список пользователей с помощью библиотеки Gson.


import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

data class User(
    @SerializedName("username")
    var username: String? = null,
    @SerializedName("age")
    var age: Int = 0
)

fun GsonRemoteJsonListExample() {
    val gson = Gson()

    // 1. The JSON string for a list of users returned from remote
    val jsonOutput = """[{"username":"alice","age":30}, {"username":"bob","age":25}]"""

    // 2. Deserialize the JSON string into a List<User>
    // We must use TypeToken for generic types like List
    val listType = object : TypeToken<List<User>>() {}.type
    val deserializedList: List<User> = gson.fromJson(jsonOutput, listType)

    // Print the list
    println("First user from list: ${deserializedList}")
}

Во время компиляции стирание типов в Java удаляет аргументы обобщенных типов. Это означает, что во время выполнения и List<String> , и List<User> отображаются как необработанный List . Поэтому библиотеки, такие как Gson, которые полагаются на рефлексию, не могут определить конкретные типы объектов, которые были объявлены в List при десериализации JSON-списка, что может привести к проблемам во время выполнения.

Для сохранения информации о типе Gson использует TypeToken . Обёртка TypeToken сохраняет необходимую информацию о десериализации.

Выражение Kotlin object:TypeToken<List<User>>() {}.type создаёт анонимный внутренний класс, расширяющий TypeToken и сохраняющий информацию об универсальном типе. В этом примере анонимный класс называется $GsonRemoteJsonListExample$listType$1 .

Язык программирования Java сохраняет общую сигнатуру суперкласса в виде метаданных, известных как атрибут Signature , в скомпилированном файле класса. Затем TypeToken использует эти метаданные Signature для восстановления типа во время выполнения. Это позволяет Gson использовать рефлексию для чтения Signature и успешного обнаружения полного типа List<User> необходимого для десериализации.

При включении R8 в режиме совместимости атрибут Signature сохраняется для классов, включая анонимные внутренние классы, такие как $GsonRemoteJsonListExample$listType$1 , даже если конкретные правила сохранения не определены явно. Таким образом, режим совместимости с R8 не требует дополнительных явных правил сохранения для корректной работы этого примера.

// keep rule for compatibility mode
-keepattributes Signature

При включении R8 в полном режиме атрибут Signature анонимного внутреннего класса $GsonRemoteJsonListExample$listType$1 удаляется. Без этой информации о типе в Signature Gson не может найти правильный тип приложения, что приводит к исключению IllegalStateException . Для предотвращения этого необходимы следующие правила:

// keep rule required for full mode
-keepattributes Signature
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken
  • -keepattributes Signature : Это правило предписывает R8 сохранить атрибут, который необходимо прочитать Gson. В полном режиме R8 сохраняет атрибут Signature только для классов, полей или методов, которые явно соответствуют правилу keep .

  • -keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken : Это правило необходимо, поскольку TypeToken является оболочкой для типа десериализуемого объекта. После удаления типа создаётся анонимный внутренний класс для сохранения информации об универсальном типе. Без явного сохранения com.google.gson.reflect.TypeToken R8 в полном режиме не включит этот тип класса в атрибут Signature необходимый для десериализации.

  • -keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken : Это правило сохраняет информацию о типе анонимных классов, расширяющих TypeToken , например, $GsonRemoteJsonListExample$listType$1 в данном примере. Без этого правила R8 в полном режиме удаляет необходимую информацию о типе, что приводит к сбою десериализации.

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

Важно понимать, что ранее представленные правила решают только задачу определения универсального типа (например, List<User> ). R8 также переименовывает поля классов. Если вы не используете аннотации @SerializedName в моделях данных, Gson не сможет десериализовать JSON, поскольку имена полей больше не будут соответствовать ключам JSON.

Однако если вы используете версию Gson старше 2.11 или если ваши модели не используют аннотацию @SerializedName , вам необходимо добавить явные правила сохранения для этих моделей.

Сохранить конструктор по умолчанию

В полном режиме R8 конструктор без аргументов/по умолчанию не сохраняется неявно, даже если сохраняется сам класс. При создании экземпляра класса с помощью class.getDeclaredConstructor().newInstance() или class.newInstance() необходимо явно сохранить конструктор без аргументов в полном режиме. В режиме совместимости же конструктор без аргументов сохраняется всегда.

Рассмотрим пример, в котором экземпляр PrecacheTask создаётся с помощью рефлексии для динамического вызова его метода run . Хотя в режиме совместимости этот сценарий не требует дополнительных правил, в полном режиме конструктор PrecacheTask по умолчанию будет удалён. Поэтому требуется специальное правило keep.

// In library
interface StartupTask {
    fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
    fun execute(taskClass: Class<out StartupTask>) {
        // The class isn't removed, but its constructor might be.
        val task = taskClass.getDeclaredConstructor().newInstance()
        task.run()
    }
}

// In app
class PreCacheTask : StartupTask {
    override fun run() {
        Log.d("Pre cache task", "Warming up the cache...")
    }
}

fun runTaskRunner() {
    // The library is given a direct reference to the app's task class.
    TaskRunner.execute(PreCacheTask::class.java)
}
# Full mode keep rule
# default constructor needs to be specified

-keep class com.example.fullmoder8.PreCacheTask {
    <init>();
}

Изменение доступа включено по умолчанию.

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

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

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