Die folgenden Beispiele basieren auf gängigen Szenarien, in denen Sie R8 zur Optimierung verwenden, aber erweiterte Anleitungen zum Erstellen von Keep-Regeln benötigen.
Reflexion
Im Allgemeinen wird für eine optimale Leistung nicht empfohlen, Reflection zu verwenden. In bestimmten Fällen ist das jedoch unvermeidlich. Die folgenden Beispiele bieten eine Anleitung für Keep-Regeln in gängigen Szenarien, in denen Reflektion verwendet wird.
Reflection mit Klassen, die nach Namen geladen werden
Bibliotheken laden Klassen häufig dynamisch, indem sie den Klassennamen als String
verwenden.
R8 kann jedoch nicht erkennen, welche Klassen auf diese Weise geladen werden, und entfernt möglicherweise Klassen, die es für ungenutzt hält.
Stellen Sie sich beispielsweise das folgende Szenario vor, 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 Bibliotheks-Code 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 Datei mit Keep-Regeln 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 Klasse nicht direkt verwendet wird. Dadurch wird die Integration unterbrochen. Mit der Regel werden die Klassen gefunden, die die StartupTask
-Schnittstelle Ihrer Bibliothek implementieren, und sie werden zusammen mit ihrem Konstruktor ohne Argumente beibehalten. So kann die Bibliothek PreCacheTask
erfolgreich instanziieren und ausführen.
Reflexion mit ::class.java
Bibliotheken können Klassen laden, indem die App das Class
-Objekt direkt übergibt. Das ist eine robustere Methode als das Laden von Klassen nach Namen. Dadurch wird eine starke Referenz auf die Klasse erstellt, die R8 erkennen kann. Dadurch wird zwar verhindert, dass R8 die Klasse entfernt, Sie müssen aber trotzdem eine Keep-Regel verwenden, um anzugeben, dass die Klasse reflektiv instanziiert wird, und um die reflektiv aufgerufenen Elemente wie den Konstruktor zu schützen.
Betrachten Sie beispielsweise das folgende Szenario, in dem Sie eine Bibliothek und eine App haben, die die Bibliothek verwendet. Der Bibliotheks-Loader instanziiert eine StartupTask
-Schnittstelle, indem er die Klassenreferenz direkt übergibt.
Der Bibliotheks-Code 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 Datei mit Keep-Regeln 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 von Reflection funktionieren. So wird eine maximale Optimierung ermöglicht und gleichzeitig sichergestellt, dass der Code korrekt funktioniert. Durch die Regeln kann R8 den Klassennamen verschleiern und die Implementierung der StartupTask
-Klasse verkleinern oder entfernen, wenn sie in der App nie verwendet wird. Bei jeder Implementierung, z. B. der im Beispiel verwendeten PrecacheTask
, 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 jede Klasse, die IhreStartupTask
-Schnittstelle implementiert.-keep class * implements com.example.library.StartupTask
: Dadurch wird jede Klasse (*
) beibehalten, die Ihre Schnittstelle implementiert.,allowobfuscation
: Damit wird R8 angewiesen, die Klasse trotz Beibehaltung umzubenennen oder zu verschleiern. Das ist sicher, da Ihre Bibliothek nicht auf den Namen der Klasse angewiesen ist. Sie ruft dasClass
-Objekt direkt ab.,allowshrinking
: Mit diesem Modifikator wird R8 angewiesen, die Klasse zu entfernen, wenn sie nicht verwendet wird. So kann R8 eine Implementierung vonStartupTask
, die nie anTaskRunner.execute()
übergeben wird, sicher löschen. Kurz gesagt: Wenn eine App eine Klasse verwendet, dieStartupTask
implementiert, behält R8 die Klasse bei. R8 kann die Klasse umbenennen, um ihre Größe zu reduzieren, und sie löschen, wenn sie von der App nicht verwendet wird.
-keepclassmembers class * implements com.example.library.StartupTask { <init>(); }
: Diese Regel zielt auf bestimmte Mitglieder der Klassen ab, die in der ersten Regel identifiziert wurden – in diesem Fall der Konstruktor.-keepclassmembers class * implements com.example.library.StartupTask
: Dadurch werden bestimmte Elemente (Methoden, Felder) der Klasse beibehalten, die dieStartupTask
-Schnittstelle implementiert, aber nur, wenn die implementierte Klasse selbst beibehalten wird.{ <init>(); }
: Dies ist die Mitgliederauswahl.<init>
ist der spezielle interne Name für einen Konstruktor im Java-Bytecode. Dieser Teil bezieht sich speziell auf den Standardkonstruktor ohne Argumente.- Diese Regel ist wichtig, da in Ihrem Code
getDeclaredConstructor().newInstance()
ohne Argumente aufgerufen wird, wodurch der Standardkonstruktor reflektiv aufgerufen wird. Ohne diese Regel sieht R8, dass kein Codenew PreCacheTask()
direkt aufruft, geht davon aus, dass der Konstruktor nicht verwendet wird, und entfernt ihn. Dies führt dazu, dass Ihre App zur Laufzeit mit einemInstantiationException
abstürzt.
Reflexion basierend auf der Methodenannotation
In Bibliotheken werden häufig Anmerkungen definiert, mit denen Entwickler Methoden oder Felder taggen.
Die Bibliothek verwendet dann die Reflektion, um diese annotierten Elemente zur Laufzeit zu finden. Die Annotation @OnLifecycleEvent
wird beispielsweise verwendet, um die erforderlichen Methoden zur Laufzeit zu finden.
Sehen Sie sich beispielsweise das folgende Szenario an, in dem Sie eine Bibliothek und eine App haben, die die Bibliothek verwendet. Das Beispiel zeigt einen Eventbus, der Methoden findet und aufruft, die mit @OnEvent
annotiert sind.
Der Bibliotheks-Code 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 Datei mit Keep-Regeln für Nutzer enthalten, in der alle Methoden mit den zugehörigen Annotationen automatisch beibehalten werden:
-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
@com.example.library.OnEvent <methods>;
}
-keepattributes RuntimeVisibleAnnotations
: Mit dieser Regel werden Annotationen beibehalten, die zur Laufzeit gelesen werden sollen.-keep @interface com.example.library.OnEvent
: Mit dieser Regel wird die AnnotationsklasseOnEvent
selbst beibehalten.-keepclassmembers class * {@com.example.library.OnEvent <methods>;}
: Mit dieser Regel werden eine Klasse und bestimmte Mitglieder nur beibehalten, wenn die Klasse verwendet wird und diese Mitglieder enthält.-keepclassmembers
: Mit dieser Regel werden eine Klasse und bestimmte Elemente nur beibehalten, wenn die Klasse verwendet wird und diese Elemente enthält.class *
: Die Regel gilt für jede Klasse.@com.example.library.OnEvent <methods>;
: Dadurch werden alle Klassen beibehalten, die eine oder mehrere Methoden (<methods>
) mit der Annotation@com.example.library.OnEvent
haben, und auch die annotierten Methoden selbst werden beibehalten.
Reflexion basierend auf Klassenannotationen
Bibliotheken können die Reflektion verwenden, um nach Klassen zu suchen, die eine bestimmte Annotation haben. In diesem Fall findet die Task-Runner-Klasse mithilfe von Reflection alle Klassen, die mit ReflectiveExecutor
annotiert sind, und führt die Methode execute
aus.
Stellen Sie sich beispielsweise das folgende Szenario vor, in dem Sie eine Bibliothek und eine App haben, die die Bibliothek verwendet.
Die Bibliothek enthält 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 Reflexion verwendet, um bestimmte Klassen abzurufen, sollte sie eine Datei mit Keep-Regeln für den Consumer 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.
Reflection zur Unterstützung optionaler Abhängigkeiten
Ein häufiger Anwendungsfall für die Reflektion ist das Erstellen einer schwachen Abhängigkeit zwischen einer Kernbibliothek und einer optionalen Add-on-Bibliothek. Die Core-Bibliothek 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 Core-Bibliothek eine direkte Abhängigkeit von ihnen hat.
Die Core-Bibliothek verwendet die Reflektion (Class.forName
), um nach einer bestimmten Klasse anhand ihres Namens zu suchen. Wenn der Kurs gefunden wird, ist die Funktion aktiviert. Andernfalls schlägt der Vorgang fehl.
Im folgenden Code prüft beispielsweise ein Core AnalyticsManager
auf eine optionale VideoEventTracker
-Klasse, um die Videoanalyse zu aktivieren.
Die Core-Bibliothek enthält 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 enthält 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 dafür verantwortlich, die erforderliche Aufbewahrungsregel für Verbraucher bereitzustellen. Diese Keep-Regel sorgt dafür, dass jede App, die die optionale Bibliothek verwendet, den Code beibehält, den die Core-Bibliothek benötigt.
# 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 es in diesem Modul nicht direkt verwendet wird. Durch die Keep-Regel bleiben die Klasse und ihr Konstruktor erhalten, sodass die Core-Bibliothek sie instanziieren kann.
Reflection für den Zugriff auf private Member
Die Verwendung von Reflection 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. Dieser Code kann ohne Vorankündigung geändert werden, was zu unerwartetem Verhalten oder Abstürzen in Ihrer Anwendung führen kann.
Wenn Sie sich bei nicht öffentlichen APIs auf die Reflektion 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 Fehlerkorrekturen oder wichtige Sicherheitsupdates.
R8-Optimierungen und Reflection
Wenn Sie den privaten oder geschützten Code einer Bibliothek reflektieren müssen, achten Sie genau auf die Optimierungen von R8. Wenn es keine direkten Verweise auf diese Elemente 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
.
Das folgende Beispiel zeigt, wie Sie auf ein privates Feld einer Bibliotheksklasse zugreifen können.
Eine Bibliothek, die Ihnen nicht gehört, enthält den folgenden Code:
class LibraryClass {
private val secretMessage = "R8 will remove me"
}
Ihre App enthält 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 Elemente einer Klasse nur beibehalten, wenn die Klasse selbst beibehalten wird.class com.example.LibraryClass
: Hiermit wird auf die genaue Klasse verwiesen, die das Feld enthält.private java.lang.String secretMessage;
: Hiermit 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 ebenfalls 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 verhindert, dass native Methoden gekürzt werden.
-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 erkennen, welche Methoden von Ihrem nativen Code aufgerufen werden. Wenn es in Ihrer App keine direkten Verweise auf diese Methoden gibt, geht R8 fälschlicherweise davon aus, dass sie nicht verwendet werden, und entfernt sie, was zum Absturz Ihrer App führt.
Das folgende Beispiel zeigt eine Kotlin-Klasse mit einer Methode, die aus einer nativen Bibliothek aufgerufen werden soll. Die native Bibliothek instanziiert einen Anwendungstyp und übergibt Daten aus dem nativen 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, damit der Anwendungstyp nicht optimiert wird. Wenn in Methoden, die aus nativem Code aufgerufen werden, eigene Klassen in den Signaturen als Parameter oder Rückgabetypen verwendet werden, müssen Sie außerdem 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);}
: Damit werden bestimmte Elemente einer Klasse nur beibehalten, wenn die Klasse zuerst in Kotlin- oder Java-Code instanziiert wird. R8 wird dadurch mitgeteilt, dass die App die Klasse verwendet und bestimmte Elemente der Klasse beibehalten werden sollen.-keepclassmembers
: Dadurch werden bestimmte Elemente 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 bestimmte Elemente der Klasse beibehalten werden sollen.class com.example.JniBridge
: Hiermit wird auf die genaue Klasse verwiesen, die das Feld enthält.includedescriptorclasses
: Dieser Modifikator behält auch alle Klassen bei, die in der Signatur oder dem Deskriptor der Methode gefunden werden. In diesem Fall wird verhindert, dass R8 die Klassecom.example.models.NativeData
, die als Parameter verwendet wird, umbenennt oder entfernt. WennNativeData
umbenannt würde (z. B. ina.a
), würde die Methodensignatur nicht mehr dem entsprechen, was der native Code erwartet, was zu einem Absturz führen würde.public void onNativeEvent(com.example.models.NativeData);
: Gibt die genaue Java-Signatur der beizubehaltenden Methode an.
-keep class NativeData{<init>(java.lang.Integer, java.lang.String);}
:includedescriptorclasses
sorgt dafür, dass dieNativeData
-Klasse selbst beibehalten wird. Für alle Mitglieder (Felder oder Methoden) inNativeData
, auf die direkt über Ihren nativen JNI-Code zugegriffen wird, sind jedoch eigene Keep-Regeln erforderlich.-keep class NativeData
: Dies bezieht sich auf die Klasse mit dem NamenNativeData
. Der Block gibt an, welche Elemente in der KlasseNativeData
beibehalten werden sollen.<init>(java.lang.Integer, java.lang.String)
: Dies ist die Signatur des Konstruktors. Er identifiziert eindeutig den Konstruktor, der zwei Parameter akzeptiert: Der erste ist einInteger
und der zweite einString
.
Indirekte Plattformaufrufe
Daten mit einer Implementierung von Parcelable
übertragen
Das Android-Framework verwendet Reflection, um Instanzen Ihrer Parcelable
-Objekte zu erstellen. Bei 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.
Im folgenden Beispiel wird das kotlin-parcelize
-Plug-in verwendet, 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 Fall gibt es keine empfohlene Aufbewahrungsregel. Das kotlin-parcelize
-Gradle-Plugin generiert automatisch die erforderlichen Keep-Regeln für die Klassen, die Sie mit @Parcelize
annotieren. Die Komplexität wird für Sie übernommen und es wird dafür gesorgt, dass die generierten CREATOR
und Konstruktoren für die Reflexionsaufrufe des Android-Frameworks beibehalten werden.
Wenn Sie eine Parcelable
-Klasse manuell in Kotlin schreiben, ohne @Parcelize
zu verwenden, sind Sie dafür verantwortlich, das CREATOR
-Feld und den Konstruktor beizubehalten, der ein Parcel
akzeptiert. Wenn Sie das vergessen, stürzt Ihre App ab, wenn das System versucht, das 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 erforderlichenkeep
-Regeln, um diese Felder für eine ordnungsgemäße Funktion beizubehalten. - App-Entwickler müssen überprüfen, ob alle erforderlichen
keep
-Regeln vorhanden sind, insbesondere bei benutzerdefinierten Implementierungen oder Drittanbieterabhängigkeiten.