Anwendungsfälle und Beispiele für die Aufbewahrungsregel

Die folgenden Beispiele basieren auf häufigen Szenarien, in denen Sie R8 zur Optimierung verwenden, aber erweiterte Anleitungen zum Erstellen von Keep-Regeln benötigen.

Beurteilung

Im Allgemeinen wird für eine optimale Leistung nicht empfohlen, die Beurteilung zu verwenden. In bestimmten Szenarien ist sie jedoch möglicherweise unvermeidlich. Die folgenden Beispiele enthalten Anleitungen für Keep-Regeln in häufigen Szenarien, in denen die Beurteilung verwendet wird.

Beurteilung mit Klassen, die nach Namen geladen werden

Bibliotheken laden Klassen häufig dynamisch, indem sie den Klassennamen als String verwenden. R8 kann jedoch keine Klassen erkennen, die auf diese Weise geladen werden, und entfernt möglicherweise die Klassen, die als nicht verwendet gelten.

Betrachten Sie beispielsweise das folgende Szenario, in dem Sie eine Bibliothek und eine App haben, die die Bibliothek verwendet. Der Code zeigt einen Bibliotheksloader, der eine StartupTask-Schnittstelle instanziiert, die von einer App implementiert wird.

Der Bibliothekscode sieht so aus:

// The interface for a task that runs once.
interface StartupTask {
    fun run()
}

// The library object that loads and executes the task.
object TaskRunner {
    fun execute(className: String) {
        // R8 won't retain classes specified by this string value at runtime
        val taskClass = Class.forName(className)
        val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask
        task.run()
    }
}

Die App, die die Bibliothek verwendet, hat den folgenden Code:

// The app's task to pre-cache data.
// R8 will remove this class because it's only referenced by a string.
class PreCacheTask : StartupTask {
    override fun run() {
        // This log will never appear if the class is removed by R8.
        Log.d("AppTask", "Warming up the cache...")
    }
}

fun onCreate() {
    // The library is told to run the app's task by its name.
    TaskRunner.execute("com.example.app.PreCacheTask")
}

In diesem Szenario sollte Ihre Bibliothek eine Keep-Regeldatei für Nutzer mit den folgenden Keep-Regeln enthalten:

-keep class * implements com.example.library.StartupTask {
    <init>();
}

Ohne diese Regel entfernt R8 PreCacheTask aus der App, da die App die Klasse nicht direkt verwendet, wodurch die Integration unterbrochen wird. Die Regel findet die Klassen, die die StartupTask-Schnittstelle Ihrer Bibliothek implementieren, und behält sie zusammen mit ihrem Konstruktor ohne Argumente bei, sodass die Bibliothek PreCacheTask erfolgreich instanziieren und ausführen kann.

Beurteilung mit ::class.java

Bibliotheken können Klassen laden, indem sie die App das Class-Objekt direkt übergeben lassen. Dies ist eine robustere Methode als das Laden von Klassen nach Namen. Dadurch wird ein starker Verweis auf die Klasse erstellt, die R8 erkennen kann. Dadurch wird zwar verhindert, dass R8 die Klasse entfernt, Sie müssen jedoch weiterhin eine Keep-Regel verwenden, um zu deklarieren, dass die Klasse reflektiv instanziiert wird, und um die Mitglieder zu schützen, auf die reflektiv zugegriffen wird, z. B. den Konstruktor.

Betrachten Sie beispielsweise das folgende Szenario, in dem Sie eine Bibliothek und eine App haben, die die Bibliothek verwendet. Der Bibliotheksloader instanziiert eine StartupTask Schnittstelle, indem er den Klassenverweis direkt übergibt.

Der Bibliothekscode sieht so aus:

// The interface for a task that runs once.
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()
    }
}

Die App, die die Bibliothek verwendet, hat den folgenden Code:

// The app's task is to pre-cache data.
class PreCacheTask : StartupTask {
    override fun run() {
        Log.d("AppTask", "Warming up the cache...")
    }
}

fun onCreate() {
    // The library is given a direct reference to the app's task class.
    TaskRunner.execute(PreCacheTask::class.java)
}

In diesem Szenario sollte Ihre Bibliothek eine Keep-Regeldatei für Nutzer mit den folgenden Keep-Regeln enthalten:

