Dexpreopt et <uses-library> vérifications

Le système de compilation d'Android 12 a été modifié pour la compilation anticipée (ou "compilation AOT") des fichiers DEX (dexpreopt) pour les modules Java ayant des dépendances <uses-library>. Dans certains cas, ces modifications du système de compilation peuvent endommager les builds. Utilisez cette page pour vous préparer aux défaillances, et suivez les recettes décrites sur cette page pour les corriger et les atténuer.

Dexpreopt est le processus de compilation anticipée des bibliothèques et applications Java. Dexpreopt se produit sur l'hôte au moment de la compilation (contrairement à dexopt, qui se produit sur l'appareil). La structure des dépendances de bibliothèque partagée utilisée par un module Java (bibliothèque ou application) est appelée contexte de chargeur de classe (CLC). Pour garantir l'exactitude de dexpreopt, les commissions au niveau d'une catégorie au moment de la compilation et de l'exécution doivent coïncider. Le CLC au moment de la compilation est ce que le compilateur dex2oat utilise au moment de la compilation dexpreopt (il est enregistré dans les fichiers ODEX), et le CLC au moment de l&#39;exécution est le contexte dans lequel le code précompilé est chargé sur l&#39;appareil.

Ces CLC de compilation et d&#39;exécution doivent coïncider pour des raisons d&#39;exactitude et de performances. Pour des raisons de correction, il est nécessaire de gérer les classes en double. Si les dépendances de la bibliothèque partagée au moment de l'exécution sont différentes de celles utilisées pour la compilation, certaines classes peuvent être résolues différemment, ce qui entraîne de subtils bugs lors de l'exécution. Les performances sont également affectées par les vérifications d&#39;exécution des classes en double.

Cas d&#39;utilisation concernés

Le premier démarrage est le principal cas d'utilisation affecté par ces modifications: si ART détecte une incohérence entre les CLC au moment de la compilation et de l'exécution, il rejette les artefacts dexpreopt et exécute dexopt à la place. Pour les démarrages ultérieurs, ce n'est pas un problème, car les applications peuvent être désactivées en arrière-plan et stockées sur le disque.

Composants Android concernés

Cela affecte toutes les applications et bibliothèques Java qui ont des dépendances d&#39;exécution sur d&#39;autres bibliothèques Java. Android compte des milliers d&#39;applications, dont des centaines utilisent des bibliothèques partagées. Les partenaires sont également concernés, car ils ont leurs propres bibliothèques et applications.

Interrompre les modifications

Le système de compilation doit connaître les dépendances <uses-library> avant de générer des règles de compilation dexpreopt. Toutefois, il ne peut pas accéder directement au fichier manifeste ni lire les tags <uses-library> qu'il contient, car le système de compilation n'est pas autorisé à lire des fichiers arbitraires lorsqu'il génère des règles de compilation (pour des raisons de performances). De plus, le fichier manifeste peut être empaqueté dans un APK ou un fichier prédéfini. Par conséquent, les informations <uses-library> doivent être présentes dans les fichiers de compilation (Android.bp ou Android.mk).

Auparavant, ART utilisait une solution de contournement qui ignorait les dépendances des bibliothèques partagées (connue sous le nom de &-classpath). Ce comportement n'était pas sécurisé et entraînait des bugs subtils. Cette solution a donc été supprimée dans Android 12.

Par conséquent, les modules Java qui ne fournissent pas les bonnes informations <uses-library> dans leurs fichiers de compilation peuvent entraîner des interruptions de compilation (provoquées par une non-concordance de la charge de travail au moment de la compilation) ou des régressions au premier démarrage (provoquées par une non-concordance au moment du démarrage suivie de dexopt).

Chemin de migration

