Articles

Práctica ProGuard reglas de ejemplos

wojtek se Kaliciński
wojtek se Kaliciński

Seguir

20 de Febrero, 2018 · 9 min leer

En mi anterior artículo que explica por qué todo el mundo debería utilizar ProGuard para sus aplicaciones de Android, de cómo habilitar y qué tipo de errores que pueden surgir a la hora de hacerlo. Había mucha teoría involucrada, ya que creo que es importante entender los principios subyacentes para estar preparado para lidiar con cualquier problema potencial.

También hablé en un artículo separado sobre el problema muy específico de configurar ProGuard para una compilación instantánea de aplicaciones.

En esta parte, me gustaría hablar sobre los ejemplos prácticos de reglas de ProGuard en una aplicación de muestra de tamaño mediano: Plaid de Nick Butcher.

Plaid en realidad resultó ser un gran tema para investigar problemas de ProGuard, ya que contiene una mezcla de bibliotecas de terceros que usan cosas como procesamiento de anotaciones y generación de código, reflexión, carga de recursos java y código nativo (JNI). He extraído y escrito algunos consejos prácticos que se deben aplicar a otras aplicaciones en general:

clases de Datos

public class User {
String name;
int age;
...
}

Probablemente, cada aplicación tiene algún tipo de clase de datos (también conocido como DMOs, modelos, etc. dependiendo del contexto y del lugar en el que se encuentren en la arquitectura de la aplicación). Lo que pasa con los objetos de datos es que, por lo general, en algún momento se cargarán o guardarán (serializados) en algún otro medio, como la red (una solicitud HTTP), una base de datos (a través de un OR), un archivo JSON en disco o en un almacén de datos Firebase.