# Allow any implementation of StartupTask to be removed if unused.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
# Keep the default constructor, which is called via reflection.
-keepclassmembers class * implements com.example.library.StartupTask {
    <init>();
}

Diese Regeln sind so konzipiert, dass sie perfekt mit dieser Art der Beurteilung funktionieren und eine maximale Optimierung ermöglichen, während gleichzeitig sichergestellt wird, dass der Code korrekt funktioniert. Mit den Regeln kann R8 den Klassennamen verschleiern und die Implementierung der StartupTask-Klasse verkleinern oder entfernen, wenn die App sie nie verwendet. Für jede Implementierung, z. B. PrecacheTask, die im Beispiel verwendet wird, wird jedoch der Standardkonstruktor (<init>()) beibehalten, den Ihre Bibliothek aufrufen muss.

  • -keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask: Diese Regel gilt für alle Klassen, die Ihre StartupTask Schnittstelle implementieren.
    • -keep class * implements com.example.library.StartupTask: Dadurch wird jede Klasse (*) beibehalten, die die Schnittstelle implementiert.
    • ,allowobfuscation: Diese Anweisung weist R8 an, die Klasse trotz Beibehaltung umzubenennen oder zu verschleiern. Das ist sicher, da Ihre Bibliothek nicht auf den Namen der Klasse angewiesen ist. Sie erhält das Class-Objekt direkt.
    • ,allowshrinking: Mit diesem Modifikator wird R8 angewiesen, die Klasse zu entfernen, wenn sie nicht verwendet wird. So kann R8 eine Implementierung von StartupTask sicher löschen, die nie an TaskRunner.execute() übergeben wird. Kurz gesagt, diese Regel bedeutet Folgendes: Wenn eine App eine Klasse verwendet, die StartupTask implementiert, behält R8 die Klasse bei. R8 kann die Klasse umbenennen, um ihre Größe zu reduzieren, und sie löschen, wenn die App sie nicht verwendet.
  • -keepclassmembers class * implements com.example.library.StartupTask { <init>(); }: Diese Regel gilt für bestimmte Mitglieder der Klassen, die in der ersten Regel identifiziert wurden, in diesem Fall den Konstruktor.
    • -keepclassmembers class * implements com.example.library.StartupTask: Dadurch werden bestimmte Mitglieder (Methoden, Felder) der Klasse beibehalten, die die StartupTask-Schnittstelle implementiert, aber nur, wenn die implementierte Klasse selbst beibehalten wird.
    • { <init>(); }: Dies ist der Mitgliedsauswähler. <init> ist der spezielle interne Name für einen Konstruktor im Java-Bytecode. Dieser Teil gilt speziell für den Standardkonstruktor ohne Argumente.
    • Diese Regel ist wichtig, da Ihr Code getDeclaredConstructor().newInstance() ohne Argumente aufruft, wodurch der Standardkonstruktor reflektiv aufgerufen wird. Ohne diese Regel sieht R8, dass kein Code new PreCacheTask() direkt aufruft, geht davon aus, dass der Konstruktor nicht verwendet wird, und entfernt ihn. Dadurch stürzt Ihre App zur Laufzeit mit einer InstantiationException ab.

Beurteilung basierend auf der Methodenannotation

Bibliotheken definieren häufig Annotationen, mit denen Entwickler Methoden oder Felder taggen. Die Bibliothek verwendet dann die Beurteilung, um diese annotierten Mitglieder zur Laufzeit zu finden. Die Annotation @OnLifecycleEvent wird beispielsweise verwendet, um die erforderlichen Methoden zur Laufzeit zu finden.

Betrachten Sie beispielsweise das folgende Szenario, in dem Sie eine Bibliothek und eine App haben, die die Bibliothek verwendet. Das Beispiel zeigt einen Event-Bus, der Methoden findet und aufruft, die mit @OnEvent annotiert sind.

Der Bibliothekscode sieht so aus:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class OnEvent

class EventBus {
    fun dispatch(listener: Any) {
        // Find all methods annotated with @OnEvent and invoke them
        listener::class.java.declaredMethods.forEach { method ->
            if (method.isAnnotationPresent(OnEvent::class.java)) {
                try {
                    method.invoke(listener)
                } catch (e: Exception) { /* ... */ }
            }
        }
    }
}