Pour corriger un build défectueux, procédez comme suit:

  1. Désactiver globalement la vérification au moment de la compilation pour un produit spécifique à l&#39;aide d&#39;un paramètre

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    dans le fichier makefile du produit. Cela corrige les erreurs de compilation (sauf dans les cas particuliers listés dans la section Corriger les erreurs). Toutefois, il s'agit d'une solution temporaire qui peut entraîner une incohérence au niveau des commissions au niveau du démarrage et de dexopt.

  2. Corrigez les modules qui ont échoué avant d'avoir désactivé globalement la vérification au moment de la compilation en ajoutant les informations <uses-library> nécessaires à leurs fichiers de compilation (pour en savoir plus, consultez la section Corriger les erreurs). Pour la plupart des modules, cela nécessite d'ajouter quelques lignes dans Android.bp ou Android.mk.

  3. Désactivez la vérification au moment de la compilation et dexpreopt pour les cas problématiques, module par module. Désactivez dexpreopt afin de ne pas perdre de temps de compilation ni de stockage sur des artefacts refusés au démarrage.

  4. Réactivez globalement la vérification de l'heure de compilation en désactivant PRODUCT_BROKEN_VERIFY_USES_LIBRARIES défini à l'étape 1. La compilation ne doit pas échouer après cette modification (à cause des étapes 2 et 3).

  5. Corrigez les modules que vous avez désactivés à l'étape 3, un par un, puis réactivez dexpreopt et la vérification <uses-library>. Signalez les bugs si nécessaire.

Les vérifications <uses-library> au moment de la compilation sont appliquées dans Android 12.

Résoudre les problèmes de non-fonctionnement

Les sections suivantes vous expliquent comment corriger des types de pannes spécifiques.

Erreur de compilation : incohérence CLC

Le système de compilation effectue une vérification de cohérence au moment de la compilation entre les informations contenues dans les fichiers Android.bp ou Android.mk et le fichier manifeste. Le système de compilation ne peut pas lire le fichier manifeste, mais il peut générer des règles de compilation pour le lire (en l'extrayant d'un APK si nécessaire) et comparer les balises <uses-library> du fichier manifeste aux informations <uses-library> des fichiers de compilation. Si la vérification échoue, l'erreur se présente comme suit:

error: mismatch in the <uses-library> tags between the build system and the manifest:
    - required libraries in build system: []
                     vs. in the manifest: [org.apache.http.legacy]
    - optional libraries in build system: []
                     vs. in the manifest: [com.x.y.z]
    - tags in the manifest (.../X_intermediates/manifest/AndroidManifest.xml):
        <uses-library android:name="com.x.y.z"/>
        <uses-library android:name="org.apache.http.legacy"/>

note: the following options are available:
    - to temporarily disable the check on command line, rebuild with RELAX_USES_LIBRARY_CHECK=true (this will set compiler filter "verify" and disable AOT-compilation in dexpreopt)
    - to temporarily disable the check for the whole product, set PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true in the product makefiles
    - to fix the check, make build system properties coherent with the manifest
    - see build/make/Changes.md for details