Muchas de las herramientas que simplifican la serialización y deserialización de estos campos se basan en la reflexión. GSON, Retrofit, Firebase: todos inspeccionan los nombres de campo en las clases de datos y los convierten en otra representación (por ejemplo: {"name”: "Sue”, "age”: 28}), ya sea para transporte o almacenamiento. Lo mismo sucede cuando se leen los datos en un objeto Java — ven un par clave-valor "name”:”John” y tratar de aplicarlo a un objeto Java buscando un String name campo.

Conclusión: No podemos permitir que ProGuard cambie el nombre o elimine ningún campo de estas clases de datos, ya que tienen que coincidir con el formato serializado. Es una apuesta segura agregar una anotación @Keep en toda la clase o una regla comodín en todos sus modelos:

-keep class io.plaidapp.data.api.dribbble.model.** { *; }

Advertencia: Es posible cometer un error al probar si su aplicación es susceptible a este problema. Por ejemplo, si serializa un objeto en JSON y lo guarda en el disco en la versión N de su aplicación sin las reglas de conservación adecuadas, los datos guardados pueden tener este aspecto: {"a”: "Sue”, "b”: 28}. Debido a que ProGuard cambió el nombre de sus campos a a y b, todo parecerá funcionar, los datos se guardarán y cargarán correctamente.

Sin embargo, cuando vuelva a compilar su aplicación y lance la versión N+1 de su aplicación, ProGuard podría decidir cambiar el nombre de sus campos a algo diferente, como c y d. Como resultado, los datos guardados anteriormente no se cargarán.

En primer lugar, debe asegurarse de tener las reglas de mantenimiento adecuadas.

El código Java llamado desde el lado nativo (JNI)

Los archivos ProGuard predeterminados de Android (siempre debe incluirlos, tienen algunas reglas realmente útiles) ya contienen una regla para los métodos que se implementan en el lado nativo (-keepclasseswithmembernames class * { native <methods>; }). Desafortunadamente, no hay una manera general de mantener el código invocado en la dirección opuesta: de JNI a Java.

Con JNI es completamente posible construir un objeto JVM o encontrar y llamar a un método en un controlador JVM desde código C/C++ y, de hecho, una de las bibliotecas utilizadas en Plaid lo hace.

Conclusión: Debido a que ProGuard solo puede inspeccionar clases Java, no sabrá sobre los usos que ocurren en el código nativo. Debemos conservar explícitamente dichos usos de clases y miembros a través de una anotación @Keep o una regla -keep.

-keep, includedescriptorclasses 
class in.uncod.android.bypass.Document { *; }
-keep, includedescriptorclasses
class in.uncod.android.bypass.Element { *; }

Abrir recursos desde JAR/APK

Android tiene su propio sistema de recursos y activos que normalmente no debería ser un problema para ProGuard. Sin embargo, en Java simple hay otro mecanismo para cargar recursos directamente desde un archivo JAR y algunas bibliotecas de terceros podrían usarlo incluso cuando se compilan en aplicaciones Android (en ese caso, intentarán cargarlo desde el APK).

El problema es que, por lo general, estas clases buscarán recursos bajo su propio nombre de paquete (lo que se traduce en una ruta de archivo en el JAR o APK). ProGuard puede cambiar el nombre de los paquetes cuando se ofuscan, por lo que después de la compilación puede ocurrir que la clase y su archivo de recursos ya no estén en el mismo paquete en el APK final.

Para identificar los recursos de carga de esta manera, puede buscar llamadas a Class.getResourceAsStream / getResource y ClassLoader.getResourceAsStream / getResource en su código y en cualquier biblioteca de terceros de la que dependa.

Conclusión: Debemos mantener el nombre de cualquier clase que cargue recursos del APK utilizando este mecanismo.

En Plaid, en realidad hay dos: uno en la biblioteca OkHttp y uno en Jsoup:

-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
-keepnames class org.jsoup.nodes.Entities

Cómo crear reglas para bibliotecas de terceros

En un mundo ideal, cada dependencia que use proporcionaría sus reglas de ProGuard requeridas en el AAR. A veces se olvidan de hacer esto o solo publican frascos, que no tienen una forma estandarizada de suministrar reglas de ProGuard.

En ese caso, antes de comenzar a depurar su aplicación y crear reglas, recuerde revisar la documentación. Algunos autores de bibliotecas proporcionan reglas de ProGuard recomendadas (como la adaptación utilizada en cuadros escoceses) que pueden ahorrarle mucho tiempo y frustración. Desafortunadamente, muchas bibliotecas no lo hacen (como es el caso de Jsoup y Bypass mencionado en este artículo). También tenga en cuenta que en algunos casos la configuración suministrada con la biblioteca solo funcionará con optimizaciones deshabilitadas, por lo que si las está activando, podría estar en un territorio desconocido.

Entonces, ¿cómo crear reglas cuando la biblioteca no las suministra?
Solo puedo darte algunos consejos:

  1. Leer la salida de compilación y logcat!
  2. Construir advertencias le dirán que el -dontwarn reglas para agregar
  3. ClassNotFoundExceptionMethodNotFoundException y FieldNotFoundException le dirán que el -keep reglas para agregar

Usted debe estar contento cuando su aplicación se bloquea con ProGuard habilitado — vas a tener un lugar para comenzar su investigación 🙂

La peor clase de problemas para depurar cuando la aplicación funciona, pero por ejemplo no muestra una pantalla o no carga los datos de la red.

Ahí es donde debes considerar algunos de los escenarios que describí en este artículo y ensuciarte las manos, incluso sumergirte en el código de terceros y comprender por qué podría fallar, como cuando usa reflexión, introspección o JNI.

Depuración y seguimiento de pila

ProGuard eliminará de forma predeterminada muchos atributos de código y metadatos ocultos que no son necesarios para la ejecución del programa . Algunos de ellos son realmente útiles para el desarrollador, por ejemplo, es posible que desee conservar los nombres de archivo de origen y los números de línea para los rastros de pila para facilitar la depuración:

-keepattributes SourceFile, LineNumberTable

También debe recordar guardar los archivos de asignaciones de ProGuard producidos al compilar una versión de lanzamiento y cargarlos para reproducirlos para obtener rastros de pila usuarios.

Si va a adjuntar un depurador para pasar por el código de método en una compilación programada de su aplicación, también debe mantener los siguientes atributos para conservar información de depuración sobre variables locales (solo necesita esta línea en su debug tipo de compilación):

-keepattributes LocalVariableTable, LocalVariableTypeTable

Tipo de compilación de depuración minificada

Los tipos de compilación predeterminados están configurados de forma que debug no ejecuta ProGuard. Eso tiene sentido, porque queremos iterar y compilar rápido al desarrollar, pero aún así queremos que la compilación de la versión que use ProGuard sea lo más pequeña y optimizada posible.

Pero para probar y depurar completamente cualquier problema de ProGuard, es bueno configurar una compilación de depuración separada y minificada como esta:

buildTypes {
debugMini {
initWith debug
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
matchingFallbacks =
}
}

Con este tipo de compilación, podrá conectar el depurador, ejecutar pruebas de interfaz de usuario (también en un servidor CI) o probar monkey su aplicación para detectar posibles problemas en una compilación que se acerque lo más posible a su compilación de lanzamiento.

Conclusión: Cuando usas ProGuard, siempre debes controlar el control de calidad de tu versión, ya sea realizando pruebas de extremo a extremo o revisando manualmente todas las pantallas de tu aplicación para ver si falta algo o se bloquea.

Anotaciones en tiempo de ejecución, escriba introspección

ProGuard eliminará de forma predeterminada todas las anotaciones e incluso parte de la información de tipo excedente de su código. Para algunas bibliotecas, eso no es un problema: las que procesan anotaciones y generan código en tiempo de compilación (como Dagger 2 o Glide y muchas más) podrían no necesitar estas anotaciones más adelante cuando se ejecute el programa.

Hay otra clase de herramientas que inspeccionan las anotaciones o observan la información de tipos de parámetros y excepciones en tiempo de ejecución. Retrofit, por ejemplo, lo hace interceptando las llamadas a sus métodos usando un objeto Proxy, luego mira las anotaciones y la información de tipo para decidir qué colocar o leer de la solicitud HTTP.

Conclusión: A veces es necesario conservar la información de tipo y las anotaciones que se leen en tiempo de ejecución, en lugar de en tiempo de compilación. Puede consultar la lista de atributos en el manual de ProGuard.

-keepattributes *Annotation*, Signature, Exception

Si está utilizando el archivo de configuración predeterminado de Android ProGuard (getDefaultProguardFile('proguard-android.txt')), las dos primeras opciones, Anotaciones y firma, se especifican para usted. Si no estás usando el valor predeterminado, debes asegurarte de agregarlos tú mismo (tampoco está de más duplicarlos si sabes que son un requisito para tu aplicación).

Mover todo al paquete predeterminado

La opción-repackageclasses no se agrega de forma predeterminada en la configuración de ProGuard. Si ya está ofuscando su código y ha solucionado cualquier problema con las reglas de mantenimiento adecuadas, puede agregar esta opción para reducir aún más el tamaño DEX. Funciona moviendo todas las clases al paquete predeterminado (raíz), esencialmente liberando el espacio ocupado por cadenas como «com.ejemplo.myapp.somepackage».

-repackageclasses

Optimizaciones de ProGuard

Como mencioné anteriormente, ProGuard puede hacer 3 cosas por usted:

  1. se deshace del código no utilizado,
  2. cambia el nombre de los identificadores para reducir el código,
  3. realiza optimizaciones de todo el programa.

Como yo lo veo, todos deberían intentar configurar su compilación para obtener 1. y 2. trabajo.

Para desbloquear 3. (optimizaciones adicionales), debe usar un archivo de configuración de ProGuard predeterminado diferente. Cambie el parámetro proguard-android.txt a proguard-android-optimize.txt en su build.gradle:

release {
minifyEnabled true
proguardFiles
getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}

Esto hará que la compilación de su versión sea más lenta, pero potencialmente hará que su aplicación se ejecute más rápido y reducirá el tamaño del código aún más, gracias a optimizaciones como la inserción de métodos, la fusión de clases y la eliminación de código más agresiva. Sin embargo, esté preparado para que pueda introducir errores nuevos y difíciles de diagnosticar, así que úselo con precaución y si algo no funciona, asegúrese de deshabilitar ciertas optimizaciones o deshabilitar el uso de la configuración de optimización por completo.

En el caso de Plaid, las optimizaciones de ProGuard interfirieron con la forma en que la adaptación utiliza objetos Proxy sin implementaciones concretas, y eliminaron algunos parámetros de método que realmente eran necesarios. Tuve que añadir esta línea a mi configuración:

-optimizations !method/removal/parameter

Puede encontrar una lista de posibles optimizaciones y cómo deshabilitarlas en el manual de ProGuard.

Cuándo usar @Keep y-keep

@Keep la compatibilidad se implementa como un conjunto de reglas -keep en el archivo de reglas ProGuard de Android predeterminado, por lo que son esencialmente equivalentes. Especificar -keep reglas es más flexible, ya que ofrece comodines, también puede utilizar diferentes variantes que hacer cosas ligeramente diferentes (-keepnames-keepclasseswithmembers y más).

Siempre que se necesite una regla simple de «mantener esta clase» o «mantener este método», en realidad prefiero la simplicidad de agregar una anotación@Keep en la clase o miembro, ya que se mantiene cerca del código, casi como documentación.

Si algún otro desarrollador que viene después de mí quiere refactorizar el código, sabrá inmediatamente que una clase/miembro marcada con @Keep requiere un manejo especial, sin tener que recordar consultar la configuración de ProGuard y arriesgarse a romper algo. También la mayoría de refactorizaciones de código en el IDE deben conservar la anotación @Keep con la clase automáticamente.

Estadísticas de cuadros

Aquí hay algunas estadísticas de cuadros, que muestran cuánto código logré eliminar usando ProGuard. En una aplicación más compleja con más dependencias y un DEX más grande, los ahorros pueden ser aún más sustanciales.