Die App, die die Bibliothek verwendet, hat den folgenden Code:

class MyEventListener {
    @OnEvent
    fun onSomethingHappened() {
        // This method will be removed by R8 without a keep rule
        Log.d(TAG, "Event received!")
    }
}

fun onCreate() {
    // Instantiate the listener and the event bus
    val listener = MyEventListener()
    val eventBus = EventBus()

    // Dispatch the listener to the event bus
    eventBus.dispatch(listener)
}

Die Bibliothek sollte eine Keep-Regeldatei für Nutzer enthalten, die automatisch alle Methoden beibehält, die ihre Annotationen verwenden:

-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
    @com.example.library.OnEvent <methods>;
}
  • -keepattributes RuntimeVisibleAnnotations: Diese Regel behält Annotationen bei, die zur Laufzeit gelesen werden sollen.
  • -keep @interface com.example.library.OnEvent: Diese Regel behält die OnEvent Annotationsklasse selbst bei.
  • -keepclassmembers class * {@com.example.library.OnEvent <methods>;}: Diese Regel behält eine Klasse und bestimmte Mitglieder nur bei, wenn die Klasse verwendet wird und die Klasse diese Mitglieder enthält.
    • -keepclassmembers: Diese Regel behält eine Klasse und bestimmte Mitglieder nur bei, wenn die Klasse verwendet wird und die Klasse diese Mitglieder enthält.
    • class *: Die Regel gilt für alle Klassen.
    • @com.example.library.OnEvent <methods>;: Dadurch werden alle Klassen beibehalten , die eine oder mehrere Methoden (<methods>) haben, die mit @com.example.library.OnEvent annotiert sind, und auch die annotierten Methoden selbst.

Beurteilung basierend auf Klassenannotationen

Bibliotheken können die Beurteilung verwenden, um nach Klassen zu suchen, die eine bestimmte Annotation haben. In diesem Fall findet die Task-Runner-Klasse alle Klassen, die mit ReflectiveExecutor annotiert sind, mithilfe der Beurteilung und führt die Methode execute aus.

Betrachten Sie beispielsweise das folgende Szenario, in dem Sie eine Bibliothek und eine App haben, die die Bibliothek verwendet.

Die Bibliothek hat den folgenden Code:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class ReflectiveExecutor

class TaskRunner {
    fun process(task: Any) {
        val taskClass = task::class.java
        if (taskClass.isAnnotationPresent(ReflectiveExecutor::class.java)) {
            val methodToCall = taskClass.getMethod("execute")
            methodToCall.invoke(task)
        }
    }
}

Die App, die die Bibliothek verwendet, hat den folgenden Code:

// In consumer app

@ReflectiveExecutor
class ImportantBackgroundTask {
    fun execute() {
        // This class will be removed by R8 without a keep rule
        Log.e("ImportantBackgroundTask", "Executing the important background task...")
    }
}

// Usage of ImportantBackgroundTask

fun onCreate(){
    val task = ImportantBackgroundTask()
    val runner = TaskRunner()
    runner.process(task)
}

Da die Bibliothek die Beurteilung reflektiv verwendet, um bestimmte Klassen abzurufen, sollte die Bibliothek eine Keep-Regeldatei für Nutzer mit den folgenden Keep-Regeln enthalten:

# Retain annotation metadata for runtime reflection.
-keepattributes RuntimeVisibleAnnotations

# Keep the annotation interface itself.
-keep @interface com.example.library.ReflectiveExecutor

# Keep the execute method in the classes which are being used
-keepclassmembers @com.example.library.ReflectiveExecutor class * {
   public void execute();
}

Diese Konfiguration ist sehr effizient, da sie R8 genau mitteilt, was beibehalten werden soll.

Beurteilung zur Unterstützung optionaler Abhängigkeiten

Ein häufiger Anwendungsfall für die Beurteilung ist das Erstellen einer weichen Abhängigkeit zwischen einer Kernbibliothek und einer optionalen Add-on-Bibliothek. Die Kernbibliothek kann prüfen, ob das Add-on in der App enthalten ist, und bei Bedarf zusätzliche Funktionen aktivieren. So können Sie Add-on-Module bereitstellen, ohne dass die Kernbibliothek eine direkte Abhängigkeit von ihnen hat.

