Articles

Exemples pratiques de règles de ProGuard

Wojtek Kaliciński
Wojtek Kaliciński

Suivre
20 février 2018 · 9 min de lecture

Dans mon article précédent, j’ai expliqué pourquoi tout le monde devrait utiliser ProGuard pour ses applications Android, comment l’activer et quels types d’erreurs vous pourriez rencontrer en le faisant. Il y avait beaucoup de théorie impliquée, car je pense qu’il est important de comprendre les principes sous-jacents afin d’être prêt à faire face à tout problème potentiel.

J’ai également parlé dans un article séparé du problème très spécifique de la configuration de ProGuard pour une génération d’application instantanée.

Dans cette partie, j’aimerais parler des exemples pratiques de règles ProGuard sur une application d’échantillon de taille moyenne: Plaid de Nick Butcher.

Plaid s’est avéré être un excellent sujet de recherche sur les problèmes de ProGuard, car il contient un mélange de bibliothèques tierces qui utilisent des éléments tels que le traitement des annotations et la génération de code, la réflexion, le chargement des ressources Java et le code natif (JNI). J’ai extrait et noté quelques conseils pratiques qui devraient s’appliquer à d’autres applications en général:

Classes de données

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

Probablement chaque application a une sorte de classe de données (également appelée DMOs, modèles, etc. selon le contexte et l’endroit où ils se trouvent dans l’architecture de votre application). La chose à propos des objets de données est qu’ils seront généralement chargés ou enregistrés (sérialisés) sur un autre support, tel que le réseau (une requête HTTP), une base de données (via un ORM), un fichier JSON sur disque ou dans un magasin de données Firebase.

