Wykorzystaj pełny potencjał optymalizatora R8

R8 ma 2 tryby: tryb zgodności i tryb pełny. Tryb pełny zapewnia zaawansowane optymalizacje, które poprawiają wydajność aplikacji.

Ten przewodnik jest przeznaczony dla deweloperów Androida, którzy chcą korzystać z najskuteczniejszych optymalizacji R8. Wyjaśnia kluczowe różnice między trybem zgodności a trybem pełnym i zawiera konkretne konfiguracje potrzebne do bezpiecznego przeniesienia projektu i uniknięcia typowych awarii w czasie działania.

Włącz tryb pełny

Aby włączyć tryb pełny, usuń z pliku gradle.properties ten wiersz:

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

Zachowywanie klas powiązanych z atrybutami

Atrybuty to metadane przechowywane w skompilowanych plikach klas, które nie są częścią kodu wykonywalnego. Mogą być jednak potrzebne w przypadku niektórych rodzajów refleksji. Typowe przykłady to Signature (który zachowuje informacje o typie ogólnym po wymazaniu typu), InnerClassesEnclosingMethod (do odzwierciedlania struktury klasy) oraz adnotacje widoczne w czasie działania.

Poniższy kod pokazuje, jak wygląda atrybut Signature w przypadku pola w kodzie bajtowym. W przypadku pola:

List<User> users;

Skompilowany plik klasy będzie zawierał ten kod bajtowy:

.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

Biblioteki, które w dużym stopniu korzystają z odbicia (np. Gson), często polegają na tych atrybutach, aby dynamicznie sprawdzać i rozumieć strukturę kodu. W trybie pełnym R8 domyślnie atrybuty są zachowywane tylko wtedy, gdy powiązana klasa, pole lub metoda są jawnie zachowywane.

Poniższy przykład pokazuje, dlaczego atrybuty są niezbędne i jakie reguły keep należy dodać podczas przechodzenia z trybu zgodności na tryb pełny.

Rozważmy przykład, w którym deserializujemy listę użytkowników za pomocą biblioteki 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}")
}

Podczas kompilacji mechanizm usuwania typów w Javie usuwa argumenty typu ogólnego. Oznacza to, że w czasie działania programu zarówno List<String>, jak i List<User> będą wyświetlane jako surowy znak List. Dlatego biblioteki takie jak Gson, które korzystają z odbicia, nie mogą określić konkretnych typów obiektów, które miały być zawarte w List podczas deserializacji listy JSON, co może prowadzić do problemów w czasie działania programu.

Aby zachować informacje o typie, Gson używa TypeToken. Zawijanie TypeTokenzachowuje niezbędne informacje o deserializacji.

Wyrażenie w języku Kotlin object:TypeToken<List<User>>() {}.type tworzy anonimową klasę wewnętrzną, która rozszerza klasę TypeToken i przechowuje informacje o typie ogólnym. W tym przykładzie klasa anonimowa ma nazwę $GsonRemoteJsonListExample$listType$1.

Język programowania Java zapisuje ogólną sygnaturę klasy nadrzędnej jako metadane, znane jako atrybut Signature, w skompilowanym pliku klasy. TypeToken używa tych metadanych Signature do odzyskania typu w czasie działania. Dzięki temu biblioteka Gson może używać odbicia do odczytywania Signature i odkrywania pełnego typu List<User> potrzebnego do deserializacji.

Gdy R8 jest włączony w trybie zgodności, zachowuje atrybut Signature dla klas, w tym anonimowych klas wewnętrznych, takich jak $GsonRemoteJsonListExample$listType$1, nawet jeśli nie są wyraźnie zdefiniowane konkretne reguły zachowywania. W związku z tym tryb zgodności R8 nie wymaga żadnych dodatkowych reguł zachowywania, aby ten przykład działał zgodnie z oczekiwaniami.

// keep rule for compatibility mode
-keepattributes Signature

Gdy R8 jest włączony w trybie pełnym, atrybut Signature anonimowej klasy wewnętrznej $GsonRemoteJsonListExample$listType$1 jest usuwany. Bez tych informacji o typie w Signature biblioteka Gson nie może znaleźć prawidłowego typu aplikacji, co powoduje wystąpienie błędu IllegalStateException. Reguły przechowywania, które są niezbędne, aby temu zapobiec:

// 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: ta reguła nakazuje R8 zachowanie atrybutu, który jest potrzebny bibliotece Gson do odczytu. W trybie pełnym R8 zachowuje atrybut Signature tylko w przypadku klas, pól lub metod, które są wyraźnie dopasowane przez regułę keep.

  • -keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken: ta reguła jest konieczna, ponieważ element TypeToken zawiera typ deserializowanego obiektu. Po wymazaniu typu tworzona jest anonimowa klasa wewnętrzna, która zachowuje informacje o typie ogólnym. Jeśli nie zachowasz jawnie com.google.gson.reflect.TypeToken,Signaturew trybie pełnym nie będzie zawierać tego typu klasy w atrybucie Signaturewymaganym do deserializacji.

  • -keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken: ta reguła zachowuje informacje o typie klas anonimowych, które rozszerzają klasę TypeToken, np. $GsonRemoteJsonListExample$listType$1 w tym przykładzie. Bez tej reguły narzędzie R8 w trybie pełnym usuwa niezbędne informacje o typie, co powoduje niepowodzenie deserializacji.

Od wersji 2.11.0 biblioteka Gson zawiera niezbędne reguły keep wymagane do deserializacji w trybie pełnym. Gdy tworzysz aplikację z włączonym R8, automatycznie znajduje on i stosuje te reguły z biblioteki. Zapewnia to ochronę, której potrzebuje Twoja aplikacja, bez konieczności ręcznego dodawania ani utrzymywania tych konkretnych reguł w projekcie.

Warto pamiętać, że podane wcześniej reguły rozwiązują tylko problem wykrywania typu ogólnego (np. List<User>). R8 zmienia też nazwy pól klas. Jeśli nie używasz @SerializedName adnotacji w modelach danych, Gson nie będzie w stanie zdeserializować kodu JSON, ponieważ nazwy pól nie będą już pasować do kluczy JSON.

Jeśli jednak używasz wersji Gson starszej niż 2.11 lub Twoje modele nie korzystają z adnotacji @SerializedName, musisz dodać do nich jawne reguły zachowywania.

Zachowaj konstruktor domyślny

W trybie pełnym R8 konstruktor bez argumentów lub domyślny nie jest niejawnie zachowywany, nawet jeśli sama klasa jest zachowywana. Jeśli tworzysz instancję klasy za pomocą adnotacji class.getDeclaredConstructor().newInstance() lub class.newInstance(), w trybie pełnym musisz jawnie zachować konstruktor bez argumentów. Z kolei tryb zgodności zawsze zachowuje konstruktor bez argumentów.

Rozważmy przykład, w którym instancja klasy PrecacheTask jest tworzona za pomocą refleksji, aby dynamicznie wywołać jej metodę run. W trybie zgodności ta sytuacja nie wymaga dodatkowych reguł, ale w trybie pełnym domyślny konstruktor PrecacheTask zostałby usunięty. Dlatego wymagana jest konkretna reguła zachowywania.

// 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>();
}

Modyfikowanie dostępu jest domyślnie włączone

W trybie zgodności R8 nie zmienia widoczności metod i pól w klasie. W trybie pełnym R8 ulepsza jednak optymalizację, zmieniając widoczność metod i pól, np. z prywatnej na publiczną. Umożliwia to większe wstawianie kodu w miejscu wywołania.

Ta optymalizacja może powodować problemy, jeśli kod używa odbicia, które w szczególności zależy od tego, czy elementy mają określoną widoczność. R8 nie rozpozna tego pośredniego użycia, co może prowadzić do awarii aplikacji. Aby temu zapobiec, musisz dodać konkretne reguły -keep, które zachowają użytkowników, a także ich pierwotną widoczność.

Więcej informacji znajdziesz w tym przykładzie. Dowiesz się z niego, dlaczego nie zaleca się uzyskiwania dostępu do prywatnych elementów za pomocą odbicia, oraz poznasz reguły zachowywania tych pól lub metod.