Die Kernbibliothek verwendet die Beurteilung (Class.forName), um nach einer bestimmten Klasse anhand ihres Namens zu suchen. Wenn die Klasse gefunden wird, wird die Funktion aktiviert. Andernfalls schlägt sie ordnungsgemäß fehl.

Betrachten Sie beispielsweise den folgenden Code, in dem ein AnalyticsManager nach einer optionalen VideoEventTracker-Klasse sucht, um die Videoanalyse zu aktivieren.

Die Kernbibliothek hat den folgenden Code:

object AnalyticsManager {
    private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker"

    fun initialize() {
        try {
            // Attempt to load the optional module's class using reflection
            Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance()
            Log.d(TAG, "Video tracking enabled.")
        } catch (e: ClassNotFoundException) {
            Log.d(TAG,"Video tracking module not found. Skipping.")
        } catch (e: Exception) {
            Log.e(TAG, e.printStackTrace())
        }
    }
}

Die optionale Videobibliothek hat den folgenden Code:

package com.example.analytics.video

class VideoEventTracker {
    // This constructor must be kept for the reflection call to succeed.
    init { /* ... */ }
}

Der Entwickler der optionalen Bibliothek ist für die Bereitstellung der erforderlichen Keep-Regel für Nutzer verantwortlich. Diese Keep-Regel sorgt dafür, dass jede App, die die optionale Bibliothek verwendet, den Code beibehält, den die Kernbibliothek finden muss.

# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
    <init>();
}

Ohne diese Regel entfernt R8 VideoEventTracker wahrscheinlich aus der optionalen Bibliothek, da nichts in diesem Modul sie direkt verwendet. Die Keep-Regel behält die Klasse und ihren Konstruktor bei, sodass die Kernbibliothek sie erfolgreich instanziieren kann.

Beurteilung für den Zugriff auf private Mitglieder

Die Verwendung der Beurteilung für den Zugriff auf privaten oder geschützten Code, der nicht Teil der öffentlichen API einer Bibliothek ist, kann zu erheblichen Problemen führen. Solcher Code kann ohne Vorankündigung geändert werden, was zu unerwartetem Verhalten oder Abstürzen in Ihrer Anwendung führen kann.

Wenn Sie sich auf die Beurteilung für nicht öffentliche APIs verlassen, können die folgenden Probleme auftreten:

  • Blockierte Updates:Änderungen am privaten oder geschützten Code können verhindern, dass Sie auf höhere Bibliotheksversionen aktualisieren.
  • Verpasste Vorteile:Sie verpassen möglicherweise neue Funktionen, wichtige Fehlerbehebungen oder wichtige Sicherheitsupdates.

R8-Optimierungen und Beurteilung

Wenn Sie den privaten oder geschützten Code einer Bibliothek beurteilen müssen, achten Sie genau auf die Optimierungen von R8. Wenn es keine direkten Verweise auf diese Mitglieder gibt, geht R8 möglicherweise davon aus, dass sie nicht verwendet werden, und entfernt oder benennt sie um. Dies kann zu Laufzeitabstürzen führen, oft mit irreführenden Fehlermeldungen wie NoSuchMethodException oder NoSuchFieldException.

Betrachten Sie beispielsweise das folgende Szenario, in dem gezeigt wird, wie Sie auf ein privates Feld aus einer Bibliotheksklasse zugreifen können.

Eine Bibliothek, die Ihnen nicht gehört, hat den folgenden Code:

class LibraryClass {
    private val secretMessage = "R8 will remove me"
}

Ihre App hat den folgenden Code:

fun accessSecretMessage(instance: LibraryClass) {
    // Use Java reflection from Kotlin to access the private field
    val secretField = instance::class.java.getDeclaredField("secretMessage")
    secretField.isAccessible = true
    // This will crash at runtime with R8 enabled
    val message = secretField.get(instance) as String
}

Fügen Sie in Ihrer App eine -keep-Regel hinzu, um zu verhindern, dass R8 das private Feld entfernt:

-keepclassmembers class com.example.LibraryClass {
    private java.lang.String secretMessage;
}
  • -keepclassmembers: Dadurch werden bestimmte Mitglieder einer Klasse nur beibehalten, wenn die Klasse selbst beibehalten wird.
  • class com.example.LibraryClass: Dies gilt für die genaue Klasse, die das Feld enthält.
  • private java.lang.String secretMessage;: Dadurch wird das spezifische private Feld anhand seines Namens und Typs identifiziert.

Java Native Interface (JNI)

Die Optimierungen von R8 können Probleme verursachen, wenn Upcalls von nativem (C/C++-Code) zu Java oder Kotlin verwendet werden. Das Gegenteil ist auch der Fall: Downcalls von Java oder Kotlin zu nativem Code können Probleme verursachen. Die Standarddatei proguard-android-optimize.txt enthält jedoch die folgende Regel, damit die Downcalls funktionieren. Diese Regel schützt vor dem Entfernen nativer Methoden.

-keepclasseswithmembernames,includedescriptorclasses class * {
  native <methods>;
}

Interaktion mit nativem Code über das Java Native Interface (JNI)

Wenn Ihre App JNI verwendet, um Upcalls von nativem (C/C++)-Code zu Java oder Kotlin auszuführen, kann R8 nicht sehen, welche Methoden von Ihrem nativen Code aufgerufen werden. Wenn es keine direkten Verweise auf diese Methoden in Ihrer App gibt, geht R8 fälschlicherweise davon aus, dass diese Methoden nicht verwendet werden, und entfernt sie, wodurch Ihre App abstürzt.

Das folgende Beispiel zeigt eine Kotlin-Klasse mit einer Methode, die von einer nativen Bibliothek aufgerufen werden soll. Die native Bibliothek instanziiert einen Anwendungstyp und übergibt Daten von nativem Code an den Kotlin-Code.

package com.example.models

// This class is used in the JNI bridge method signature
data class NativeData(val id: Int, val payload: String)
package com.example.app
// In package com.example.app
class JniBridge {
    /**
     *   This method is called from the native side.
     *   R8 will remove it if it's not kept.
     */
    fun onNativeEvent(data: NativeData) {
        Log.d(TAG, "Received event from native code: $data")
    }
    // Use 'external' to declare a native method
    external fun startNativeProcess()

    companion object {
        init {
            // Load the native library
            System.loadLibrary("my-native-lib")
        }
    }
}

In diesem Fall müssen Sie R8 informieren, um zu verhindern, dass der Anwendungstyp optimiert wird. Wenn Methoden, die von nativem Code aufgerufen werden, außerdem Ihre eigenen Klassen in ihren Signaturen als Parameter oder Rückgabetypen verwenden, müssen Sie auch prüfen, ob diese Klassen nicht umbenannt werden.

Fügen Sie Ihrer App die folgenden Keep-Regeln hinzu:

-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
    public void onNativeEvent(com.example.model.NativeData);
}

-keep class NativeData{
        <init>(java.lang.Integer, java.lang.String);
}

Diese Keep-Regeln verhindern, dass R8 die Methode onNativeEvent und vor allem ihren Parametertyp entfernt oder umbenennt.

  • -keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}: Dadurch werden bestimmte Mitglieder einer Klasse nur beibehalten, wenn die Klasse zuerst in Kotlin- oder Java-Code instanziiert wird. R8 wird mitgeteilt, dass die App die Klasse verwendet und dass bestimmte Mitglieder der Klasse beibehalten werden sollen.
    • -keepclassmembers: Dadurch werden bestimmte Mitglieder einer Klasse nur beibehalten, wenn die Klasse zuerst in Kotlin- oder Java-Code instanziiert wird. R8 wird mitgeteilt, dass die App die Klasse verwendet und dass bestimmte Mitglieder der Klasse beibehalten werden sollen.
    • class com.example.JniBridge: Dies gilt für die genaue Klasse, die das Feld enthält.
    • includedescriptorclasses: Mit diesem Modifikator werden auch alle Klassen beibehalten, die in der Methodensignatur oder im Deskriptor gefunden werden. In diesem Fall wird verhindert, dass R8 die Klasse com.example.models.NativeData umbenennt oder entfernt, die als Parameter verwendet wird. Wenn NativeData umbenannt würde (z. B. in a.a), würde die Methodensignatur nicht mehr mit den Erwartungen des nativen Codes übereinstimmen, was zu einem Absturz führen würde.
    • public void onNativeEvent(com.example.models.NativeData);: Dadurch wird die genaue Java-Signatur der Methode angegeben, die beibehalten werden soll.
  • -keep class NativeData{<init>(java.lang.Integer, java.lang.String);}: Mit includedescriptorclasses wird zwar sichergestellt, dass die NativeData Klasse selbst beibehalten wird, aber für alle Mitglieder (Felder oder Methoden) in NativeData auf die direkt über Ihren nativen JNI-Code zugegriffen wird, sind eigene Keep Regeln erforderlich.
    • -keep class NativeData: Dies gilt für die Klasse mit dem Namen NativeData und der Block gibt an, welche Mitglieder in der NativeData-Klasse beibehalten werden sollen.
    • <init>(java.lang.Integer, java.lang.String): Dies ist die Signatur des Konstruktors. Sie identifiziert eindeutig den Konstruktor, der zwei Parameter akzeptiert: Der erste ist ein Integer und der zweite ein String.