De nombreux outils qui simplifient la sérialisation et la désérialisation de ces champs reposent sur la réflexion. GSON, Retrofit— Firebase – ils inspectent tous les noms de champs dans les classes de données et les transforment en une autre représentation (par exemple: {"name”: "Sue”, "age”: 28}), que ce soit pour le transport ou le stockage. La même chose se produit lorsqu’ils lisent des données dans un objet Java — ils voient une paire clé-valeur "name”:”John” et essaient de l’appliquer à un objet Java en recherchant un champ String name .

Conclusion: Nous ne pouvons pas laisser ProGuard renommer ou supprimer des champs sur ces classes de données, car ils doivent correspondre au format sérialisé. C’est une valeur sûre d’ajouter une annotation @Keep sur toute la classe ou une règle générique sur tous vos modèles :

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

Avertissement: Il est possible de faire une erreur lors du test si votre application est sensible à ce problème. Par exemple, si vous sérialisez un objet en JSON et l’enregistrez sur le disque dans la version N de votre application sans les règles de conservation appropriées, les données enregistrées peuvent ressembler à ceci: {"a”: "Sue”, "b”: 28}. Comme ProGuard a renommé vos champs en a et b, tout semblera fonctionner, les données seront enregistrées et chargées correctement.

Cependant, lorsque vous créez à nouveau votre application et que vous publiez la version N + 1 de votre application, ProGuard peut décider de renommer vos champs en quelque chose de différent, tel que c et d. Par conséquent, les données enregistrées précédemment ne se chargeront pas.

Vous devez d’abord vous assurer d’avoir les règles de conservation appropriées.

Le code Java appelé du côté natif (JNI)

Les fichiers ProGuard par défaut d’Android (vous devez toujours les inclure, ils ont des règles vraiment utiles) contiennent déjà une règle pour les méthodes implémentées du côté natif (-keepclasseswithmembernames class * { native <methods>; }). Malheureusement, il n’y a pas de moyen fourre-tout de garder le code invoqué dans la direction opposée: de JNI à Java.

Avec JNI, il est tout à fait possible de construire un objet JVM ou de trouver et d’appeler une méthode sur un handle JVM à partir de code C / C ++ et en fait, l’une des bibliothèques utilisées dans Plaid le fait.

Conclusion: Comme ProGuard ne peut inspecter que les classes Java, il ne connaîtra aucune utilisation qui se produit dans le code natif. Nous devons explicitement conserver ces utilisations des classes et des membres via une annotation @Keep ou une règle -keep.

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

Ouvrir des ressources à partir de JAR/APK

Android a son propre système de ressources et d’actifs qui ne devrait normalement pas poser de problème à ProGuard. Cependant, en Java ordinaire, il existe un autre mécanisme de chargement des ressources directement à partir d’un fichier JAR et certaines bibliothèques tierces peuvent l’utiliser même lorsqu’elles sont compilées dans des applications Android (dans ce cas, elles essaieront de charger à partir de l’APK).

Le problème est que généralement ces classes recherchent des ressources sous leur propre nom de package (ce qui se traduit par un chemin de fichier dans le fichier JAR ou APK). ProGuard peut renommer les noms de paquets lors de l’obscurcissement, donc après la compilation, il peut arriver que la classe et son fichier de ressources ne soient plus dans le même paquet dans l’APK final.

Pour identifier les ressources de chargement de cette manière, vous pouvez rechercher des appels à Class.getResourceAsStream / getResource et ClassLoader.getResourceAsStream / getResource dans votre code et dans toutes les bibliothèques tierces dont vous dépendez.

Conclusion: Nous devrions conserver le nom de toute classe qui charge des ressources de l’APK en utilisant ce mécanisme.

Dans Plaid, il y en a en fait deux — un dans la bibliothèque OkHttp et un dans Jsoup:

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

Comment trouver des règles pour les bibliothèques tierces

Dans un monde idéal, chaque dépendance que vous utilisez fournirait les règles de ProGuard requises dans l’AAR. Parfois, ils oublient de le faire ou publient uniquement des POTS, qui n’ont pas de moyen standardisé de fournir des règles ProGuard.

Dans ce cas, avant de commencer à déboguer votre application et de proposer des règles, n’oubliez pas de vérifier la documentation. Certains auteurs de bibliothèques fournissent des règles de ProGuard recommandées (telles que la modification utilisée dans Plaid) qui peuvent vous faire gagner beaucoup de temps et de frustration. Malheureusement, de nombreuses bibliothèques ne le font pas (comme c’est le cas avec Jsoup et Bypass mentionnés dans cet article). Sachez également que dans certains cas, la configuration fournie avec la bibliothèque ne fonctionnera qu’avec les optimisations désactivées, donc si vous les activez, vous pourriez être en territoire inexploré.

Alors, comment trouver des règles lorsque la bibliothèque ne les fournit pas?
Je ne peux que vous donner quelques conseils:

  1. Lisez la sortie de construction et logcat!
  2. Les avertissements de construction vous indiqueront quelles -dontwarnrègles à ajouter
  3. ClassNotFoundExceptionMethodNotFoundException et FieldNotFoundException vous indiqueront lesquelles -keeprègles à ajouter

Vous devriez être heureux lorsque votre application se bloque avec ProGuard activé — vous aurez un endroit pour commencer votre enquête 🙂

La pire classe de problèmes à déboguer est lorsque votre application fonctionne, mais par exemple n’affiche pas d’écran ou ne charge pas les données du réseau.

C’est là que vous devez considérer certains des scénarios que j’ai décrits dans cet article et vous salir les mains, même en plongeant dans le code tiers et en comprenant pourquoi il pourrait échouer, par exemple lorsqu’il utilise la réflexion, l’introspection ou JNI.

Débogage et traces de pile

ProGuard supprimera par défaut de nombreux attributs de code et métadonnées cachées qui ne sont pas nécessaires à l’exécution du programme. Certains d’entre eux sont en fait utiles au développeur — par exemple, vous voudrez peut-être conserver les noms de fichiers source et les numéros de ligne pour les traces de pile afin de faciliter le débogage:

-keepattributes SourceFile, LineNumberTable

Vous devez également vous rappeler d’enregistrer les fichiers de mappage ProGuard produits lorsque vous créez une version et de les télécharger en lecture pour obtenir des traces de pile obscurcies à partir de tout crash rencontré par votre utilisateurs.

Si vous allez attacher un débogueur pour parcourir le code de méthode dans une version ProGuardée de votre application, vous devez également conserver les attributs suivants pour conserver des informations de débogage sur les variables locales (vous n’avez besoin que de cette ligne dans votre type de construction debug) :

-keepattributes LocalVariableTable, LocalVariableTypeTable

Type de build de débogage minifié

Les types de build par défaut sont configurés de telle sorte que debug n’exécute pas ProGuard. Cela a du sens, car nous voulons itérer et compiler rapidement lors du développement, mais voulons toujours que la version de la version utilise ProGuard soit aussi petite et optimisée que possible.

Mais pour tester et déboguer complètement tous les problèmes de ProGuard, il est bon de configurer une version de débogage séparée et minifiée comme ceci:

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

Avec ce type de version, vous pourrez connecter le débogueur, exécuter des tests d’interface utilisateur (également sur un serveur CI) ou tester votre application pour d’éventuels problèmes sur une version aussi proche que possible de votre version.

Conclusion: Lorsque vous utilisez ProGuard, vous devez toujours vérifier soigneusement la qualité de votre version, soit en effectuant des tests de bout en bout, soit en parcourant manuellement tous les écrans de votre application pour voir si quelque chose manque ou se bloque.

Annotations d’exécution, type introspection

ProGuard supprimera par défaut toutes les annotations et même certaines informations de type excédentaires de votre code. Pour certaines bibliothèques, ce n’est pas un problème — celles qui traitent les annotations et génèrent du code au moment de la compilation (telles que Dagger 2 ou Glide et bien d’autres) pourraient ne pas avoir besoin de ces annotations plus tard lorsque le programme s’exécute.

Il existe une autre classe d’outils qui inspectent réellement les annotations ou examinent les informations de type des paramètres et des exceptions à l’exécution. Retrofit, par exemple, le fait en interceptant vos appels de méthode en utilisant un objet Proxy, puis en regardant les annotations et les informations de type pour décider quoi mettre ou lire à partir de la requête HTTP.

Conclusion: Il est parfois nécessaire de conserver les informations de type et les annotations qui sont lues au moment de l’exécution, par opposition au moment de la compilation. Vous pouvez consulter la liste des attributs dans le manuel ProGuard.

-keepattributes *Annotation*, Signature, Exception

Si vous utilisez le fichier de configuration ProGuard Android par défaut (getDefaultProguardFile('proguard-android.txt')), les deux premières options — Annotations et Signature — sont spécifiées pour vous. Si vous n’utilisez pas la valeur par défaut, vous devez vous assurer de les ajouter vous-même (cela ne fait pas de mal non plus de simplement les dupliquer si vous savez qu’ils sont une exigence pour votre application).

Tout déplacer vers le package par défaut

L’option -repackageclasses n’est pas ajoutée par défaut dans la configuration de ProGuard. Si vous obscurcissez déjà votre code et que vous avez résolu des problèmes avec les règles de conservation appropriées, vous pouvez ajouter cette option pour réduire davantage la taille de DEX. Cela fonctionne en déplaçant toutes les classes vers le package par défaut (racine), libérant essentiellement l’espace occupé par des chaînes comme « com.exemple.mapp.un paquet « .

-repackageclasses

Optimisations ProGuard

Comme je l’ai mentionné précédemment, ProGuard peut faire 3 choses pour vous:

  1. il se débarrasse du code inutilisé,
  2. renomme les identifiants pour réduire le code,
  3. effectue des optimisations de programme entières.

De la façon dont je le vois, tout le monde devrait essayer de configurer sa construction pour obtenir 1. et 2. travailler.

Pour déverrouiller 3. (optimisations supplémentaires), vous devez utiliser un fichier de configuration ProGuard par défaut différent. Changez le paramètre proguard-android.txt en proguard-android-optimize.txt dans votre build.gradle:

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

Cela ralentira la construction de votre version, mais pourrait accélérer l’exécution de votre application et réduire encore plus la taille du code, merci aux optimisations telles que l’inlining de méthode, la fusion de classes et la suppression de code plus agressive. Soyez cependant prêt à ce qu’il puisse introduire de nouveaux bogues difficiles à diagnostiquer, alors utilisez-le avec prudence et si quelque chose ne fonctionne pas, assurez-vous de désactiver certaines optimisations ou de désactiver complètement l’utilisation de la configuration d’optimisation.

Dans le cas de Plaid, les optimisations ProGuard ont interféré avec la façon dont Retrofit utilise des objets Proxy sans implémentations concrètes, et ont supprimé certains paramètres de méthode qui étaient réellement nécessaires. J’ai dû ajouter cette ligne à ma configuration:

-optimizations !method/removal/parameter

Vous trouverez une liste des optimisations possibles et comment les désactiver dans le manuel ProGuard.

Quand utiliser @Keep et -keep

@Keeple support est en fait implémenté sous forme de règles -keep dans le fichier de règles ProGuard Android par défaut, elles sont donc essentiellement équivalentes. La spécification des règles -keep est plus flexible car elle offre des caractères génériques, vous pouvez également utiliser différentes variantes qui font des choses légèrement différentes (-keepnames-keepclasseswithmembers et plus).

Chaque fois qu’une simple règle « keep this class » ou « keep this method » est nécessaire, je préfère en fait la simplicité d’ajouter une annotation @Keep sur la classe ou le membre, car elle reste proche du code, presque comme la documentation.

Si un autre développeur venant après moi veut refactoriser le code, il saura immédiatement qu’une classe/membre marquée avec @Keep nécessite une manipulation particulière, sans avoir à se souvenir de consulter la configuration de ProGuard et risquer de casser quelque chose. De plus, la plupart des refactorisations de code dans l’EDI doivent conserver automatiquement l’annotation @Keep avec la classe.

Statistiques de Plaid

Voici quelques statistiques de Plaid, qui montrent combien de code j’ai réussi à supprimer en utilisant ProGuard. Sur une application plus complexe avec plus de dépendances et un DEX plus important, les économies peuvent être encore plus substantielles.