Comme le suggère le message d'erreur, il existe plusieurs solutions, en fonction de l'urgence :

  • Pour une correction temporaire à l'échelle du produit, définissez PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true dans le fichier makefile du produit. La vérification de la cohérence au moment de la compilation est toujours effectuée, mais un échec de vérification ne signifie pas que la compilation a échoué. Au lieu de cela, un échec de vérification oblige le système de compilation à rétrograder le filtre du compilateur dex2oat vers verify dans dexpreopt, ce qui désactive complètement la compilation AOT pour ce module.
  • Pour une correction globale rapide via la ligne de commande, utilisez la variable d'environnement RELAX_USES_LIBRARY_CHECK=true. Il a le même effet que PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, mais il est destiné à être utilisé avec la ligne de commande. La variable d'environnement remplace la variable de produit.
  • Pour corriger l'erreur liée à l'origine du problème, informez le système de compilation des balises <uses-library> dans le fichier manifeste. Une inspection du message d'erreur indique les bibliothèques à l'origine du problème (tout comme l'inspection de AndroidManifest.xml ou du fichier manifeste dans un APK qui peut être vérifié avec aapt dump badging $APK | grep uses-library).

Pour les modules Android.bp:

  1. Recherchez la bibliothèque manquante dans la propriété libs du module. Le cas échéant, Soong ajoute normalement ces bibliothèques automatiquement, sauf dans les cas particuliers suivants:

    • La bibliothèque n'est pas une bibliothèque de SDK (elle est définie comme java_library plutôt que java_sdk_library).
    • La bibliothèque a un nom de bibliothèque (dans le fichier manifeste) différent de son nom de module (dans le système de compilation).

    Pour résoudre ce problème temporairement, ajoutez provides_uses_lib: "<library-name>" dans la définition de la bibliothèque Android.bp. Pour une solution à long terme, corrigez le problème sous-jacent: convertissez la bibliothèque en bibliothèque SDK ou renommez son module.

  2. Si l'étape précédente n'a pas fourni de résolution, ajoutez uses_libs: ["<library-module-name>"] pour les bibliothèques requises ou optional_uses_libs: ["<library-module-name>"] pour les bibliothèques facultatives à la définition Android.bp du module. Ces propriétés acceptent une liste de noms de modules. L&#39;ordre relatif des bibliothèques dans la liste doit être le même que celui du fichier manifeste.

Pour les modules Android.mk:

  1. Vérifiez si le nom de la bibliothèque (dans le fichier manifeste) est différent de son nom de module (dans le système de compilation). Si c'est le cas, corrigez ce problème temporairement en ajoutant LOCAL_PROVIDES_USES_LIBRARY := <library-name> dans le fichier Android.mk de la bibliothèque ou ajoutez provides_uses_lib: "<library-name>" dans le fichier Android.bp de la bibliothèque (ces deux cas sont possibles, car un module Android.mk peut dépendre d'une bibliothèque Android.bp). Pour une solution à long terme, corrigez le problème sous-jacent: renommez le module de bibliothèque.

  2. Ajoutez LOCAL_USES_LIBRARIES := <library-module-name> pour les bibliothèques requises et LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name> pour les bibliothèques facultatives à la définition Android.mk du module. Ces propriétés acceptent une liste de noms de modules. L'ordre relatif des bibliothèques de la liste doit être le même que dans le fichier manifeste.

Erreur de compilation: chemin d'accès à la bibliothèque inconnu

Si le système de compilation ne trouve pas de chemin d'accès à un fichier JAR DEX <uses-library> (chemin d'accès au moment de la compilation sur l'hôte ou chemin d'installation sur l'appareil), la compilation échoue généralement. L'échec de la recherche d'un chemin d'accès peut indiquer que la bibliothèque est configurée de manière inattendue. Corrigez temporairement la compilation en désactivant dexpreopt pour le module problématique.

Android.bp (propriétés du module):

enforce_uses_libs: false,
dex_preopt: {
    enabled: false,
},

Android.mk (variables de module):

LOCAL_ENFORCE_USES_LIBRARIES := false
LOCAL_DEX_PREOPT := false

Signalez un bug pour examiner les scénarios non pris en charge.

Erreur de compilation : dépendance de bibliothèque manquante

Une tentative d'ajout de <uses-library> X à partir du fichier manifeste du module Y au fichier de compilation pour Y peut entraîner une erreur de compilation en raison de la dépendance manquante, X.

Voici un exemple de message d&#39;erreur pour les modules Android.bp:

"Y" depends on undefined module "X"

Voici un exemple de message d&#39;erreur pour les modules Android.mk:

'.../JAVA_LIBRARIES/com.android.X_intermediates/dexpreopt.config', needed by '.../APPS/Y_intermediates/enforce_uses_libraries.status', missing and no known rule to make it

Une source courante de telles erreurs se produit lorsqu'une bibliothèque est nommée différemment de son module correspondant dans le système de compilation. Par exemple, si l'entrée <uses-library> du fichier manifeste est com.android.X, mais que le nom du module de bibliothèque est simplement X, une erreur se produit. Pour résoudre ce cas, indiquez au système de compilation que le module nommé X fournit un <uses-library> nommé com.android.X.

Voici un exemple pour les bibliothèques Android.bp (propriété de module):

provides_uses_lib: “com.android.X”,

Voici un exemple pour les bibliothèques Android.mk (variable de module):

LOCAL_PROVIDES_USES_LIBRARY := com.android.X

Non-concordance des tables de service au démarrage