Indirekte Plattformaufrufe

Daten mit einer Implementierung von Parcelable übertragen

Das Android-Framework verwendet die Beurteilung, um Instanzen Ihrer Parcelable-Objekte zu erstellen. In der modernen Kotlin-Entwicklung sollten Sie das kotlin-parcelize-Plug-in verwenden, das automatisch die erforderliche Parcelable-Implementierung generiert, einschließlich des Felds CREATOR und der Methoden, die das Framework benötigt.

Betrachten Sie beispielsweise das folgende Beispiel, in dem das kotlin-parcelize-Plug-in verwendet wird, um eine Parcelable-Klasse zu erstellen:

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

// Add the @Parcelize annotation to your data class
@Parcelize
data class UserData(
    val name: String,
    val age: Int
) : Parcelable

In diesem Szenario gibt es keine empfohlene Keep-Regel. Das Gradle-Plug-in kotlin-parcelize generiert automatisch die erforderlichen Keep-Regeln für die Klassen, die Sie mit @Parcelize annotieren. Es übernimmt die Komplexität für Sie und sorgt dafür, dass der generierte CREATOR und die Konstruktoren für die Beurteilungsaufrufe des Android-Frameworks beibehalten werden.

Wenn Sie eine Parcelable-Klasse manuell in Kotlin schreiben, ohne @Parcelize zu verwenden, sind Sie dafür verantwortlich, das Feld CREATOR und den Konstruktor beizubehalten, der ein Parcel akzeptiert. Wenn Sie das vergessen, stürzt Ihre App ab, wenn das System versucht, Ihr Objekt zu deserialisieren. Die Verwendung von @Parcelize ist die Standardmethode und sicherer.

Beachten Sie bei der Verwendung des kotlin-parcelize-Plug-ins Folgendes:

  • Das Plug-in erstellt während der Kompilierung automatisch CREATOR-Felder.
  • Die Datei proguard-android-optimize.txt enthält die erforderlichen keep-Regeln, um diese Felder für die ordnungsgemäße Funktion beizubehalten.
  • App-Entwickler müssen prüfen, ob alle erforderlichen keep-Regeln vorhanden sind, insbesondere für benutzerdefinierte Implementierungen oder Drittanbieterabhängigkeiten.

Bibliotheken, die die Beurteilung oder Bytecode-Transformationen verwenden, greifen zur Laufzeit dynamisch auf Code zu. Wenn R8 Klassen, Felder oder Methoden entfernt oder umbenennt, auf die auf diese Weise zugegriffen wird, kann Ihre App abstürzen.

Beliebte Drittanbieterbibliotheken (z. B. Gson, Retrofit und Kotlinx Serialization) bündeln jedoch automatisch ihre eigenen R8-Keep-Regeln für Nutzer. Wenn Sie aktuelle Versionen dieser Bibliotheken verwenden, müssen Sie Ihrem Projekt keine manuellen Keep-Regeln hinzufügen.

Gson

Gson ist eine Bibliothek für die JSON-Serialisierung und -Deserialisierung, die stark auf der Beurteilung basiert. Wenn Sie den vollständigen Modus verwenden, um Ihre App zu optimieren, werden generische Typsignaturen, Standardkonstruktoren und nicht annotierte Felder entfernt, sofern nicht ausdrücklich anders angegeben.

