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 Leistung Ihrer App 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 angegeben, die für die sichere Migration Ihres Projekts und zur Vermeidung häufiger Laufzeitabstürze erforderlich sind.
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 zum ausführbaren Code gehören. Sie können jedoch für bestimmte Arten der Reflexion erforderlich sein. Häufige Beispiele sind Signature (mit dem generische Typinformationen nach dem Löschen von Typen beibehalten werden), 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.
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 Wrapping von TypeToken bleiben die erforderlichen Deserialisierungsinformationen erhalten.
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 Superklasse 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. Die dazu erforderlichen Regeln sind:
// 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 generischen Typinformationen beizubehalten. Wenncom.google.gson.reflect.TypeTokennicht explizit beibehalten wird, nimmt R8 im vollständigen Modus diesen Klassentyp nicht in das AttributSignatureauf, 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.
Ab Gson-Version 2.11.0 enthält die Bibliothek die erforderlichen Keep-Regeln für die Deserialisierung im vollständigen Modus. Wenn Sie Ihre App mit aktiviertem R8 erstellen, werden diese Regeln automatisch von R8 in der Bibliothek gefunden und angewendet. So wird der Schutz geboten, den Ihre App benötigt, ohne dass Sie diese spezifischen Regeln manuell in Ihrem Projekt hinzufügen oder verwalten müssen.
Es ist wichtig zu verstehen, dass die zuvor genannten Regeln nur das Problem der Ermittlung des generischen Typs lösen (z.B. List<User>).
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 warum Keep-Regeln zum Beibehalten dieser Felder/Methoden erforderlich sind.