Au premier démarrage, recherchez dans logcat les messages liés à une non-concordance au niveau de la communauté, comme indiqué ci-dessous:

$ adb wait-for-device && adb logcat \
  | grep -E 'ClassLoaderContext [a-z ]+ mismatch' -A1

Le résultat peut contenir des messages qui se présentent comme suit:

[...] W system_server: ClassLoaderContext shared library size mismatch Expected=..., found=... (PCL[]... | PCL[]...)
[...] I PackageDexOptimizer: Running dexopt (dexoptNeeded=1) on: ...

Si un avertissement de non-concordance CLC s'affiche, recherchez une commande dexopt pour le module défectueux. Pour résoudre ce problème, assurez-vous que la vérification de l'heure de la compilation du module réussit. Si cela ne fonctionne pas, il peut s'agir d'un cas particulier non compatible avec le système de compilation (par exemple, une application qui charge un autre APK, et non une bibliothèque). Le système de compilation ne gère pas tous les cas, car au moment de la compilation, il est impossible de savoir avec certitude ce que l'application charge au moment de l'exécution.

Contexte du chargeur de classe

Le CLC est une structure arborescente qui décrit la hiérarchie des chargeurs de classe. Le système de compilation utilise un CLC au sens strict (il ne couvre que les bibliothèques, pas les APK ni les chargeurs de classe personnalisés) : il s'agit d'un arbre de bibliothèques qui représente la fermeture transitive de toutes les dépendances <uses-library> d'une bibliothèque ou d'une application. Les éléments de niveau supérieur d'un CLC sont les dépendances <uses-library> directes spécifiées dans le fichier manifeste (le chemin d'accès aux classes). Chaque nœud d'une arborescence de commissions au niveau d'une catégorie est un nœud <uses-library> qui peut avoir ses propres sous-nœuds <uses-library>.

Étant donné que les dépendances <uses-library> sont un graphe orienté acyclique, et pas nécessairement une arborescence, les commissions au niveau d'une catégorie peuvent contenir plusieurs sous-arborescences pour la même bibliothèque. En d'autres termes, la CLC correspond au graphique de dépendances "déplié" dans une arborescence. La duplication ne se produit qu&#39;au niveau logique. Les chargeurs de classe sous-jacents réels ne sont pas dupliqués (au moment de l&#39;exécution, il n&#39;y a qu&#39;une seule instance de chargeur de classe pour chaque bibliothèque).

La CMC définit l'ordre de recherche des bibliothèques lors de la résolution des classes Java utilisées par la bibliothèque ou l'application. L'ordre de recherche est important, car les bibliothèques peuvent contenir des classes en double, et la classe est résolue en première correspondance.

CLC sur l'appareil (au moment de l'exécution)

PackageManager (dans frameworks/base) crée un CLC pour charger un module Java sur l'appareil. Il ajoute les bibliothèques listées dans les balises <uses-library> du fichier manifeste du module en tant qu'éléments CLC de niveau supérieur.

Pour chaque bibliothèque utilisée, PackageManager obtient toutes ses dépendances <uses-library> (spécifiées en tant que balises dans le fichier manifeste de cette bibliothèque) et ajoute une LCA imbriquée pour chaque dépendance. Ce processus se poursuit de manière récursive jusqu'à ce que tous les nœuds feuilles de l'arborescence CLC construite soient des bibliothèques sans dépendances <uses-library>.

PackageManager ne prend en charge que les bibliothèques partagées. La définition de "partagée" dans cette utilisation diffère de sa signification habituelle (par exemple, partagée et statique). Sous Android, les bibliothèques partagées Java sont celles listées dans les configurations XML installées sur l'appareil (/system/etc/permissions/platform.xml). Chaque entrée contient le nom d'une bibliothèque partagée, un chemin d'accès à son fichier JAR DEX et une liste de dépendances (autres bibliothèques partagées que celle-ci utilise au moment de l'exécution et spécifiées dans les balises <uses-library> de son fichier manifeste).

En d'autres termes, il existe deux sources d'informations qui permettent à PackageManager de créer des CLC au moment de l'exécution : les balises <uses-library> dans le fichier manifeste et les dépendances de bibliothèque partagées dans les configurations XML.

