Los siguientes ejemplos se basan en situaciones comunes en las que usas R8 para la optimización, pero necesitas orientación avanzada para redactar reglas de conservación.
Reflexión
En general, para obtener un rendimiento óptimo, no se recomienda usar la reflexión. Sin embargo, en ciertas situaciones, podría ser inevitable. En los siguientes ejemplos, se brinda orientación sobre las reglas de conservación en situaciones comunes que usan la reflexión.
Reflexión con clases cargadas por nombre
Las bibliotecas suelen cargar clases de forma dinámica usando el nombre de la clase como un String
.
Sin embargo, R8 no puede detectar las clases que se cargan de esta manera y podría quitar las clases que considera que no se usan.
Por ejemplo, considera la siguiente situación en la que tienes una biblioteca y una app que usa la biblioteca. El código muestra un cargador de biblioteca que crea una instancia de una interfaz StartupTask
implementada por una app.
El código de la biblioteca es el siguiente:
// 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()
}
}
La app que usa la biblioteca tiene el siguiente código:
// 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")
}
En esta situación, tu biblioteca debe incluir un archivo de reglas de conservación del consumidor con las siguientes reglas de conservación:
-keep class * implements com.example.library.StartupTask {
<init>();
}
Sin esta regla, R8 quita PreCacheTask
de la app porque esta no usa la clase directamente, lo que interrumpe la integración. La regla encuentra las clases que implementan la interfaz StartupTask
de tu biblioteca y las conserva, junto con su constructor sin argumentos, lo que permite que la biblioteca cree instancias y ejecute PreCacheTask
correctamente.
Reflexión con ::class.java
Las bibliotecas pueden cargar clases haciendo que la app pase el objeto Class
directamente, lo que es un método más sólido que cargar clases por nombre. Esto crea una referencia sólida a la clase que R8 puede detectar. Sin embargo, si bien esto evita que R8 quite la clase, aún debes usar una regla de conservación para declarar que la clase se instancia de forma reflexiva y para proteger los miembros a los que se accede de forma reflexiva, como el constructor.
Por ejemplo, considera la siguiente situación en la que tienes una biblioteca y una app que la usa. El cargador de la biblioteca crea una instancia de una interfaz StartupTask
pasando la referencia de clase directamente.
El código de la biblioteca es el siguiente:
// 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()
}
}
La app que usa la biblioteca tiene el siguiente código:
// 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)
}
En esta situación, tu biblioteca debe incluir un archivo de reglas de conservación del consumidor con las siguientes reglas de conservación:
# 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>();
}
Estas reglas están diseñadas para funcionar perfectamente con este tipo de reflexión, lo que permite una optimización máxima y, al mismo tiempo, garantiza que el código funcione correctamente. Las reglas permiten que R8 ofusque el nombre de la clase y reduzca o quite la implementación de la clase StartupTask
si la app nunca la usa. Sin embargo, para cualquier implementación, como la PrecacheTask
que se usa en el ejemplo, conservan el constructor predeterminado (<init>()
) que tu biblioteca necesita llamar.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
: Esta regla se aplica a cualquier clase que implemente tu interfazStartupTask
.-keep class * implements com.example.library.StartupTask
: Esto conserva cualquier clase (*
) que implemente tu interfaz.,allowobfuscation
: Indica a R8 que, a pesar de mantener la clase, puede cambiarle el nombre o ofuscarla. Esto es seguro porque tu biblioteca no depende del nombre de la clase; obtiene el objetoClass
directamente.,allowshrinking
: Este modificador indica a R8 que puede quitar la clase si no se usa. Esto ayuda a R8 a borrar de forma segura una implementación deStartupTask
que nunca se pasa aTaskRunner.execute()
. En resumen, esta regla implica lo siguiente: Si una app usa una clase que implementaStartupTask
, R8 conserva la clase. R8 puede cambiar el nombre de la clase para reducir su tamaño y puede borrarla si la app no la usa.
-keepclassmembers class * implements com.example.library.StartupTask { <init>(); }
: Esta regla se dirige a miembros específicos de las clases que se identificaron en la primera regla, en este caso, el constructor.-keepclassmembers class * implements com.example.library.StartupTask
: Conserva miembros específicos (métodos, campos) de la clase que implementa la interfazStartupTask
, pero solo si se conserva la clase implementada.{ <init>(); }
: Este es el selector de miembros.<init>
es el nombre interno especial de un constructor en el código de bytes de Java. Esta parte se dirige específicamente al constructor predeterminado sin argumentos.- Esta regla es fundamental porque tu código llama a
getDeclaredConstructor().newInstance()
sin ningún argumento, lo que invoca de forma reflexiva al constructor predeterminado. Sin esta regla, R8 ve que ningún código llama directamente anew PreCacheTask()
, supone que el constructor no se usa y lo quita. Esto provoca que tu app falle en el tiempo de ejecución con unInstantiationException
.
run
Reflexión basada en la anotación del método
Las bibliotecas suelen definir anotaciones que los desarrolladores usan para etiquetar métodos o campos.
Luego, la biblioteca usa la reflexión para encontrar estos miembros anotados en el tiempo de ejecución. Por ejemplo, la anotación @OnLifecycleEvent
se usa para encontrar los métodos requeridos en el tiempo de ejecución.
Por ejemplo, considera la siguiente situación en la que tienes una biblioteca y una app que usa la biblioteca. El ejemplo muestra un bus de eventos que encuentra e invoca métodos anotados con @OnEvent
.
El código de la biblioteca es el siguiente:
@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) { /* ... */ }
}
}
}
}
La app que usa la biblioteca tiene el siguiente código:
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 biblioteca debe incluir un archivo de reglas de conservación del consumidor que conserve automáticamente cualquier método que use sus anotaciones:
-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
@com.example.library.OnEvent <methods>;
}
-keepattributes RuntimeVisibleAnnotations
: Esta regla conserva las anotaciones que se deben leer durante el tiempo de ejecución.-keep @interface com.example.library.OnEvent
: Esta regla conserva la clase de anotaciónOnEvent
en sí.-keepclassmembers class * {@com.example.library.OnEvent <methods>;}
: Esta regla conserva una clase y miembros específicos solo si se usa la clase y esta contiene esos miembros.-keepclassmembers
: Esta regla conserva una clase y miembros específicos solo si se usa la clase y esta contiene esos miembros.class *
: La regla se aplica a cualquier clase.@com.example.library.OnEvent <methods>;
: Conserva cualquier clase que tenga uno o más métodos (<methods>
) anotados con@com.example.library.OnEvent
y también conserva los métodos anotados.
Reflexión basada en las anotaciones de la clase
Las bibliotecas pueden usar la reflexión para buscar clases que tengan una anotación específica. En este caso, la clase del ejecutor de tareas encuentra todas las clases anotadas con ReflectiveExecutor
usando la reflexión y ejecuta el método execute
.
Por ejemplo, considera la siguiente situación en la que tienes una biblioteca y una app que la usa.
La biblioteca tiene el siguiente código:
@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)
}
}
}
La app que usa la biblioteca tiene el siguiente código:
// 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)
}
Dado que la biblioteca usa la reflexión de forma reflexiva para obtener clases específicas, debe incluir un archivo de reglas de conservación del consumidor con las siguientes reglas de conservación:
# 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();
}
Esta configuración es muy eficiente porque le indica a R8 exactamente qué conservar.
Reflexión para admitir dependencias opcionales
Un caso de uso común de la reflexión es crear una dependencia flexible entre una biblioteca principal y una biblioteca complementaria opcional. La biblioteca principal puede verificar si el complemento está incluido en la app y, si lo está, puede habilitar funciones adicionales. Esto te permite enviar módulos de complementos sin obligar a la biblioteca principal a tener una dependencia directa de ellos.
La biblioteca principal usa la reflexión (Class.forName
) para buscar una clase específica por su nombre. Si se encuentra la clase, se habilita la función. De lo contrario, falla de forma correcta.
Por ejemplo, considera el siguiente código en el que un AnalyticsManager
principal verifica si hay una clase VideoEventTracker
opcional para habilitar las estadísticas de video.
La biblioteca principal tiene el siguiente código:
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 biblioteca de videos opcional tiene el siguiente código:
package com.example.analytics.video
class VideoEventTracker {
// This constructor must be kept for the reflection call to succeed.
init { /* ... */ }
}
El desarrollador de la biblioteca opcional es responsable de proporcionar la regla de conservación del consumidor necesaria. Esta regla de conservación garantiza que cualquier app que use la biblioteca opcional conserve el código que necesita la biblioteca principal para encontrar.
# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}
Sin esta regla, es probable que R8 quite VideoEventTracker
de la biblioteca opcional, ya que nada en ese módulo la usa directamente. La regla de conservación preserva la clase y su constructor, lo que permite que la biblioteca principal la cree una instancia correctamente.
Reflexión para acceder a miembros privados
Usar la reflexión para acceder a código privado o protegido que no forma parte de la API pública de una biblioteca puede generar problemas significativos. Este código está sujeto a cambios sin previo aviso, lo que puede provocar comportamientos inesperados o fallas en tu aplicación.
Cuando dependes de la reflexión para las APIs no públicas, es posible que surjan los siguientes problemas:
- Actualizaciones bloqueadas: Los cambios en el código privado o protegido pueden impedir que actualices a versiones superiores de la biblioteca.
- Beneficios perdidos: Es posible que te pierdas nuevas funciones, correcciones importantes de fallas o actualizaciones de seguridad esenciales.
Optimizaciones de R8 y reflexión
Si debes realizar una reflexión en el código privado o protegido de una biblioteca, presta mucha atención a las optimizaciones de R8. Si no hay referencias directas a estos miembros, es posible que R8 suponga que no se usan y, posteriormente, los quite o les cambie el nombre.
Esto puede provocar fallas durante el tiempo de ejecución, a menudo con mensajes de error engañosos, como NoSuchMethodException
o NoSuchFieldException
.
Por ejemplo, considera la siguiente situación que demuestra cómo podrías acceder a un campo privado desde una clase de biblioteca.
Una biblioteca que no es tuya tiene el siguiente código:
class LibraryClass {
private val secretMessage = "R8 will remove me"
}
Tu app tiene el siguiente código:
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
}
Agrega una regla -keep
en tu app para evitar que R8 quite el campo privado:
-keepclassmembers class com.example.LibraryClass {
private java.lang.String secretMessage;
}
-keepclassmembers
: Esto conserva miembros específicos de una clase solo si la clase en sí se retiene.class com.example.LibraryClass
: Segmenta la clase exacta que contiene el campo.private java.lang.String secretMessage;
: Identifica el campo privado específico por su nombre y tipo.
Interfaz nativa de Java (JNI)
Las optimizaciones de R8 pueden tener problemas cuando se trabaja con llamadas ascendentes desde código nativo (C/C++) a Java o Kotlin. Si bien lo contrario también es cierto (las llamadas descendentes de Java o Kotlin al código nativo pueden tener problemas), el archivo predeterminado proguard-android-optimize.txt
incluye la siguiente regla para que las llamadas descendentes sigan funcionando. Esta regla protege contra el recorte de métodos nativos.
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
Interacción con código nativo a través de la interfaz nativa de Java (JNI)
Cuando tu app usa JNI para realizar llamadas ascendentes desde código nativo (C/C++) a Java o Kotlin, R8 no puede ver qué métodos se llaman desde tu código nativo. Si no hay referencias directas a estos métodos en tu app, R8 supone incorrectamente que no se usan y los quita, lo que provoca que la app falle.
En el siguiente ejemplo, se muestra una clase de Kotlin con un método que se puede llamar desde una biblioteca nativa. La biblioteca nativa crea una instancia de un tipo de aplicación y pasa datos del código nativo al código de 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")
}
}
}
En este caso, debes informar a R8 para evitar que se optimice el tipo de aplicación. Además, si los métodos llamados desde el código nativo usan tus propias clases en sus firmas como parámetros o tipos de devolución, también debes verificar que no se cambie el nombre de esas clases.
Agrega las siguientes reglas de conservación a tu app:
-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
public void onNativeEvent(com.example.model.NativeData);
}
-keep class NativeData{
<init>(java.lang.Integer, java.lang.String);
}
Estas reglas de conservación evitan que R8 quite o cambie el nombre del método onNativeEvent
y, lo que es fundamental, su tipo de parámetro.
-keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}
: Conserva miembros específicos de una clase solo si la clase se instancia primero en código Kotlin o Java. Le indica a R8 que la app usa la clase y que debe conservar miembros específicos de la clase.-keepclassmembers
: Conserva miembros específicos de una clase solo si la clase se instancia primero en código Kotlin o Java. Le indica a R8 que la app usa la clase y que debe conservar miembros específicos de la clase.class com.example.JniBridge
: Segmenta la clase exacta que contiene el campo.includedescriptorclasses
: Este modificador también conserva las clases que se encuentran en la firma o el descriptor del método. En este caso, evita que R8 cambie el nombre o quite la clasecom.example.models.NativeData
, que se usa como parámetro. Si se cambiara el nombre deNativeData
(por ejemplo, aa.a
), la firma del método ya no coincidiría con lo que espera el código nativo, lo que provocaría una falla.public void onNativeEvent(com.example.models.NativeData);
: Especifica la firma exacta de Java del método que se debe conservar.
-keep class NativeData{<init>(java.lang.Integer, java.lang.String);}
: Si bienincludedescriptorclasses
garantiza que la claseNativeData
en sí se conserve, todos los miembros (campos o métodos) dentro deNativeData
a los que se accede directamente desde tu código JNI nativo necesitan sus propias reglas de conservación.-keep class NativeData
: Este destino se dirige a la clase llamadaNativeData
y el bloque especifica qué miembros dentro de la claseNativeData
se deben conservar.<init>(java.lang.Integer, java.lang.String)
: Esta es la firma del constructor. Identifica de forma única el constructor que toma dos parámetros: el primero es unInteger
y el segundo es unString
.
Llamadas indirectas a la plataforma
Transfiere datos con una implementación de Parcelable
El framework de Android usa la reflexión para crear instancias de tus objetos Parcelable
. En el desarrollo moderno de Kotlin, debes usar el complemento kotlin-parcelize
, que genera automáticamente la implementación de Parcelable
necesaria, incluidos el campo CREATOR
y los métodos que necesita el framework.
Por ejemplo, considera el siguiente ejemplo en el que se usa el complemento kotlin-parcelize
para crear una clase 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
En este caso, no hay una regla de conservación recomendada. El complemento de Gradle kotlin-parcelize
genera automáticamente las reglas de conservación requeridas para las clases que anotas con @Parcelize
. Maneja la complejidad por ti y se asegura de que los constructores y CREATOR
generados se conserven para las llamadas de reflexión del framework de Android.
Si escribes una clase Parcelable
de forma manual en Kotlin sin usar @Parcelize
, eres responsable de mantener el campo CREATOR
y el constructor que acepta un Parcel
. Si olvidas hacerlo, la app fallará cuando el sistema intente deserializar tu objeto. Usar @Parcelize
es la práctica estándar y más segura.
Cuando uses el complemento kotlin-parcelize
, ten en cuenta lo siguiente:
- El complemento crea automáticamente campos
CREATOR
durante la compilación. - El archivo
proguard-android-optimize.txt
contiene las reglaskeep
necesarias para conservar estos campos y garantizar la funcionalidad adecuada. - Los desarrolladores de aplicaciones deben verificar que estén presentes todas las reglas de
keep
requeridas, en especial para las implementaciones personalizadas o las dependencias de terceros.