Optimiser le code et les ressources de la plate-forme avec R8

Le système de compilation de la plate-forme Android (Soong) exécute le compilateur R8 sur les cibles qui compilent le bytecode DEX, y compris les applications Android (android_app), les tests (android_test) et les bibliothèques Java installables avec compile_dex: true (comme services.jar), pour réduire, optimiser et supprimer le code et les ressources inutilisés. Sur les bibliothèques statiques (android_library et java_library statique), Soong n'exécute pas R8 directement, mais utilise le bloc de propriété optimize pour associer et propager les règles de conservation des consommateurs (export_proguard_flags_files: true) aux cibles DEX en aval qui les associent de manière statique.

Pour les ingénieurs de plate-forme qui créent des images système ou des packages de fournisseurs, l'élimination des règles de conservation trop larges offre des avantages directs pour l'état du système :

  • Empreinte de partition système plus petite : R8 supprime les classes, méthodes et ressources inutilisées avant d'empaqueter les APK et les JAR sur /system, /system_ext, /product et /vendor.
  • Artefacts compilés plus petits : moins de méthodes DEX signifie que les fichiers .odex et .vdex générés par dex2oat lors de la compilation ou de la compilation sur l'appareil sont plus petits.
  • Réduction de la pression sur la mémoire d'exécution : étant donné qu'Android mappe les tables de ressources .odex, .vdex et APK dans la mémoire du processus à l'aide d'appels mmap paginés à la demande, des binaires plus petits réduisent les erreurs de page majeures lors du démarrage de l'application et diminuent la mémoire du code et des ressources résidents entre les processus. Pour en savoir plus sur l'impact du code d'application compilé sur la mémoire de l'appareil, consultez Le code d'application est de la mémoire.
  • Optimisation plus efficace de l'ensemble du programme : les contraintes de conservation étroites permettent à R8 d'intégrer des méthodes, de dévirtualiser les appels d'interface et virtuels, de supprimer les champs inutilisés et de propager des constantes entre les classes.

Configurer l'optimisation R8 dans Soong

Dans les fichiers Android.bp, configurez l'optimisation R8 à l'aide du bloc de propriété optimize sur les cibles android_app et java_library installables (ou sur les modules android_library et java_library statiques pour exporter les règles de conservation des consommateurs) :

android_app {
    name: "MySystemApp",
    srcs: ["src/**/*.java"],
    optimize: {
        obfuscate: true,
        shrink_resources: true,
    },
}

Le tableau suivant récapitule les propriétés optimize les plus courantes dans Soong (définies dans build/soong/java/dex.go) :