CLC sur l'hôte (au moment de la compilation)

Les commissions au niveau d'une catégorie ne sont pas nécessaires uniquement lors du chargement d'une bibliothèque ou d'une application. Elles sont également nécessaires lors de la compilation d'une bibliothèque ou d'une application. La compilation peut se produire sur l'appareil (dexopt) ou lors de la compilation (dexpreopt). Étant donné que dexopt s'exécute sur l'appareil, il contient les mêmes informations que PackageManager (fichiers manifestes et dépendances de bibliothèques partagées). Dexpreopt, cependant, se déroule sur l'hôte et dans un environnement totalement différent, et doit obtenir les mêmes informations auprès du système de compilation.

Ainsi, la CLC au moment de la compilation utilisée par dexpreopt et la CLC au moment de l'exécution utilisée par PackageManager sont la même, mais calculées de deux manières différentes.

Les CLC au moment de la compilation et de l'exécution doivent coïncider, sinon le code compilé par AOT créé par dexpreopt sera rejeté. Pour vérifier l'égalité des CLC au moment de la compilation et de l'exécution, le compilateur dex2oat enregistre le CLC au moment de la compilation dans les fichiers *.odex (dans le champ classpath de l'en-tête du fichier OAT). Pour trouver le CLC stocké, utilisez la commande suivante :

oatdump --oat-file=<FILE> | grep '^classpath = '

Un décalage entre le CLC au moment de la compilation et au moment de l'exécution est signalé dans logcat au démarrage. Recherchez-le à l'aide de la commande suivante:

logcat | grep -E 'ClassLoaderContext [a-z ]+ mismatch'

Les incohérences ont un impact négatif sur les performances, car elles obligent la bibliothèque ou l'application à être désoptées pour s'exécuter sans optimisation (par exemple, le code de l'application peut avoir besoin d'être extrait en mémoire de l'APK, ce qui est une opération très coûteuse).

Une bibliothèque partagée peut être facultative ou obligatoire. Du point de vue de dexpreopt, une bibliothèque requise doit être présente au moment de la compilation (son absence est une erreur de compilation). Une bibliothèque facultative peut être présente ou absente au moment de la compilation : si elle est présente, elle est ajoutée au CLC, transmise à dex2oat et enregistrée dans le fichier *.odex. Si une bibliothèque facultative est absente, elle est ignorée et n'est pas ajoutée à la CLC. S'il existe un décalage entre l'état au moment de la compilation et l'état au moment de l'exécution (la bibliothèque facultative est présente dans un cas, mais pas dans l'autre), les CLC au moment de la compilation et au moment de l'exécution ne correspondent pas et le code compilé est rejeté.

Détails avancés du système de compilation (outil de fixation du fichier manifeste)

Il arrive que des balises <uses-library> manquent dans le fichier manifeste source d'une bibliothèque ou d'une application. Cela peut se produire, par exemple, si l'une des dépendances transitives de la bibliothèque ou de l'application commence à utiliser une autre balise <uses-library> et que le fichier manifeste de la bibliothèque ou de l'application n'est pas mis à jour pour l'inclure.

Soong peut calculer automatiquement certaines des balises <uses-library> manquantes pour une bibliothèque ou une application donnée, comme les bibliothèques de SDK dans la fermeture de dépendance transitive de la bibliothèque ou de l'application. La fermeture est nécessaire, car la bibliothèque (ou l'application) peut dépendre d'une bibliothèque statique qui dépend d'une bibliothèque de SDK, et peut-être à nouveau dépendre de manière transitive via une autre bibliothèque.

Toutes les balises <uses-library> ne peuvent pas être calculées de cette manière, mais dans la mesure du possible, il est préférable de laisser Soong ajouter automatiquement des entrées de fichier manifeste. Cela réduit le risque d'erreurs et simplifie la maintenance. Par exemple, lorsque de nombreuses applications utilisent une bibliothèque statique qui ajoute une nouvelle dépendance <uses-library>, toutes les applications doivent être mises à jour, ce qui est difficile à gérer.