Damit Gson ordnungsgemäß funktioniert, fügen Sie bestimmte Regeln hinzu, um nicht transiente Felder in Ihren Datenmodellklassen beizubehalten und die TypeToken-Hierarchie beizubehalten:

# Preserve generic type information required for deserialization
-keepattributes Signature

# Keep all non-transient fields in your data model classes for reflection
-keepclassmembers class com.example.models.** {
    !transient <fields>;
}

# Keep TypeToken itself and any anonymous classes extending it
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken

Felder, die mit dem transient Modifikator gekennzeichnet sind, werden von Gson bei der Serialisierung und Deserialisierung ignoriert. Daher gilt die Keep-Regel speziell für nicht transiente Felder (!transient).

Retrofit

Retrofit ist eine Netzwerkbibliothek, die Dienstschnittstellenmethoden, die mit HTTP-Annotationen (z. B. @GET oder @POST) annotiert sind, mithilfe der Beurteilung prüft, um Netzwerkanfragen zu erstellen und Antworten zu konvertieren.

Retrofit generiert zur Laufzeit dynamisch Implementierungen Ihrer API-Schnittstellen mit Proxy.newProxyInstance(). Da R8 keine Klasse sieht, die diese Schnittstellen statisch implementiert, werden die Methoden oder ihre generischen Rückgabetypen möglicherweise entfernt.

Gebündelte Keep-Regeln

Retrofit verwendet die Beurteilung zur Laufzeit, um generische Parameter, Methodenannotationen und Parameterannotationen zu prüfen. Ohne die richtige Konfiguration kann der vollständige Modus von R8 generische Signaturen aus Rückgabetypen, Kotlin-Fortsetzungen und Antwortklassen entfernen oder sogar Schnittstellenwerte durch „null“ ersetzen, da Retrofit-Schnittstellen dynamisch mit einem Proxy instanziiert werden.

Ab Retrofit 2.10.0 bündelt die Bibliothek automatisch die offiziellen Keep-Regeln, die erforderlich sind, um Annotationen-Standardwerte, Dienstmethodenparameter und erforderliche Klassenmetadaten beizubehalten. Weitere Informationen finden Sie unter Von Retrofit verwendete Regeln.

Generische Rückgabetypen beibehalten

Retrofit prüft die generische Signatur des Rückgabetyps (z. B. Observable<Data>), um die Netzwerkantwort korrekt zu deserialisieren. Wenn R8 die generische Signatur entfernt, ersetzt Retrofit das instanziierte Objekt durch null.

Damit der vollständige Modus von R8 die generische Signatur Ihrer Rückgabetypen nicht entfernt, verwenden Sie die folgende bedingte Regel:

# Preserve generic type information for Call/Observable return types
-keepattributes Signature

# If an interface has a Retrofit HTTP annotation, keep its return type (class <3>)
-if interface * {
    @retrofit2.http.* public *** *(...);
}
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>

Die tatsächliche Datenmodellklasse, die zurückgegeben wird (z. B. Data in Observable<Data>), muss ebenfalls beibehalten werden, da sie von dem Konverter (z. B. Gson) reflektiv erstellt wird.

Koroutinen

Wenn Sie Kotlin-Koroutinen verwenden, transformiert der Kotlin-Compiler suspend-Funktionen, indem er der kompilierten Methodensignatur einen Continuation-Parameter anhängt.

Wenn Bibliotheken wie Retrofit die generische Signatur einer suspend-Funktion reflektiv lesen, verwenden sie diesen Continuation-Parameter. Im vollständigen Modus wird das Attribut Signature nur für Klassen beibehalten, die explizit beibehalten werden. Da Continuation ein synthetischer Parameter ist, entfernt R8 standardmäßig seine Signatur, wodurch die Beurteilung unterbrochen wird.

Um das Entfernen von Signaturen zu verhindern und die Laufzeitkompatibilität im vollständigen Modus zu gewährleisten, fügen Sie die folgende Regel hinzu:

# Keep the signature attribute globally
-keepattributes Signature

# Explicitly keep the Continuation class so its signature is not stripped
-keep class kotlin.coroutines.Continuation