R8 bietet zwei Modi: den Kompatibilitätsmodus und den vollständigen Modus. Im vollständigen Modus stehen Ihnen leistungsstarke Optimierungen zur Verfügung, mit denen Sie die App-Leistung verbessern können.
Dieser Leitfaden richtet sich an Android-Entwickler, die die leistungsstärksten Optimierungen von R8 nutzen möchten. Es werden die wichtigsten Unterschiede zwischen dem Kompatibilitäts- und dem vollständigen Modus erläutert und die expliziten Konfigurationen beschrieben, die für die sichere Migration Ihres Projekts erforderlich sind, um häufige Laufzeitabstürze zu vermeiden.
Vollmodus aktivieren
Entfernen Sie die folgende Zeile aus der Datei gradle.properties, um den vollständigen Modus zu aktivieren:
android.enableR8.fullMode=false // Remove this line to enable full mode
Mit Attributen verknüpfte Klassen beibehalten
Attribute sind Metadaten, die in kompilierten Klassendateien gespeichert sind und nicht Teil des ausführbaren Codes sind. Für bestimmte Arten der Reflexion können sie jedoch erforderlich sein. Häufige Beispiele sind Signature (die generische Typinformationen nach dem Löschen des Typs beibehält), InnerClasses und EnclosingMethod (zum Reflektieren der Klassenstruktur) sowie zur Laufzeit sichtbare Anmerkungen.
Der folgende Code zeigt, wie ein Signature-Attribut für ein Feld im Bytecode aussieht. Für ein Feld:
List<User> users;
Die kompilierte Klassendatei würde den folgenden Bytecode enthalten:
.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
Bibliotheken, die Reflection intensiv nutzen (z. B. Gson), sind häufig auf diese Attribute angewiesen, um die Struktur Ihres Codes dynamisch zu untersuchen und zu verstehen. Im vollständigen Modus von R8 werden Attribute standardmäßig nur beibehalten, wenn die zugehörige Klasse, das zugehörige Feld oder die zugehörige Methode explizit beibehalten wird.
Das folgende Beispiel zeigt, warum Attribute erforderlich sind und welche Regeln Sie beim Migrieren vom Kompatibilitäts- zum vollständigen Modus hinzufügen müssen. Neben den Klassen, Feldern oder Methoden, die reflektiert werden, müssen Sie auch die Attribute, auf die sie sich stützen, explizit beibehalten.
Im folgenden Beispiel wird eine Liste von Nutzern mit der Gson-Bibliothek deserialisiert.
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}")
}
Während der Kompilierung werden generische Typargumente durch die Typauslöschung von Java entfernt. Das bedeutet, dass zur Laufzeit sowohl List<String> als auch List<User> als Roh-List angezeigt werden. Daher können Bibliotheken wie Gson, die auf Reflection basieren, beim Deserialisieren einer JSON-Liste nicht die spezifischen Objekttypen ermitteln, die List enthalten soll. Dies kann zu Laufzeitproblemen führen.
Um Typinformationen beizubehalten, verwendet Gson TypeToken. Durch das Umschließen von TypeToken
werden die erforderlichen Deserialisierungsinformationen beibehalten.
Der Kotlin-Ausdruck object:TypeToken<List<User>>() {}.type erstellt eine anonyme innere Klasse, die TypeToken erweitert und die Informationen zum generischen Typ erfasst. In diesem Beispiel heißt die anonyme Klasse $GsonRemoteJsonListExample$listType$1.
In der Java-Programmiersprache wird die generische Signatur einer Basisklasse als Metadaten, das sogenannte Signature-Attribut, in der kompilierten Klassendatei gespeichert.
TypeToken verwendet dann diese Signature-Metadaten, um den Typ zur Laufzeit wiederherzustellen.
So kann Gson die Reflektion verwenden, um Signature zu lesen und den vollständigen List<User>-Typ zu ermitteln, der für die Deserialisierung erforderlich ist.
Wenn R8 im Kompatibilitätsmodus aktiviert ist, wird das Attribut Signature für Klassen, einschließlich anonymer innerer Klassen wie $GsonRemoteJsonListExample$listType$1, beibehalten, auch wenn keine spezifischen Keep-Regeln explizit definiert sind. Daher sind im R8-Kompatibilitätsmodus keine weiteren expliziten Keep-Regeln erforderlich, damit dieses Beispiel wie erwartet funktioniert.
// keep rule for compatibility mode
-keepattributes Signature
Wenn R8 im vollständigen Modus aktiviert ist, wird das Attribut Signature der anonymen inneren Klasse $GsonRemoteJsonListExample$listType$1 entfernt. Ohne diese Typinformationen im Signature kann Gson den richtigen Anwendungstyp nicht finden, was zu einem IllegalStateException führt.
Wenn Sie eine Gson-Version vor 2.11.0 verwenden, sind die folgenden Keep-Regeln erforderlich, um dies zu verhindern:
// 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: Diese Regel weist R8 an, das Attribut beizubehalten, das Gson zum Lesen benötigt. Im vollständigen Modus behält R8 das AttributSignaturenur für Klassen, Felder oder Methoden bei, die explizit mit einerkeep-Regel übereinstimmen.-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken: Diese Regel ist erforderlich, daTypeTokenden Typ des zu deserialisierenden Objekts umschließt. Nach dem Löschen des Typs wird eine anonyme innere Klasse erstellt, um die Informationen zum generischen Typ beizubehalten. Wenn Siecom.google.gson.reflect.TypeTokennicht explizit beibehalten, wird dieser Klassentyp von R8 im vollständigen Modus nicht in das AttributSignatureaufgenommen, das für die Deserialisierung erforderlich ist.-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken: Bei dieser Regel werden die Typinformationen anonymer Klassen beibehalten, dieTypeTokenerweitern, z. B.$GsonRemoteJsonListExample$listType$1in diesem Beispiel. Ohne diese Regel entfernt R8 im vollständigen Modus die erforderlichen Typinformationen, was dazu führt, dass die Deserialisierung fehlschlägt.
Die oben genannten Regeln lösen nur das Problem, den generischen Typ (z. B. List<User>) zu ermitteln. R8 benennt auch die Felder von Klassen um. Wenn Sie keine @SerializedName-Annotationen für Ihre Datenmodelle verwenden, kann Gson JSON nicht deserialisieren, da die Feldnamen nicht mehr mit den JSON-Schlüsseln übereinstimmen.
Wenn Sie jedoch eine Gson-Version vor 2.11 verwenden oder Ihre Modelle nicht die Annotation @SerializedName verwenden, müssen Sie explizite Keep-Regeln für diese Modelle hinzufügen.
Standardkonstruktor beibehalten
Im R8-Vollmodus wird der Standardkonstruktor ohne Argumente nicht implizit beibehalten, auch wenn die Klasse selbst beibehalten wird. Wenn Sie eine Instanz einer Klasse mit class.getDeclaredConstructor().newInstance() oder class.newInstance() erstellen, müssen Sie den Konstruktor ohne Argumente im vollständigen Modus explizit beibehalten. Im Kompatibilitätsmodus wird der Konstruktor ohne Argumente immer beibehalten.
Angenommen, eine Instanz von PrecacheTask wird mithilfe von Reflection erstellt, um die Methode run dynamisch aufzurufen. In diesem Szenario sind im Kompatibilitätsmodus keine zusätzlichen Regeln erforderlich. Im vollständigen Modus wird jedoch der Standardkonstruktor von PrecacheTask entfernt. Daher ist eine bestimmte Aufbewahrungsregel erforderlich.
// 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>();
}
Die Zugriffsänderung ist standardmäßig aktiviert
Im Kompatibilitätsmodus ändert R8 die Sichtbarkeit von Methoden und Feldern innerhalb einer Klasse nicht. Im vollständigen Modus wird die Optimierung durch R8 jedoch verbessert, indem die Sichtbarkeit Ihrer Methoden und Felder geändert wird, z. B. von „privat“ zu „öffentlich“. Dadurch kann mehr Inline-Code verwendet werden.
Diese Optimierung kann Probleme verursachen, wenn in Ihrem Code Reflection verwendet wird, die speziell darauf angewiesen ist, dass Mitglieder eine bestimmte Sichtbarkeit haben. R8 erkennt diese indirekte Verwendung nicht, was möglicherweise zu App-Abstürzen führt. Um dies zu verhindern, müssen Sie spezifische -keep-Regeln hinzufügen, um die Mitglieder beizubehalten. Dadurch wird auch ihre ursprüngliche Sichtbarkeit beibehalten.
In diesem Beispiel wird erläutert, warum der Zugriff auf private Elemente über Reflection nicht empfohlen wird und welche Keep-Regeln zum Beibehalten dieser Felder/Methoden erforderlich sind.
Kotlin-spezifische Metadaten
Beim Kompilieren von Kotlin-Code speichert der Kotlin-Compiler sprachspezifische Metadaten (z. B. Null-Zulässigkeit, Erweiterungsfunktionen und Koroutinen-Signaturen) in einer @kotlin.Metadata-Annotation in jeder Klassendatei.
Wenn Ihre App oder ihre Abhängigkeiten die Kotlin-Reflektion (kotlin.reflect) verwenden, parst die Reflektionsbibliothek diese Metadaten zur Laufzeit, um die Klassenstruktur zu prüfen.
Im R8-Vollmodus entfernt R8 Annotationen standardmäßig, wenn sie nicht explizit beibehalten werden. Wenn R8 Ihre Klassen minimiert oder verkleinert, ohne die Metadaten beizubehalten und zu aktualisieren, schlägt die Kotlin-Reflektion zur Laufzeit fehl, was zu unvorhersehbarem Verhalten oder Abstürzen (z. B. KotlinReflectionInternalError) führt.
Damit unvorhersehbares Verhalten vermieden wird und Kotlin-Reflexionsfunktionen nach der Minimierung korrekt funktionieren, müssen Sie zur Laufzeit sichtbare Annotationen beibehalten und die Klasse kotlin.Metadata explizit beibehalten:
# Preserve runtime-visible annotations required for inspecting metadata
-keepattributes RuntimeVisibleAnnotations
# Keep Kotlin metadata to ensure kotlin.reflect functions correctly
-keep class kotlin.Metadata { *; }