R8 proporciona dos modos: el modo de compatibilidad y el modo completo. El modo completo te brinda optimizaciones potentes que mejoran el rendimiento de tu app.
Esta guía está destinada a los desarrolladores de Android que desean usar las optimizaciones más potentes de R8. Explora las diferencias clave entre el modo de compatibilidad y el modo completo, y proporciona las configuraciones explícitas necesarias para migrar tu proyecto de forma segura y evitar fallas comunes en el tiempo de ejecución.
Cómo habilitar el modo completo
Para habilitar el modo completo, quita la siguiente línea de tu archivo gradle.properties:
android.enableR8.fullMode=false // Remove this line to enable full mode
Conserva las clases asociadas con los atributos
Los atributos son metadatos almacenados en archivos de clase compilados que no forman parte del código ejecutable. Sin embargo, pueden ser necesarias para ciertos tipos de reflexión. Algunos ejemplos comunes incluyen Signature (que conserva la información del tipo genérico después del borrado de tipo), InnerClasses y EnclosingMethod (para la reflexión sobre la estructura de la clase) y las anotaciones visibles en el tiempo de ejecución.
En el siguiente código, se muestra cómo se ve un atributo Signature para un campo en bytecode. En el caso de un campo, haz lo siguiente:
List<User> users;
El archivo de clase compilado contendría el siguiente bytecode:
.field public static final users:Ljava/util/List;
.annotation system Ldalvik/annotation/Signature;
value = {
"Ljava/util/List<",
"Lcom/example/package/User;",
">;"
}
.end annotation
.end field
Las bibliotecas que usan mucho la reflexión (como Gson) suelen depender de estos atributos para inspeccionar y comprender de forma dinámica la estructura de tu código. De forma predeterminada, en el modo completo de R8, los atributos se conservan solo si la clase, el campo o el método asociados se conservan de forma explícita.
En el siguiente ejemplo, se demuestra por qué son necesarios los atributos y qué reglas de conservación debes agregar cuando migras del modo de compatibilidad al modo completo.
Considera el siguiente ejemplo en el que deserializamos una lista de usuarios con la biblioteca de Gson.
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
data class User(
@SerializedName("username")
var username: String? = null,
@SerializedName("age")
var age: Int = 0
)
fun GsonRemoteJsonListExample() {
val gson = Gson()
// 1. The JSON string for a list of users returned from remote
val jsonOutput = """[{"username":"alice","age":30}, {"username":"bob","age":25}]"""
// 2. Deserialize the JSON string into a List<User>
// We must use TypeToken for generic types like List
val listType = object : TypeToken<List<User>>() {}.type
val deserializedList: List<User> = gson.fromJson(jsonOutput, listType)
// Print the list
println("First user from list: ${deserializedList}")
}
Durante la compilación, el borrado de tipos de Java quita los argumentos de tipos genéricos. Esto significa que, en el tiempo de ejecución, tanto List<String> como List<User> aparecen como un List sin procesar. Por lo tanto, las bibliotecas como Gson, que dependen de la reflexión, no pueden determinar los tipos de objetos específicos que se declaró que contenía List cuando se deserializa una lista JSON, lo que puede generar problemas en el tiempo de ejecución.
Para conservar la información de tipo, Gson usa TypeToken. El ajuste TypeToken conserva la información de deserialización necesaria.
La expresión de Kotlin object:TypeToken<List<User>>() {}.type crea una clase interna anónima que extiende TypeToken y captura la información del tipo genérico. En este ejemplo, la clase anónima se llama $GsonRemoteJsonListExample$listType$1.
El lenguaje de programación Java guarda la firma genérica de una superclase como metadatos, conocidos como el atributo Signature, dentro del archivo de clase compilado.
Luego, TypeToken usa estos metadatos de Signature para recuperar el tipo en el tiempo de ejecución.
Esto permite que Gson use la reflexión para leer el Signature y descubrir correctamente el tipo List<User> completo que necesita para la deserialización.
Cuando R8 está habilitado en el modo de compatibilidad, conserva el atributo Signature para las clases, incluidas las clases internas anónimas como $GsonRemoteJsonListExample$listType$1, incluso si no se definen reglas de conservación específicas de forma explícita. Como resultado, el modo de compatibilidad de R8 no requiere ninguna regla de conservación explícita adicional para que este ejemplo funcione según lo esperado.
// keep rule for compatibility mode
-keepattributes Signature
Cuando R8 está habilitado en modo completo, se quita el atributo Signature de la clase interna anónima $GsonRemoteJsonListExample$listType$1. Sin esta información de tipo en Signature, Gson no puede encontrar el tipo de aplicación correcto, lo que genera un IllegalStateException. Las reglas de conservación necesarias para evitar esto son las siguientes:
// keep rule required for full mode
-keepattributes Signature
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken
-keepattributes Signature: Esta regla indica a R8 que conserve el atributo que Gson necesita leer. En el modo completo, R8 solo conserva el atributoSignaturepara las clases, los campos o los métodos que coinciden de forma explícita con una reglakeep.-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken: Esta regla es necesaria porqueTypeTokenencapsula el tipo del objeto que se deserializa. Después del borrado de tipos, se crea una clase interna anónima para conservar la información del tipo genérico. Si no se conservacom.google.gson.reflect.TypeTokende forma explícita, R8 en modo completo no incluirá este tipo de clase en el atributoSignaturenecesario para la deserialización.-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken: Esta regla conserva la información de tipo de las clases anónimas que extiendenTypeToken, como$GsonRemoteJsonListExample$listType$1en este ejemplo. Sin esta regla, R8 en modo completo quita la información de tipo necesaria, lo que provoca que falle la deserialización.
A partir de la versión 2.11.0 de Gson, la biblioteca incluye las reglas de conservación necesarias para la deserialización en modo completo. Cuando compilas tu app con R8 habilitado, R8 encuentra y aplica automáticamente estas reglas de la biblioteca. Esto proporciona la protección que necesita tu app sin que tengas que agregar o mantener manualmente estas reglas específicas en tu proyecto.
Es importante comprender que las reglas que se compartieron anteriormente solo resuelven el problema de descubrir el tipo genérico (p.ej., List<User>).
R8 también cambia el nombre de los campos de las clases. Si no usas anotaciones @SerializedName en tus modelos de datos, Gson no podrá deserializar JSON porque los nombres de los campos ya no coincidirán con las claves JSON.
Sin embargo, si usas una versión de Gson anterior a la 2.11 o si tus modelos no usan la anotación @SerializedName, debes agregar reglas de conservación explícitas para esos modelos.
Conserva el constructor predeterminado
En el modo completo de R8, el constructor predeterminado o sin argumentos no se conserva de forma implícita, incluso cuando se retiene la clase. Si creas una instancia de una clase con class.getDeclaredConstructor().newInstance() o class.newInstance(), debes conservar de forma explícita el constructor sin argumentos en el modo completo. En cambio, el modo de compatibilidad siempre conserva el constructor sin argumentos.
Considera un ejemplo en el que se crea una instancia de PrecacheTask con la reflexión para llamar de forma dinámica a su método run. Si bien este caso no requiere reglas adicionales en el modo de compatibilidad, en el modo completo, se quitaría el constructor predeterminado de PrecacheTask. Por lo tanto, se requiere una regla de conservación específica.
// In library
interface StartupTask {
fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
fun execute(taskClass: Class<out StartupTask>) {
// The class isn't removed, but its constructor might be.
val task = taskClass.getDeclaredConstructor().newInstance()
task.run()
}
}
// In app
class PreCacheTask : StartupTask {
override fun run() {
Log.d("Pre cache task", "Warming up the cache...")
}
}
fun runTaskRunner() {
// The library is given a direct reference to the app's task class.
TaskRunner.execute(PreCacheTask::class.java)
}
# Full mode keep rule
# default constructor needs to be specified
-keep class com.example.fullmoder8.PreCacheTask {
<init>();
}
La modificación del acceso está habilitada de forma predeterminada
En el modo de compatibilidad, R8 no altera la visibilidad de los métodos y los campos dentro de una clase. Sin embargo, en el modo completo, R8 mejora la optimización cambiando la visibilidad de tus métodos y campos, por ejemplo, de privado a público. Esto permite una mayor inserción.
Esta optimización puede causar problemas si tu código usa la reflexión que depende específicamente de que los miembros tengan una visibilidad particular. R8 no reconocerá este uso indirecto, lo que podría provocar fallas en la app. Para evitar esto, debes agregar reglas específicas de -keep para conservar a los miembros, lo que también conservará su visibilidad original.
Para obtener más información, consulta este ejemplo para comprender por qué no se recomienda acceder a miembros privados con la reflexión y las reglas de conservación para retener esos campos o métodos.