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