Gli esempi seguenti si basano su scenari comuni in cui utilizzi R8 per l'ottimizzazione, ma hai bisogno di indicazioni avanzate per creare regole di conservazione.
Riflesso
In generale, per prestazioni ottimali, non è consigliabile utilizzare la reflection. Tuttavia, in alcuni scenari, potrebbe essere inevitabile. Gli esempi seguenti forniscono indicazioni per le regole di conservazione in scenari comuni che utilizzano la reflection.
Reflection con classi caricate per nome
Le librerie spesso caricano le classi in modo dinamico utilizzando il nome della classe come String
.
Tuttavia, R8 non è in grado di rilevare le classi caricate in questo modo e potrebbe
rimuovere le classi che considera inutilizzate.
Ad esempio, considera lo scenario seguente in cui hai una libreria e un'app
che utilizza la libreria. Il codice mostra un caricatore di librerie che
crea un'istanza di un'interfaccia StartupTask
implementata da un'app.
Il codice della libreria è il seguente:
// 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()
}
}
L'app che utilizza la libreria ha il seguente codice:
// 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 questo scenario, la tua raccolta deve includere un file di regole di conservazione dei consumatori con le seguenti regole di conservazione:
-keep class * implements com.example.library.StartupTask {
<init>();
}
Senza questa regola, R8 rimuove PreCacheTask
dall'app perché l'app
non utilizza la classe direttamente, interrompendo l'integrazione. La regola trova le classi che implementano l'interfaccia StartupTask
della tua libreria e le conserva, insieme al relativo costruttore senza argomenti, consentendo alla libreria di istanziare ed eseguire correttamente PreCacheTask
.
Riflessione con ::class.java
Le librerie possono caricare le classi facendo passare direttamente l'oggetto Class
all'app, un metodo più affidabile rispetto al caricamento delle classi per nome. In questo modo viene creata
un riferimento forte alla classe che R8 può rilevare. Tuttavia, anche se ciò impedisce
a R8 di rimuovere la classe, devi comunque utilizzare una regola di conservazione per dichiarare che
la classe viene istanziata in modo riflessivo e per proteggere i membri a cui si accede
in modo riflessivo, come il costruttore.
Ad esempio, considera lo scenario seguente in cui hai una libreria e un'app che la utilizza: il caricatore della libreria crea un'interfaccia StartupTask
passando direttamente il riferimento alla classe.
Il codice della libreria è il seguente:
// 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()
}
}
L'app che utilizza la libreria ha il seguente codice:
// 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 questo scenario, la tua raccolta deve includere un file di regole di conservazione dei consumatori con le seguenti regole di conservazione:
# 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>();
}
Queste regole sono progettate per funzionare perfettamente con questo tipo di riflessione,
consentendo la massima ottimizzazione e assicurando che il codice funzioni
correttamente. Le regole consentono a R8 di offuscare il nome della classe e di ridurre o rimuovere l'implementazione della classe StartupTask
se l'app non la utilizza mai. Tuttavia,
per qualsiasi implementazione, ad esempio PrecacheTask
utilizzato nell'esempio,
viene conservato il costruttore predefinito (<init>()
) che la libreria deve chiamare.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
: questa regola ha come target qualsiasi classe che implementa l'interfacciaStartupTask
.-keep class * implements com.example.library.StartupTask
: in questo modo viene mantenuta qualsiasi classe (*
) che implementa l'interfaccia.,allowobfuscation
: indica a R8 che, nonostante la classe venga mantenuta, può rinominarla o offuscarla. Questo è sicuro perché la tua libreria non si basa sul nome della classe, ma ottiene l'oggettoClass
direttamente.,allowshrinking
: questo modificatore indica a R8 che può rimuovere la classe se non viene utilizzata. In questo modo R8 può eliminare in sicurezza un'implementazione diStartupTask
che non viene mai passata aTaskRunner.execute()
. In breve, questa regola implica quanto segue: se un'app utilizza una classe che implementaStartupTask
, R8 mantiene la classe. R8 può rinominare la classe per ridurne le dimensioni ed eliminarla se l'app non la utilizza.
-keepclassmembers class * implements com.example.library.StartupTask { <init>(); }
: Questa regola ha come target membri specifici delle classi identificate nella prima regola, in questo caso il costruttore.-keepclassmembers class * implements com.example.library.StartupTask
: conserva membri specifici (metodi, campi) della classe che implementa l'interfacciaStartupTask
, ma solo se la classe implementata stessa viene conservata.{ <init>(); }
: questo è il selettore dei membri.<init>
è il nome interno speciale di un costruttore nel bytecode Java. Questa parte ha come target specifico il costruttore predefinito senza argomenti.- Questa regola è fondamentale perché il codice chiama
getDeclaredConstructor().newInstance()
senza argomenti, il che richiama in modo riflessivo il costruttore predefinito. Senza questa regola, R8 vede che nessun codice chiama direttamentenew PreCacheTask()
, presuppone che il costruttore non venga utilizzato e lo rimuove. Ciò causa l'arresto anomalo dell'app in fase di esecuzione con unInstantiationException
.
Riflessione basata sull'annotazione del metodo
Le librerie spesso definiscono annotazioni che gli sviluppatori utilizzano per taggare metodi o campi.
La libreria utilizza quindi la reflection per trovare questi membri annotati in fase di runtime. Ad esempio, l'annotazione @OnLifecycleEvent
viene utilizzata per trovare i metodi richiesti
in fase di runtime.
Ad esempio, considera lo scenario seguente in cui hai una libreria e un'app che la utilizza. L'esempio mostra un bus di eventi che trova e richiama i metodi annotati con @OnEvent
.
Il codice della libreria è il seguente:
@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) { /* ... */ }
}
}
}
}
L'app che utilizza la libreria ha il seguente codice:
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)
}
La libreria deve includere un file di regole di conservazione per i consumatori che conservi automaticamente tutti i metodi che utilizzano le relative annotazioni:
-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
@com.example.library.OnEvent <methods>;
}
-keepattributes RuntimeVisibleAnnotations
: questa regola conserva le annotazioni che devono essere lette in fase di runtime.-keep @interface com.example.library.OnEvent
: questa regola conserva la classe di annotazioneOnEvent
stessa.-keepclassmembers class * {@com.example.library.OnEvent <methods>;}
: Questa regola conserva una classe e membri specifici solo se la classe viene utilizzata e contiene questi membri.-keepclassmembers
: questa regola conserva una classe e membri specifici solo se la classe viene utilizzata e contiene questi membri.class *
: la regola si applica a qualsiasi classe.@com.example.library.OnEvent <methods>;
: in questo modo viene mantenuta qualsiasi classe che abbia uno o più metodi (<methods>
) annotati con@com.example.library.OnEvent
e vengono mantenuti anche i metodi annotati.
Riflessione basata sulle annotazioni della classe
Le librerie possono utilizzare la reflection per cercare classi con un'annotazione specifica. In questo caso, la classe di esecuzione delle attività trova tutte le classi
annotate con ReflectiveExecutor
utilizzando la reflection ed esegue il metodo execute
.
Ad esempio, considera il seguente scenario in cui hai una libreria e un'app che utilizza la libreria.
La libreria contiene il seguente codice:
@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)
}
}
}
L'app che utilizza la libreria ha il seguente codice:
// 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)
}
Poiché la libreria utilizza la reflection per ottenere classi specifiche, la libreria deve includere un file di regole di conservazione per i consumatori con le seguenti regole di conservazione:
# 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();
}
Questa configurazione è molto efficiente perché indica a R8 esattamente cosa conservare.
Reflection per supportare le dipendenze facoltative
Un caso d'uso comune per la reflection è la creazione di una dipendenza debole tra una libreria di base e una libreria aggiuntiva facoltativa. La libreria principale può verificare se il componente aggiuntivo è incluso nell'app e, in caso affermativo, può attivare funzionalità aggiuntive. In questo modo puoi spedire moduli aggiuntivi senza forzare la libreria principale ad avere una dipendenza diretta da questi.
La libreria principale utilizza la reflection (Class.forName
) per cercare una classe specifica
in base al nome. Se il corso viene trovato, la funzionalità viene attivata. In caso contrario, non va a buon fine.
Ad esempio, considera il seguente codice in cui un controllo AnalyticsManager
di base
verifica la presenza di una classe VideoEventTracker
facoltativa per attivare l'analisi video.
La libreria principale contiene il seguente codice:
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())
}
}
}
La libreria video facoltativa contiene il seguente codice:
package com.example.analytics.video
class VideoEventTracker {
// This constructor must be kept for the reflection call to succeed.
init { /* ... */ }
}
Lo sviluppatore della libreria facoltativa è responsabile della fornitura della regola di conservazione necessaria per i consumatori. Questa regola di conservazione assicura che qualsiasi app che utilizza la libreria opzionale conservi il codice necessario alla libreria principale per essere trovato.
# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}
Senza questa regola, R8 probabilmente rimuove VideoEventTracker
dalla libreria facoltativa
poiché nulla in questo modulo lo utilizza direttamente. La regola keep conserva
la classe e il suo costruttore, consentendo alla libreria principale di istanziarla correttamente.
Riflessione per accedere ai membri privati
L'utilizzo della reflection per accedere a codice privato o protetto che non fa parte dell'API pubblica di una libreria può causare problemi significativi. Questo codice è soggetto a modifiche senza preavviso, il che può portare a comportamenti imprevisti o arresti anomali nell'applicazione.
Quando utilizzi la reflection per le API non pubbliche, potresti riscontrare i seguenti problemi:
- Aggiornamenti bloccati:le modifiche al codice privato o protetto possono impedire l'aggiornamento a versioni successive della libreria.
- Vantaggi mancati:potresti perdere nuove funzionalità, importanti correzioni di arresti anomali o aggiornamenti della sicurezza essenziali.
Ottimizzazioni e riflessioni di R8
Se devi eseguire la reflection nel codice privato o protetto di una libreria, presta molta
attenzione alle ottimizzazioni di R8. Se non sono presenti riferimenti diretti a questi
membri, R8 potrebbe presupporre che non vengano utilizzati e quindi rimuoverli o rinominarli.
Ciò può causare arresti anomali del runtime, spesso con messaggi di errore fuorvianti come
NoSuchMethodException
o NoSuchFieldException
.
Ad esempio, considera lo scenario seguente che mostra come potresti accedere a un campo privato da una classe di libreria.
Una libreria che non è di tua proprietà contiene il seguente codice:
class LibraryClass {
private val secretMessage = "R8 will remove me"
}
La tua app ha il seguente codice:
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
}
Aggiungi una regola -keep
nella tua app per impedire a R8 di rimuovere il campo privato:
-keepclassmembers class com.example.LibraryClass {
private java.lang.String secretMessage;
}
-keepclassmembers
: vengono conservati membri specifici di un corso solo se il corso stesso viene conservato.class com.example.LibraryClass
: questo ha come target la classe esatta contenente il campo.private java.lang.String secretMessage;
: identifica il campo privato specifico in base al nome e al tipo.
Java Native Interface (JNI)
Le ottimizzazioni di R8 possono presentare problemi quando si lavora con chiamate upcall dal codice nativo (C/C++) a Java o Kotlin. Sebbene sia vero anche il contrario, ovvero che le chiamate inverse da Java o
Kotlin al codice nativo possono presentare problemi, il file
proguard-android-optimize.txt
predefinito include la seguente regola per mantenere
il funzionamento delle chiamate inverse. Questa regola impedisce il taglio dei metodi nativi.
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
Interazione con il codice nativo tramite Java Native Interface (JNI)
Quando la tua app utilizza JNI per effettuare chiamate di ritorno dal codice nativo (C/C++) a Java o Kotlin, R8 non riesce a vedere quali metodi vengono chiamati dal tuo codice nativo. Se non sono presenti riferimenti diretti a questi metodi nella tua app, R8 presuppone erroneamente che questi metodi non vengano utilizzati e li rimuove, causando l'arresto anomalo dell'app.
L'esempio seguente mostra una classe Kotlin con un metodo destinato a essere chiamato da una libreria nativa. La libreria nativa crea un'istanza di un tipo di applicazione e trasferisce i dati dal codice nativo al codice Kotlin.
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 questo caso, devi informare R8 per impedire l'ottimizzazione del tipo di applicazione. Inoltre, se i metodi chiamati dal codice nativo utilizzano le tue classi nelle loro firme come parametri o tipi restituiti, devi anche verificare che queste classi non vengano rinominate.
Aggiungi le seguenti regole di conservazione alla tua app:
-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
public void onNativeEvent(com.example.model.NativeData);
}
-keep class NativeData{
<init>(java.lang.Integer, java.lang.String);
}
Queste regole di conservazione impediscono a R8 di rimuovere o rinominare il metodo onNativeEvent
e, cosa fondamentale, il suo tipo di parametro.
-keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}
: Conserva membri specifici di una classe solo se la classe viene istanziata prima nel codice Kotlin o Java. Indica a R8 che l'app utilizza la classe e che deve conservare membri specifici della classe.-keepclassmembers
: questo conserva membri specifici di una classe solo se la classe viene istanziata prima nel codice Kotlin o Java. Indica a R8 che l'app utilizza la classe e che deve conservare membri specifici della classe.class com.example.JniBridge
: questo ha come target la classe esatta contenente il campo.includedescriptorclasses
: questo modificatore conserva anche le classi trovate nella firma o nel descrittore del metodo. In questo caso, impedisce a R8 di rinominare o rimuovere la classecom.example.models.NativeData
, che viene utilizzata come parametro. SeNativeData
è stato rinominato (ad esempio ina.a
), la firma del metodo non corrisponderebbe più a ciò che si aspetta il codice nativo, causando un arresto anomalo.public void onNativeEvent(com.example.models.NativeData);
: questo specifica la firma Java esatta del metodo da conservare.
-keep class NativeData{<init>(java.lang.Integer, java.lang.String);}
: Sebbeneincludedescriptorclasses
si assicuri che la classeNativeData
stessa venga conservata, tutti i membri (campi o metodi) all'interno diNativeData
a cui si accede direttamente dal codice JNI nativo richiedono regole di conservazione proprie.-keep class NativeData
: questo ha come target la classe denominataNativeData
e il blocco specifica quali membri all'interno della classeNativeData
mantenere.<init>(java.lang.Integer, java.lang.String)
: questa è la firma del costruttore. Identifica in modo univoco il costruttore che accetta due parametri: il primo è unInteger
e il secondo è unString
.
Chiamate di piattaforma indirette
Trasferire i dati con un'implementazione di Parcelable
Il framework Android utilizza la reflection per creare istanze dei tuoi oggetti Parcelable
. Nello sviluppo Kotlin moderno, devi utilizzare il plug-in kotlin-parcelize
, che genera automaticamente l'implementazione Parcelable
necessaria, inclusi il campo CREATOR
e i metodi necessari al framework.
Ad esempio, considera il seguente esempio in cui il plug-in kotlin-parcelize
viene utilizzato per creare una classe Parcelable
:
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 questo scenario, non esiste una regola di conservazione consigliata. Il plug-in Gradle genera automaticamente le regole di conservazione richieste per le classi annotate con @Parcelize
.kotlin-parcelize
Gestisce la complessità per te, assicurandosi che i CREATOR
e i costruttori generati vengano conservati per le chiamate di reflection del framework Android.
Se scrivi manualmente una classe Parcelable
in Kotlin senza utilizzare @Parcelize
,
sei responsabile della gestione del campo CREATOR
e del costruttore che
accetta un Parcel
. Se dimentichi di farlo, l'app si arresta in modo anomalo quando il
sistema tenta di deserializzare l'oggetto. L'utilizzo di @Parcelize
è la pratica standard e più sicura.
Quando utilizzi il plug-in kotlin-parcelize
, tieni presente quanto segue:
- Il plug-in crea automaticamente i campi
CREATOR
durante la compilazione. - Il file
proguard-android-optimize.txt
contiene le regolekeep
necessarie per conservare questi campi per il corretto funzionamento. - Gli sviluppatori di app devono verificare che siano presenti tutte le regole
keep
richieste, soprattutto per implementazioni personalizzate o dipendenze di terze parti.