Propriété Description
enabled Détermine si R8 s'exécute sur la cible. La valeur par défaut est true pour toutes les cibles android_app.
shrink Contrôle le tree shaking pour supprimer les classes, champs et méthodes inaccessibles. La valeur par défaut est true pour les cibles android_app (false pour les modules java_library et de test autonomes, qui transmettent -dontshrink, sauf s'ils sont explicitement définis).
optimize Contrôle les optimisations du bytecode, telles que l'intégration de méthodes, la fusion de classes, la propagation de constantes et la suppression des branches mortes. La valeur par défaut est true pour les cibles android_app (contrôlée par l'option de compilation de version RELEASE_R8_OPTIMIZE_BY_DEFAULT).
obfuscate Contrôle la minimisation et le changement de nom des identifiants. La valeur par défaut est false pour des raisons de compatibilité historique, mais définissez-la sur true dans la mesure du possible pour réduire la taille du fichier DEX et débloquer des optimisations plus poussées (voir Activer l'obscurcissement dans la mesure du possible).
shrink_resources Supprime les ressources inutilisées (entrées res/) de l'APK empaqueté après la minification de code. La valeur par défaut est false.
optimized_shrink_resources Exécute le pipeline intégré de minification du code et des ressources R8 afin que R8 trace conjointement les références de code et de ressources en une seule passe. Lorsque shrink_resources: true est défini, la valeur par défaut est RELEASE_USE_OPTIMIZED_RESOURCE_SHRINKING_BY_DEFAULT (true dans les builds de plate-forme standards).
proguard_flags_files Répertorie les fichiers .flags ou .pro spécifiques au module contenant des règles de conservation personnalisées. Évitez d'ajouter des fichiers personnalisés lorsque les annotations standards ou les valeurs par défaut suffisent.
export_proguard_flags_files Propager le proguard_flags_files de cette bibliothèque aux modules en aval qui en dépendent de manière statique.
trace_references_from Répertorie les cibles dépendantes de la compilation DEX directe (connectées via instrumentation_for ou libs) dont les références de bytecode dans cette cible sont automatiquement tracées et conservées par R8 (également propagées dans les variantes override_android_app et override_android_test, et compatibles avec les bibliothèques Java associées non dépendantes lorsqu'elles sont associées à une division .impl).
allow_traced_references Permet aux cibles dépendantes de la compilation DEX directe (connectées via instrumentation_for ou libs, telles que les tests multi-dépôts ou de fournisseurs qui ne peuvent pas être listés dans trace_references_from) d'activer le traçage de leurs références de bytecode dans cette cible en définissant trace_references: true.
trace_references Lorsque cette option est définie sur true sur un dépendant de compilation DEX directe d'une cible avec allow_traced_references: true, exécute R8 tracereferences sur ce module et transmet les règles de conservation générées au module cible.
protect_api_surface Transmet --protect-api-surface à R8. Lorsqu'il est défini sur un java_sdk_library, il génère et associe automatiquement des règles de conservation de l'API Metalava dans tous les champs d'application de l'API activés (public, système, module-lib, serveur système et test) à la bibliothèque d'implémentation.

Les bibliothèques statiques (android_library, java_library et importations AAR) peuvent également définir required_downstream_optimization: { enabled: true, optimize: true } dans Android.bp pour exiger que toute cible de compilation DEX en aval qui les associe de manière statique (directement ou de manière transitive) active la minification R8 et l'optimisation du bytecode. Soong l'active automatiquement sur androidx.compose.runtime_runtime afin que les cibles associées à Jetpack Compose soient optimisées avec R8.

Activer l'obscurcissement chaque fois que possible

Dans Soong, obfuscate est défini par défaut sur false pour des raisons de compatibilité historique, mais vous devez définir explicitement obfuscate: true sur android_app et les cibles DEX autonomes dans la mesure du possible :

  • Empreinte DEX et .vdex réduite : renommer les packages, les classes, les champs et les méthodes en identifiants courts (a, b) réduit le pool de chaînes DEX (string_ids et string_data_item) et les descripteurs de type. Comme Android mappe les fichiers .vdex et .odex dans la mémoire du processus, des tables de symboles plus petites réduisent directement la taille de la partition système et l'espace mémoire utilisé lors de l'exécution.
  • Optimisations plus poussées de l'ensemble du programme : autoriser R8 à renommer les identifiants permet de fusionner les classes, d'aplatir les packages et de dédupliquer les classes synthétiques dans les packages que R8 doit sinon ignorer pour éviter les conflits de noms.
  • Symbolisation complète de la trace de la pile : l'activation de l'obscurcissement dans Soong ne réduit pas la capacité de débogage. Pour chaque cible compilée par R8, Soong émet un fichier de mappage proguard_dictionary dans le répertoire intermédiaire du module, regroupe tous les dictionnaires de module dans l'artefact proguard-dict.zip de la compilation, intègre un hachage de mappage (--map-id-template) dans l'en-tête DEX et réécrit les attributs SourceFile de la classe (--source-file-template) afin que retrace puisse symboliser automatiquement les traces de la pile.

Conservez obfuscate: false (ou préservez explicitement les noms d'API publics) uniquement pour :

  • Bibliothèques partagées sur le bootclasspath ou le classpath system_server : les modules (java_library ou java_sdk_library) qui exposent une surface d'API externe associée de manière dynamique par d'autres modules au moment de l'exécution (comme les cibles framework.jar, services.jar ou <uses-library>) doivent conserver obfuscate: false ou préserver explicitement leur surface d'API à l'aide des règles protect_api_surface: true, @KeepForApi ou -keep afin que les appelants compilés par rapport à leurs bouchons puissent résoudre les noms de classe et de membre au moment de l'exécution. Sur les modules java_sdk_library, la définition de protect_api_surface: true génère et associe automatiquement les règles de conservation ProGuard de Metalava à tous les champs d'application d'API activés (public, système, module-lib, system-server et test) à la bibliothèque d'implémentation. Lorsque l'optimisation R8 généralisée est activée sur des cibles bootclasspath non modifiables (framework-minus-apex, ext et framework-location), elles incluent également framework-hiddenapi-proguard-rules (généré par hiddenapi_to_proguard) pour conserver les membres listés dans les fichiers boot/hiddenapi/*.txt actifs (en omettant hiddenapi-max-target-o.txt).

Avant d'activer obfuscate: true sur une application, vérifiez les conditions préalables de migration suivantes :

  • Dépendances de l'APK de test externe : si un APK android_test externe définit instrumentation_for: "MyApp" et appelle directement des classes ou des méthodes internes de MyApp, le changement de nom de ces symboles entraîne NoSuchMethodError ou NoClassDefFoundError au moment de l'exécution du test. Configurez trace_references_from (ou allow_traced_references avec trace_references: true), structurez le test en tant qu'APK d'auto-instrumentation associant MyApp.impl de manière statique, ou annotez les hooks de test avec @VisibleForTesting (voir Étape 2 : Migrer les règles de conservation basées sur les tests) afin que les symboles internes nécessaires aux tests soient conservés tout en obscurcissant le reste de l'application.
  • Réflexion basée sur des chaînes et JNI : si un module recherche des classes, des méthodes ou des champs par des noms de chaînes littérales (Class.forName, getDeclaredMethod ou JNI FindClass et GetMethodID) sans règles de conservation ni annotations, l'obscurcissement renomme ces cibles et interrompt les recherches d'exécution. (Notez que la réflexion sans annotation échoue également sous shrink: true et optimize: true.) Annotez ces points d'entrée avec le Guide des annotations Keep (@UsesReflection, @UsedByReflection ou @UsedByNative) afin que R8 conserve leurs noms et obscurcisse le reste du module.

Suivre le principe de l'absence d'indicateurs personnalisés

Le module de plate-forme Android idéal ne comporte pas de fichiers proguard.flags ni keep.xml personnalisés. Dans la compilation de la plate-forme Android, la grande majorité des règles de conservation personnalisées sont redondantes :

  • Lignes de base de la plate-forme et AAPT2 : la plupart des points d'entrée sont déjà conservés automatiquement par les lignes de base de la plate-forme globale (comme @Keep, les méthodes JNI native et @VisibleForTesting) ou générés par AAPT2 à partir de AndroidManifest.xml et des ressources de mise en page (voir Comprendre les règles de conservation par défaut de la plate-forme).
  • Alternatives ciblées : lorsque les points d'entrée doivent être conservés, privilégiez les annotations de site de déclaration (keepanno, @VisibleForTesting), les règles de bibliothèque exportées (export_proguard_flags_files: true) ou l'association de tests statiques par rapport aux fichiers .flags dissociés (voir Auditer et migrer les règles de conservation existantes).

Avant d'ajouter ou de conserver un fichier proguard.flags ou keep.xml personnalisé, vérifiez si la règle est déjà gérée par les références par défaut ou si elle peut être migrée vers des annotations de code.

Comprendre les règles de conservation par défaut de la plate-forme

Soong transmet automatiquement les règles de référence globales suivantes à chaque appel de R8 sur les cibles de compilation DEX (modules android_app, android_test et java_library installables), configurées dans build/soong/java/dex.go :

  • build/make/core/proguard.flags: conserve les classes et les membres annotés avec @com.android.internal.annotations.VisibleForTesting dans tous les packages, et avec @VisibleForTesting (androidx.annotation.VisibleForTesting ou com.google.common.annotations.VisibleForTesting) dans les packages android.**, com.android.** et com.google.android.**. Conserve également @TestApi, @Keep (androidx.annotation, android.support.annotation, com.android.internal.annotations), @KeepForWeakReference, @WeaklyReferencedCallback et @dalvik.annotation.optimization.**.
  • build/make/core/proguard_basic_keeps.flags : conserve les attributs SourceFile pour les traces de la pile, les annotations de visibilité de l'environnement d'exécution (RuntimeVisible*Annotations), les attributs Exceptions et AnnotationDefault, les méthodes native, les membres Serializable, les méthodes @JavascriptInterface, les constructeurs Throwable(String), les champs Parcelable$CREATOR et les champs protobuf MessageLite.
  • build/make/core/proguard/kotlin.flags : désactive les avertissements inoffensifs pour des méta-annotations Kotlin spécifiques (kotlin.Metadata et kotlin.annotation.{AnnotationRetention,AnnotationTarget,Retention,Target}) et supprime les annotations Kotlin DebugMetadata dans les builds de release.
  • build/make/core/proguard/checknotnull.flags : remplace les appels d'assistance de vérification de valeur nulle courants (com.google.common.base.Preconditions.checkNotNull et dagger.internal.Preconditions.checkNotNull*) par des vérifications de valeur nulle de bytecode concises. Ce fichier omet intentionnellement Objects.requireNonNull pour conserver les messages d'exception explicites au-delà des limites de l'API du framework.
  • build/make/core/proguard/enumvalues.flags : conserve les méthodes values et valueOf sur les types enum, sauf si elles sont désactivées par la configuration de compilation.
  • Règles générées automatiquement par AAPT2 : AAPT2 inspecte les fichiers AndroidManifest.xml, XML de mise en page et XML de préférence fusionnés pour générer des règles de conservation exactes pour chaque Activity, Service, BroadcastReceiver, ContentProvider, BackupAgent, Application, sous-classe View personnalisée, sous-classe Preference et méthode XML android:onClick enregistrés.

Auditer et migrer les règles de conservation existantes

Lorsque vous auditez des fichiers proguard.flags ou keep.xml existants dans un dépôt de plate-forme, évaluez chaque règle dans l'ordre par rapport à la hiérarchie en quatre étapes suivante :

Étape 1 : Supprimez les règles redondantes ou obsolètes

Supprimez les règles qui sont déjà couvertes par les références globales, les règles de mise en page et de fichier manifeste AAPT2 ou les valeurs par défaut de R8, ainsi que les règles faisant référence à des classes ou des packages qui n'existent plus.

Si toutes les règles d'un fichier .flags sont redondantes, supprimez le fichier et supprimez proguard_flags_files de Android.bp. Si le bloc optimize restant ne fait que répéter les paramètres par défaut, supprimez le bloc redondant et formatez le fichier de compilation avec bpfmt -w Android.bp. Lorsque vous nettoyez un package, vérifiez si les modules associés dans les sous-répertoires (comme les variantes de cible Kotlin) font référence au même fichier d'options et mettez-les à jour ensemble.

Étape 2 : Migrer les règles de conservation basées sur des tests

Déplacez les règles de conservation basées sur les tests hors des fichiers .flags de production personnalisés à l'aide de l'un des modèles suivants :

  • Tracer les références à partir de cibles de test ou d'accompagnement (trace_references_from ou allow_traced_references) : lorsque la cible et un module dépendant direct compilent leur propre DEX (android_app, android_test ou un java_library dexé) et sont connectés via instrumentation_for ou libs, Soong peut exécuter R8 tracereferences sur le module dépendant et intégrer les règles de conservation générées dans la compilation R8 de la cible sans créer de cycle de dépendance ni nécessiter une division manuelle de la bibliothèque .impl :

    • Tests dans le même dépôt (trace_references_from) : listez les dépendances instrumentation_for ou libs directes sur le module cible (cela se propage également automatiquement entre les variantes override_android_app et override_android_test, et continue de prendre en charge les bibliothèques Java associées non dépendantes lorsqu'elles sont associées à une division .impl) :

      android_app {
          name: "MyApp",
          srcs: ["src/**/*.java"],
          optimize: {
              obfuscate: true,
              shrink_resources: true,
              trace_references_from: ["MyAppTests"],
          },
      }
      
      android_test {
          name: "MyAppTests",
          instrumentation_for: "MyApp",
          srcs: ["tests/**/*.java"],
      }
      
    • Tests inter-dépôts ou de fournisseurs (allow_traced_references et trace_references) : lorsque le module cible ne peut pas lister directement la dépendance dans trace_references_from (par exemple, les tests dans un autre dépôt ou une autre arborescence de fournisseurs), définissez allow_traced_references: true sur le module cible et activez l'option à partir de chaque dépendance avec trace_references: true :

      android_app {
          name: "MyApp",
          srcs: ["src/**/*.java"],
          optimize: {
              allow_traced_references: true,
          },
      }
      
      android_test {
          name: "MyAppVendorTests",
          instrumentation_for: "MyApp",
          srcs: ["tests/**/*.java"],
          optimize: {
              trace_references: true,
          },
      }
      
    • Réflexion basée sur des chaînes dans les tests : tracereferences inspecte les références du pool de constantes de bytecode et ne peut pas détecter la réflexion basée sur des chaînes dans les tests (Class.forName, getDeclaredField, getDeclaredMethod ou les assistants d'injection par champs réflexive). Annotez les champs et les méthodes de production accessibles de manière réfléchie avec @VisibleForTesting (ou @UsedByReflection).

  • Associer la bibliothèque d'implémentation aux tests (static_libs) : si vous ne souhaitez conserver aucun point d'entrée réservé aux tests dans l'APK de production et que vous ne voulez pas que les modifications de test déclenchent à nouveau R8 sur la cible de production, placez les fichiers sources de l'application dans un android_library (MyApp.impl) et associez de manière statique MyApp.impl à MyApp et MyAppTests :

    android_library {
        name: "MyApp.impl",
        srcs: ["src/**/*.java"],
        manifest: "AndroidManifest.xml",
    }
    
    android_app {
        name: "MyApp",
        static_libs: ["MyApp.impl"],
        optimize: {
            obfuscate: true,
            shrink_resources: true,
        },
    }
    
    android_test {
        name: "MyAppTests",
        srcs: ["tests/**/*.java"],
        static_libs: ["MyApp.impl"],
    }
    

    Ce modèle à trois modules permet à MyAppTests de s'exécuter en tant que test d'instrumentation avec un accès complet aux classes internes, tout en permettant à MyApp de réduire et d'obscurcir uniquement les points d'entrée de production.

  • Annoter les hooks de test avec @VisibleForTesting : si un APK de test externe n'appelle que quelques méthodes ou constructeurs internes sur une cible android_app, vous pouvez annoter ces déclarations avec @VisibleForTesting. La référence globale build/make/core/proguard.flags conserve automatiquement les éléments @VisibleForTesting dans les packages android.**, com.android.** et com.google.android.** sans fichiers .flags personnalisés. (Pour les modules de fournisseurs en dehors de ces espaces de noms, utilisez trace_references_from, @UsedByReflection ou des règles de bibliothèque exportées.) Lorsque vous annotez manuellement avec @VisibleForTesting au lieu d'utiliser trace_references_from ou trace_references, annotez également les classes transmises à Mockito mock(...) ou spy(...) (pas seulement les méthodes bouchonnées), car mockito-target (dexmaker-mockito) sur l'appareil génère des sous-classes d'exécution et R8 peut marquer les classes sans sous-classe final (MockitoException: Can't mock/spy class ... - final class).

  • Évitez ou reconditionnez les bibliothèques statiques qui se chevauchent (jarjar_rules) : lorsqu'un APK android_app et son APK de test instrumentation_for associent de manière statique la même bibliothèque (directement, comme guava, ou de manière transitive), tracereferences voit ces classes définies dans le test et n'émet aucune règle de conservation pour l'application. Au moment de l'exécution, le chargeur de classe parent de l'application masque la copie du test avec ses classes réduites par R8, ce qui entraîne une erreur IllegalAccessError ou NoSuchMethodError. Si le test n'a pas besoin de sa propre copie groupée, supprimez le doublon du static_libs du test (ou associez-le via libs) pour éviter la collision. Sinon, reconditionnez les packages de bibliothèque statique qui se chevauchent de l'application sous un préfixe spécifique au module à l'aide de jarjar_rules.

Étape 3 : Exportez les règles de la bibliothèque propriétaire

Si un java_library ou un android_library partagé nécessite des règles de conservation pour sa propre réflexion interne ou ses rappels JNI, définissez les règles sur la cible de la bibliothèque et définissez export_proguard_flags_files: true :

java_library {
    name: "my-shared-library",
    srcs: ["src/**/*.java"],
    optimize: {
        proguard_flags_files: ["proguard.flags"],
        export_proguard_flags_files: true,
    },
}

Toutes les cibles android_app et de bibliothèque en aval qui associent de manière statique my-shared-library héritent automatiquement de ces règles. Les applications en aval n'ont donc pas besoin de les dupliquer. Lorsque plusieurs modules de différents répertoires doivent partager un ensemble de règles qui n'est pas lié à une seule bibliothèque de code, publiez le fichier .flags via un filegroup explicite dans Android.bp afin que les modules puissent référencer :my-shared-flags proprement au-delà des limites du package. Pour obtenir des conseils généraux sur la création de règles de conservation des consommateurs de bibliothèques sans restreindre l'optimisation des applications en aval, consultez Optimisation pour les auteurs de bibliothèques.

Étape 4 : Annoter les déclarations dans le code source

Lorsqu'une classe, une méthode, un constructeur ou un champ est accessible via la réflexion ou JNI et n'est pas couvert par AAPT2 ou les références globales, remplacez les règles -keep dissociées dans les fichiers .flags par des annotations sources du Guide to Keep Annotations (voir la documentation de référence Javadoc keepanno) :

  • @UsesReflection : privilégiez cette annotation lorsque vous contrôlez le code qui effectue la réflexion. Placez-la au niveau du site d'appel de réflexion pour déclarer les classes, méthodes ou champs cibles auxquels l'accès est dynamique. Étant donné que @UsesReflection encode automatiquement une condition préalable selon laquelle le site d'appel annoté lui-même est accessible, R8 supprime l'appelant et la cible de réflexion si le site d'appel n'est pas utilisé.
  • @UsedByReflection : à placer sur les classes, méthodes, champs ou constructeurs instanciés ou appelés de manière réfléchie par du code ou des bibliothèques externes, comme les classes chargées avec Class.forName à partir des clés Bundle ou Settings, ou les classes de plug-ins chargées via des chargeurs de classe dynamiques. Spécifiez kind (par exemple, KeepItemKind.CLASS_AND_METHODS), preconditions (@KeepCondition) et les contraintes de paramètre afin que R8 ne conserve que le contrat de réflexion exact et puisse toujours optimiser ou supprimer les membres inutilisés de la classe. N'ajoutez pas @UsedByReflection aux composants enregistrés dans le fichier manifeste, tels que JobService ou BroadcastReceiver, car AAPT2 les conserve déjà.
  • @UsedByNative : à placer sur les méthodes ou les champs accessibles à partir du code JNI C ou C++ à l'aide de GetMethodID, GetStaticMethodID ou GetFieldID.
  • @KeepForApi : à placer sur les classes ou les membres de l'API de bibliothèque qui doivent rester intacts lorsqu'une bibliothèque est réduite avant d'être distribuée.
  • @Keep (androidx.annotation.Keep) : à utiliser comme solution de secours lorsque keepanno n'est pas applicable. L'application de @Keep à une classe conserve la classe et tous ses membres sans condition. L'application de @Keep à une méthode ou à un champ agit comme un point d'entrée inconditionnel qui conserve le membre et la classe qui le contient, même si la classe n'est jamais instanciée, tandis que keepanno exprime l'accessibilité conditionnelle.

Pour utiliser les annotations R8 keepanno dans un module Soong :

  1. Ajoutez "keepanno-annotations" à libs dans Android.bp :

    libs: [
        "keepanno-annotations",
    ],
    
  2. Importez com.android.tools.r8.keepanno.annotations.* et annotez la déclaration ou le site d'appel dans le code source Java ou Kotlin :

    import com.android.tools.r8.keepanno.annotations.KeepItemKind;
    import com.android.tools.r8.keepanno.annotations.UsedByReflection;
    
    public final class CustomPluginController {
        @UsedByReflection(
            description = "Instantiated via Class.forName from plugin config",
            kind = KeepItemKind.CLASS_AND_METHODS)
        public CustomPluginController(Context context) {
            // ...
        }
    }
    

    R8 traduit les annotations keepanno directement dans son modèle de règle de conservation interne et supprime les annotations de la sortie DEX finale, sans ajouter de surcharge de bytecode d'exécution.

Éviter les erreurs courantes

Les sections suivantes décrivent les pièges courants liés à proguard.flags et keep.xml dans le code de la plate-forme et comment les résoudre.

Règles générales de conservation des composants

# Don't do this:
-keep class * extends android.app.Activity
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
  • Pourquoi cela nuit aux performances : cette règle est complètement redondante avec AAPT2. Comme elle utilise un caractère générique sans vérifier si le composant est enregistré dans le fichier AndroidManifest.xml fusionné final, elle force R8 à conserver chaque sous-classe Activity, Service ou BroadcastReceiver trouvée n'importe où dans le chemin de classe, y compris les composants de bibliothèque inutilisés et les activités de débogage désactivées.
  • Correction recommandée : supprimez la règle. AAPT2 inspecte le fichier manifeste fusionné et génère des règles -keep exactes pour les composants enregistrés.

Règles de conservation générales pour les gestionnaires de clics XML

# Don't do this:
-keepclassmembers class * {
    public void *(android.view.View);
}
  • Pourquoi cela nuit aux performances : la conservation de chaque méthode public void *(View) dans chaque classe du module empêche R8 de supprimer ou d'intégrer toute méthode qui accepte un seul paramètre View.
  • Correction recommandée : supprimez la règle. AAPT2 analyse les fichiers XML de mise en page et génère des règles de conservation ciblées pour les méthodes référencées par les attributs android:onClick.

Règles de conservation des getters et setters de View généraux

# Don't do this:
-keep public class * extends android.view.View {
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    public void set*(...);
    public *** get*();
}
  • Pourquoi cela nuit aux performances : cette règle force R8 à conserver chaque getter et setter dans chaque sous-classe View de l'application et de toutes les bibliothèques associées (y compris les bibliothèques AndroidX et Material), ce qui empêche la suppression des méthodes mortes et l'intégration dans le code de l'interface utilisateur.
  • Correction recommandée : supprimez la règle. AAPT2 conserve déjà les constructeurs des classes View personnalisées gonflées à partir de fichiers XML de mise en page. Si le code anime une propriété de vue à l'aide de noms de chaînes réfléchissants tels que ObjectAnimator.ofFloat(view, "translationZ", ...), remplacez le nom de la chaîne par une référence de propriété typée (View.TRANSLATION_Z ou une implémentation FloatProperty ou IntProperty personnalisée) pour éviter complètement la réflexion. Si l'accès aux propriétés réfléchissantes ne peut pas être évité, annotez le getter ou le setter spécifique avec @UsedByReflection.

Désactivation de l'optimisation ou de l'obscurcissement généralisés

# Don't do this in proguard.flags:
-dontoptimize
-dontshrink
-dontobfuscate
  • Pourquoi cela nuit aux performances : placer -dontoptimize ou -dontshrink dans un fichier .flags remplace silencieusement les paramètres Android.bp du module et désactive les passes d'optimisation sur l'ensemble de la cible. Pire encore, si une bibliothèque exporte un fichier .flags contenant -dontoptimize, elle désactive l'optimisation R8 pour chaque android_app en aval qui associe la bibliothèque.
  • Correction recommandée : Supprimez -dontoptimize, -dontshrink et -dontobfuscate des fichiers .flags. Contrôlez explicitement le comportement d'optimisation dans Android.bp à l'aide du bloc optimize (shrink, optimize, obfuscate) sur la cible feuille.

Indicateurs de diagnostic dans les règles validées

# Don't do this in proguard.flags:
-verbose
-printmapping proguard.map
-printusage usage.txt
-printconfiguration config.txt
  • Pourquoi cela nuit à la compilation : les indicateurs de diagnostic inondent les journaux de compilation ou tentent d'écrire des fichiers de sortie dans des chemins d'accès locaux lors des compilations Soong en bac à sable.
  • Correction recommandée : Supprimez ces options des fichiers .flags validés. Soong écrit automatiquement les sorties de mappage et d'utilisation de R8, telles que proguard_dictionary et proguard_usage.zip, dans le répertoire intermédiaire du module sous out/soong/.intermediates/.

Règles de conservation en production pour le code de test

  • Pourquoi cela nuit aux performances : l'ajout de règles -keep personnalisées uniquement pour l'accès aux tests force ce code à rester non obscurci et à être conservé dans les builds de production. Bien que la configuration de trace_references_from (ou de allow_traced_references avec trace_references: true) ou l'annotation de méthodes avec @VisibleForTesting évite de gérer manuellement les règles -keep et ne conserve que les éléments référencés par les tests, ces symboles sont toujours inclus dans le binaire de production.
  • Correction recommandée : Éliminez les règles -keep de test personnalisées en activant trace_references_from (ou allow_traced_references et trace_references: true), ou structurez l'application dans une bibliothèque .impl associée à un APK de test d'auto-instrumentation à l'aide de static_libs pour que les points d'entrée réservés aux tests ne soient pas inclus dans l'APK de production, comme décrit à l'étape 2 : Migrer les règles de conservation basées sur les tests.

Caractères génériques de ressources globaux dans keep.xml

<!-- Don't do this in res/raw/keep.xml: -->
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@raw/*,@drawable/*,@string/*" />
  • Pourquoi cela nuit aux performances : les caractères génériques généraux conservent toutes les ressources du type correspondant dans le module et ses dépendances, ce qui annule la réduction des ressources (shrink_resources: true) et augmente la taille de l'APK et de la table resources.arsc mappée.
  • Correction recommandée :
    • Privilégiez les références de ressources statiques à keep.xml : évitez d'utiliser la méthode Resources.getIdentifier pour rechercher de manière dynamique un ensemble limité de ressources, telles que des chaînes de test numérotées ou des drawables thématiques. Utilisez plutôt une instruction switch au moment de la compilation ou mappez sur des constantes statiques R.id, R.string ou R.drawable. Les références statiques permettent à R8 et AAPT2 de tracer les ressources actives exactes, d'éliminer la surcharge de recherche de chaînes d'exécution et de supprimer complètement le besoin de keep.xml.
    • Répertorier des noms de ressources spécifiques : si une recherche dynamique est requise, par exemple par un lecteur de licence tiers, répertoriez les identifiants de ressources exacts dans tools:keep (par exemple, tools:keep="@raw/third_party_licenses").
    • Supprimer les fichiers keep.xml redondants : si les ressources listées sont déjà référencées de manière statique dans le code (R.raw.foo) ou le code XML (@raw/foo), l'outil de réduction les conserve automatiquement. Vous pouvez donc supprimer res/raw/keep.xml.

Vérifier et auditer les modifications apportées aux règles

Chaque fois que vous supprimez ou limitez des règles de conservation dans proguard.flags ou keep.xml, vérifiez que le module se compile correctement, qu'il réussit les tests unitaires et d'instrumentation, et qu'il conserve tous les points d'entrée requis.

Modules de compilation et de test

  1. Effectuez une compilation propre du module pour vérifier que R8 et AAPT2 se terminent sans avertissement de référence manquante :

    m <MODULE_NAME>
    
  2. Exécutez des tests unitaires et d'instrumentation pour le module à l'aide de atest :

    atest <TEST_MODULE_NAME>
    
  3. Pour les applications système, les services privilégiés ou les modules dépendant du matériel, exécutez des tests d'instrumentation et d'interface utilisateur sur les appareils physiques cibles ou dans votre laboratoire de test d'appareils pour tester les chemins de réflexion d'exécution, les liaisons IPC et l'inflation des ressources.

Inspecter les différences entre les fichiers DEX et les ressources

Comparez l'APK ou le JAR compilé avant et après les modifications de règles pour vérifier que R8 supprime le code et les ressources inutilisés sans supprimer les points d'entrée attendus :

  • Utilisez apkanalyzer ou dexdump pour inspecter les classes, méthodes et champs conservés dans les fichiers APK ou DEX de sortie :

    apkanalyzer dex packages $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • Lorsque vous modifiez keep.xml ou activez shrink_resources, utilisez aapt2 dump resources pour vérifier que la compilation supprime les ressources inutilisées et conserve celles requises :

    aapt2 dump resources $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
    
  • Lorsque vous exécutez prebuilts/r8/r8.jar directement sur le classes.jar pré-R8 d'un module pour vérifier les modifications d'indicateurs hors connexion sans compilation complète de la plate-forme, transmettez les fichiers JAR de chemin de classe --lib du module (android.jar, framework.jar et libs partagé) et les règles proguard.options générées par AAPT2 à partir de AndroidManifest.xml et des fichiers XML de mise en page. Sans ces entrées, R8 ne peut pas résoudre les super-types du framework (Activity, Service, Phone, Handler) et ne conserve pas suffisamment les méthodes de hiérarchie.

Analyser le rayon de conservation et la subsomption des règles avec R8

Le compilateur R8 Open Source inclut un analyseur Keep Radius (également exposé dans Android Studio et Gradle en tant qu'analyseur de configuration R8) qui mesure l'impact exact de chaque règle de conservation pendant la compilation. Soong intègre cet analyseur directement dans la compilation de la plate-forme Android (configurée dans build/soong/java/dex.go).

Pour analyser les règles de conservation d'un module de plate-forme ou d'un build entier :

  1. Exécutez la compilation avec la variable d'environnement R8_DUMP_KEEP_RADIUS=true :

    R8_DUMP_KEEP_RADIUS=true m <MODULE_NAME>
    

    Lorsque R8_DUMP_KEEP_RADIUS=true est défini, Soong demande à R8 d'enregistrer les métriques des règles de conservation dans un fichier r8keepradius.pb intermédiaire pour chaque module compilé sous out/soong/.intermediates/. Pour chaque règle de conservation et annotation keepanno, R8 enregistre :

    • Rayon de conservation immédiat : classes, champs et méthodes exacts conservés par la règle, ainsi que les contraintes spécifiques qu'elle applique contre la réduction, l'optimisation ou l'obscurcissement.
    • Subsomption de règles : quelles autres règles ou annotations de conservation conservent déjà les mêmes éléments. Si une règle personnalisée est complètement subsumée par une règle AAPT2 ou une règle de référence globale, vous pouvez la supprimer en toute sécurité.
    • Règles globales et à l'échelle du package : règles qui utilisent des caractères génériques larges à l'échelle du package ou qui appliquent des directives de configuration globales.
  2. Convertissez la sortie r8keepradius.pb en rapport HTML interactif à l'aide de KeepRadiusHtmlReportGenerator (fourni dans prebuilts/r8/r8.jar) :

    # Generate an HTML report for a single module
    INTERMEDIATES=out/soong/.intermediates/packages/apps/<APP_NAME>/<APP_NAME>
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        $INTERMEDIATES/android_common/r8keepradius.pb \
        keep_radius_report.html
    
    # Scan all built modules under out/soong/.intermediates and generate
    # per-module HTML reports plus an aggregate keepradius.html summary
    java -cp prebuilts/r8/r8.jar \
        com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \
        out/soong/.intermediates \
        out/keep_radius_reports
    

    Lorsqu'un répertoire est fourni, KeepRadiusHtmlReportGenerator parcourt tous les fichiers *keepradius*.pb, génère un rapport HTML pour chaque module et crée out/keep_radius_reports/keepradius.html résumant le nombre d'éléments actifs, le nombre d'éléments conservés et les règles de conservation de rayon le plus élevé dans la compilation. Pour en savoir plus sur l'interprétation des scores de minification, d'optimisation et d'obscurcissement dans le rapport généré, consultez Utiliser l'analyseur de configuration R8.

Soong est également compatible avec d'autres variables d'environnement de diagnostic et de boucle interne R8 dans build/soong/java/dex.go :

  • R8_DUMP_INPUT=true : écrit r8inputs.zip dans le répertoire intermédiaire du module contenant tous les fichiers JAR d'entrée, les fichiers JAR de bibliothèque et les configurations ProGuard fusionnées pour la reproduction autonome de R8.
  • R8_DUMP_PERFETTO_TRACE=true : écrit r8trace.ptrace dans le répertoire intermédiaire du module pour inspecter les passes de compilation R8 dans Perfetto.
  • SOONG_SKIP_TRACE_REFERENCES=true : ignore l'exécution des règles tracereferences configurées par trace_references_from ou trace_references pour accélérer les compilations de boucle interne locales lors de l'itération sur le code de test.

Valider les règles des consommateurs de bibliothèques

Lorsqu'un java_library ou un android_library exporte des règles de conservation vers des consommateurs en aval (export_proguard_flags_files: true), ces règles ne doivent pas inclure d'indicateurs globaux qui modifient ou désactivent l'optimisation pour l'application consommatrice.

R8 fournit un analyseur de règles de conservation Open Source (ProcessKeepRules), exposé dans la compilation de la plate-forme Android en tant qu'outil hôte process-keep-rules (défini dans prebuilts/r8/Android.bp) :

# Build the R8 keep rules validator host binary
m process-keep-rules

# Validate one or more ProGuard configuration files
out/host/linux-x86/bin/process-keep-rules <PROGUARD_FLAGS_PATH>

L'outil process-keep-rules analyse chaque fichier de configuration et échoue en affichant des diagnostics de fichier et de ligne s'il rencontre des directives non autorisées dans les règles d'utilisation de la bibliothèque. Les directives non autorisées incluent la désactivation de l'optimisation globale, de la réduction ou de l'obscurcissement (comme -dontoptimize ou -dontshrink), les indicateurs de reconditionnement de package et de modification d'accès, les indicateurs de diagnostic ou de mappage, et -keepattributes au niveau de l'application.

Auditer les arborescences sources avec pgaudit.py

Pour auditer les répertoires sources non compilés avec les analyseurs de compilation de R8, l'arborescence de la plate-forme Android inclut le script pgaudit.py sous build/make/core/proguard/tools/. Cet outil d'analyse statique analyse les fichiers Android.bp, proguard.flags et keep.xml d'un dépôt pour signaler les règles déjà couvertes par AAPT2 ou les références de la plate-forme globale, les caractères génériques larges, les remplacements -dontoptimize et les règles de conservation réservées aux tests :

# Audit a single package directory
./build/make/core/proguard/tools/pgaudit.py packages/apps/Provision/

# Scan a subsystem tree and write a structured JSON report
./build/make/core/proguard/tools/pgaudit.py packages/ --json=audit_report.json

Les équipes de plate-forme et d'OEM peuvent combiner pgaudit.py pour un tri rapide de l'arborescence source, process-keep-rules pour valider les règles de bibliothèque exportées, et R8_DUMP_KEEP_RADIUS=true avec KeepRadiusHtmlReportGenerator pour mesurer le rayon de conservation exact de la classe, du champ et de la méthode de chaque règle d'un build.