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), InnerClasses i EnclosingMethod (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 atrybutSignaturetylko 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ż elementTypeTokenzawiera typ deserializowanego obiektu. Po wymazaniu typu tworzona jest anonimowa klasa wewnętrzna, która zachowuje informacje o typie ogólnym. Jeśli nie zachowasz jawniecom.google.gson.reflect.TypeToken,Signaturew trybie pełnym nie będzie zawierać tego typu klasy w atrybucieSignaturewymaganym 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$1w 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.