Dexpreopt- und -Prüfungen

In Android 12 gibt es Änderungen am Build-System für die AOT-Kompilierung (Ahead-of-Time) von DEX-Dateien (dexpreopt) für Java-Module mit <uses-library>-Abhängigkeiten. In einigen Fällen können diese Änderungen am Build-System zu Build-Fehlern führen. Auf dieser Seite erfahren Sie, wie Sie sich auf Fehler vorbereiten und wie Sie sie mithilfe der Anleitungen auf dieser Seite beheben und minimieren können.

Dexpreopt ist die Ahead-of-Time-Kompilierung von Java-Bibliotheken und ‑Apps. Die Dexpreopt-Optimierung erfolgt auf dem Host zur Build-Zeit (im Gegensatz zu dexopt, die auf dem Gerät erfolgt). Die Struktur der von einem Java-Modul (einer Bibliothek oder einer App) verwendeten Abhängigkeiten von gemeinsam genutzten Bibliotheken wird als Klassenladerkontext (Class Loader Context, CLC) bezeichnet. Damit dexpreopt korrekt ausgeführt wird, müssen die Build- und Laufzeit-CLCs übereinstimmen. Der Build-Zeit-CLC wird vom dex2oat-Compiler zur Dexpreopt-Zeit verwendet (er wird in den ODEX-Dateien aufgezeichnet). Der Laufzeit-CLC ist der Kontext, in dem der vorkompilierte Code auf dem Gerät geladen wird.

Diese CLCs für die Build- und Laufzeit müssen aus Gründen der Korrektheit und Leistung übereinstimmen. Um die Richtigkeit zu gewährleisten, müssen doppelte Klassen behandelt werden. Wenn sich die Abhängigkeiten der freigegebenen Bibliothek zur Laufzeit von denen unterscheiden, die für die Kompilierung verwendet werden, werden einige der Klassen möglicherweise anders aufgelöst, was zu subtilen Laufzeitfehlern führen kann. Die Leistung wird auch durch die Laufzeitprüfungen auf doppelte Klassen beeinträchtigt.

Betroffene Anwendungsfälle

Der erste Bootvorgang ist der Hauptanwendungsfall, der von diesen Änderungen betroffen ist: Wenn ART eine Diskrepanz zwischen den CLCs zur Build-Zeit und zur Laufzeit erkennt, werden dexpreopt-Artefakte abgelehnt und stattdessen dexopt ausgeführt. Bei nachfolgenden Starts ist das kein Problem, da die Apps im Hintergrund dexoptimiert und auf der Festplatte gespeichert werden können.

Betroffene Bereiche von Android

Dies betrifft alle Java-Apps und ‑Bibliotheken, die Laufzeitabhängigkeiten von anderen Java-Bibliotheken haben. Für Android gibt es Tausende von Apps, von denen Hunderte freigegebene Bibliotheken verwenden. Auch Partner sind betroffen, da sie eigene Bibliotheken und Apps haben.

Funktionsgefährdende Änderungen

Das Build-System muss die <uses-library>-Abhängigkeiten kennen, bevor es dexpreopt-Build-Regeln generiert. Das Build-System kann jedoch nicht direkt auf das Manifest zugreifen und die <uses-library>-Tags darin lesen, da es aus Leistungsgründen keine beliebigen Dateien lesen darf, wenn es Build-Regeln generiert. Außerdem kann das Manifest in einem APK oder einem Prebuilt enthalten sein. Daher müssen die <uses-library>-Informationen in den Build-Dateien (Android.bp oder Android.mk) vorhanden sein.

Bisher wurde in ART ein Workaround verwendet, bei dem Abhängigkeiten von freigegebenen Bibliotheken ignoriert wurden (&-classpath). Das war unsicher und führte zu subtilen Fehlern. Daher wurde der Workaround in Android 12 entfernt.

Daher können Java-Module, die in ihren Build-Dateien keine korrekten <uses-library>-Informationen enthalten, zu Build-Fehlern (verursacht durch eine CLC-Inkompatibilität zur Build-Zeit) oder Regressionen bei der ersten Boot-Zeit (verursacht durch eine CLC-Inkompatibilität zur Boot-Zeit, gefolgt von dexopt) führen.

Migrationspfad

So beheben Sie einen fehlerhaften Build:

  1. Globale Deaktivierung der Build-Zeit-Prüfung für ein bestimmtes Produkt durch Festlegen von

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    im Produkt-Makefile. Dadurch werden Build-Fehler behoben (mit Ausnahme von Sonderfällen, die im Abschnitt Fehlerbehebung aufgeführt sind). Dies ist jedoch nur eine vorübergehende Problemumgehung und kann zu einer CLC-Fehlerbehebung während des Bootvorgangs und anschließendem Dexopt führen.

  2. Beheben Sie die Fehler in den Modulen, die fehlgeschlagen sind, bevor Sie die Build-Zeit-Prüfung global deaktiviert haben. Fügen Sie dazu die erforderlichen <uses-library>-Informationen in die Build-Dateien ein (weitere Informationen finden Sie unter Fehlerbehebung). Bei den meisten Modulen müssen dazu einige Zeilen in Android.bp oder in Android.mk hinzugefügt werden.

  3. Deaktivieren Sie die Build-Zeit-Prüfung und dexpreopt für die problematischen Fälle auf Modulbasis. Deaktivieren Sie „dexpreopt“, damit Sie keine Build-Zeit und keinen Speicherplatz für Artefakte verschwenden, die beim Booten abgelehnt werden.

  4. Aktivieren Sie die Build-Zeit-Prüfung global wieder, indem Sie PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, das in Schritt 1 festgelegt wurde, wieder entfernen. Der Build sollte nach dieser Änderung nicht fehlschlagen (aufgrund der Schritte 2 und 3).

  5. Beheben Sie die Fehler in den Modulen, die Sie in Schritt 3 deaktiviert haben, einzeln und aktivieren Sie dann „dexpreopt“ und die <uses-library>-Prüfung wieder. Melden Sie bei Bedarf Fehler.

<uses-library>-Prüfungen zur Build-Zeit werden in Android 12 erzwungen.

Fehler beheben

In den folgenden Abschnitten erfahren Sie, wie Sie bestimmte Arten von Fehlern beheben.

Build-Fehler: CLC-Abweichung

Das Build-System führt zur Build-Zeit eine Kohärenzprüfung zwischen den Informationen in Android.bp- oder Android.mk-Dateien und dem Manifest durch. Das Build-System kann das Manifest nicht lesen, aber Build-Regeln generieren, um das Manifest zu lesen (bei Bedarf aus einer APK-Datei extrahieren) und <uses-library>-Tags im Manifest mit den <uses-library>-Informationen in den Build-Dateien vergleichen. Wenn die Prüfung fehlschlägt, sieht der Fehler so aus:

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

Wie die Fehlermeldung schon sagt, gibt es je nach Dringlichkeit mehrere Lösungen:

  • Für eine vorübergehende produktweite Korrektur legen Sie PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true in der Produkt-Makefile fest. Die Kohärenzprüfung zur Build-Zeit wird weiterhin durchgeführt, aber ein Fehler bei der Prüfung führt nicht zu einem Build-Fehler. Stattdessen führt ein Fehler bei der Prüfung dazu, dass das Build-System den dex2oat-Compilerfilter in dexpreopt auf verify herabstuft. Dadurch wird die AOT-Kompilierung für dieses Modul vollständig deaktiviert.
  • Für eine schnelle, globale Korrektur über die Befehlszeile verwenden Sie die Umgebungsvariable RELAX_USES_LIBRARY_CHECK=true. Es hat dieselbe Wirkung wie PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, ist aber für die Verwendung in der Befehlszeile vorgesehen. Die Umgebungsvariable überschreibt die Produktvariable.
  • Damit das Problem behoben wird, muss das Build-System die <uses-library>-Tags im Manifest erkennen. Eine Prüfung der Fehlermeldung zeigt, welche Bibliotheken das Problem verursachen. Das lässt sich auch durch eine Prüfung von AndroidManifest.xml oder des Manifests in einer APK-Datei feststellen, die mit `aapt dump badging $APK | grep uses-library` geprüft werden kann.

Für Android.bp-Module:

  1. Suchen Sie in der libs-Eigenschaft des Moduls nach der fehlenden Bibliothek. Wenn sie vorhanden ist, fügt Soong solche Bibliotheken normalerweise automatisch hinzu, außer in den folgenden Sonderfällen:

    • Die Bibliothek ist keine SDK-Bibliothek, sondern als java_library und nicht als java_sdk_library definiert.
    • Die Bibliothek hat im Manifest einen anderen Namen als ihr Modul im Build-System.

    Um dieses Problem vorübergehend zu beheben, fügen Sie provides_uses_lib: "<library-name>" in die Android.bp-Bibliotheksdefinition ein. Als langfristige Lösung sollten Sie das zugrunde liegende Problem beheben: Konvertieren Sie die Bibliothek in eine SDK-Bibliothek oder benennen Sie das Modul um.

  2. Wenn das Problem durch den vorherigen Schritt nicht behoben wurde, fügen Sie uses_libs: ["<library-module-name>"] für erforderliche Bibliotheken oder optional_uses_libs: ["<library-module-name>"] für optionale Bibliotheken zur Android.bp-Definition des Moduls hinzu. Diese Properties akzeptieren eine Liste mit Modulnamen. Die relative Reihenfolge der Bibliotheken in der Liste muss mit der Reihenfolge im Manifest übereinstimmen.

Für Android.mk-Module:

  1. Prüfen Sie, ob die Bibliothek im Manifest einen anderen Bibliotheksnamen als im Build-System hat. Wenn das der Fall ist, können Sie das Problem vorübergehend beheben, indem Sie LOCAL_PROVIDES_USES_LIBRARY := <library-name> in die Datei Android.mk der Bibliothek einfügen oder provides_uses_lib: "<library-name>" in die Datei Android.bp der Bibliothek einfügen. Beide Fälle sind möglich, da ein Android.mk-Modul von einer Android.bp-Bibliothek abhängen kann. Um das Problem langfristig zu beheben, müssen Sie das zugrunde liegende Problem beheben: Benennen Sie das Bibliotheksmodul um.

  2. Fügen Sie LOCAL_USES_LIBRARIES := <library-module-name> für erforderliche Bibliotheken und LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name> für optionale Bibliotheken zur Android.mk-Definition des Moduls hinzu. Für diese Properties kann eine Liste mit Modulnamen angegeben werden. Die relative Reihenfolge der Bibliotheken in der Liste muss mit der im Manifest übereinstimmen.

Build-Fehler: Unbekannter Bibliothekspfad

Wenn das Build-System keinen Pfad zu einem <uses-library>-DEX-JAR finden kann (entweder ein Build-Zeit-Pfad auf dem Host oder ein Installationspfad auf dem Gerät), schlägt der Build in der Regel fehl. Wenn kein Pfad gefunden wird, kann das darauf hindeuten, dass die Bibliothek auf unerwartete Weise konfiguriert ist. Sie können den Build vorübergehend korrigieren, indem Sie „dexpreopt“ für das problematische Modul deaktivieren.

Android.bp (Moduleigenschaften):

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

Android.mk (Modulvariablen):

LOCAL_ENFORCE_USES_LIBRARIES := false
LOCAL_DEX_PREOPT := false

Melden Sie einen Fehler, um nicht unterstützte Szenarien zu untersuchen.

Build-Fehler: Fehlende Bibliotheksabhängigkeit

Wenn Sie versuchen, <uses-library> X aus dem Manifest von Modul Y in die Build-Datei für Y einzufügen, kann dies zu einem Build-Fehler aufgrund der fehlenden Abhängigkeit X führen.

Dies ist eine Beispiel-Fehlermeldung für Android.bp-Module:

"Y" depends on undefined module "X"

Dies ist eine Beispiel-Fehlermeldung für Android.mk-Module:

'.../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

Eine häufige Ursache für solche Fehler ist, wenn eine Bibliothek anders benannt ist als das entsprechende Modul im Build-System. Wenn der Manifesteintrag <uses-library> beispielsweise com.android.X lautet, der Name des Bibliotheksmoduls aber nur X ist, führt das zu einem Fehler. Um dieses Problem zu beheben, müssen Sie dem Build-System mitteilen, dass das Modul mit dem Namen X einen <uses-library> mit dem Namen com.android.X bereitstellt.

Hier ein Beispiel für Android.bp-Bibliotheken (Moduleigenschaft):

provides_uses_lib: “com.android.X”,

Hier ist ein Beispiel für Android.mk-Bibliotheken (Modulvariable):

LOCAL_PROVIDES_USES_LIBRARY := com.android.X

CLC-Abweichung zur Bootzeit

Suchen Sie beim ersten Start im Logcat nach Nachrichten im Zusammenhang mit CLC-Abweichungen, wie unten dargestellt:

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

Die Ausgabe kann Meldungen in der folgenden Form enthalten:

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

Wenn Sie eine Warnung wegen eines CLC-Konflikts erhalten, suchen Sie nach einem dexopt-Befehl für das fehlerhafte Modul. Achten Sie darauf, dass die Build-Zeit-Prüfung für das Modul bestanden wird, um das Problem zu beheben. Wenn das nicht funktioniert, handelt es sich möglicherweise um einen Sonderfall, der vom Build-System nicht unterstützt wird, z. B. eine App, die ein anderes APK und keine Bibliothek lädt. Das Build-System kann nicht alle Fälle abdecken, da zum Zeitpunkt des Builds nicht mit Sicherheit bekannt ist, was die App zur Laufzeit lädt.

Kontext des Klassenladers

Der CLC ist eine baumartige Struktur, die die Hierarchie der Klassen-Loader beschreibt. Das Build-System verwendet CLC im engeren Sinne (es umfasst nur Bibliotheken, nicht APKs oder benutzerdefinierte Klassenlader): Es ist ein Baum von Bibliotheken, der den transitiven Abschluss aller <uses-library>-Abhängigkeiten einer Bibliothek oder App darstellt. Die Elemente der obersten Ebene eines CLC sind die direkten <uses-library>-Abhängigkeiten, die im Manifest (dem Klassenpfad) angegeben sind. Jeder Knoten eines CLC-Baums ist ein <uses-library>-Knoten, der eigene <uses-library>-Unterknoten haben kann.

Da <uses-library>-Abhängigkeiten ein gerichteter azyklischer Graph und nicht unbedingt ein Baum sind, kann CLC mehrere Unterbäume für dieselbe Bibliothek enthalten. Mit anderen Worten: CLC ist der Abhängigkeitsgraph, der zu einem Baum „entfaltet“ wird. Die Duplizierung erfolgt nur auf logischer Ebene. Die zugrunde liegenden Klassenlader werden nicht dupliziert. Zur Laufzeit gibt es für jede Bibliothek eine einzelne Klassenladerinstanz.

CLC definiert die Reihenfolge, in der Bibliotheken durchsucht werden, wenn Java-Klassen aufgelöst werden, die von der Bibliothek oder App verwendet werden. Die Reihenfolge ist wichtig, da Bibliotheken doppelte Klassen enthalten können und die Klasse beim ersten Treffer aufgelöst wird.

Auf dem Gerät (Laufzeit)

PackageManager (in frameworks/base) erstellt einen CLC zum Laden eines Java-Moduls auf dem Gerät. Dadurch werden die in den <uses-library>-Tags aufgeführten Bibliotheken im Manifest des Moduls als CLC-Elemente der obersten Ebene hinzugefügt.

Für jede verwendete Bibliothek ruft PackageManager alle zugehörigen <uses-library>-Abhängigkeiten ab (die als Tags im Manifest der Bibliothek angegeben sind) und fügt für jede Abhängigkeit einen verschachtelten CLC hinzu. Dieser Vorgang wird rekursiv fortgesetzt, bis alle Blattknoten des erstellten CLC-Baums Bibliotheken ohne <uses-library>-Abhängigkeiten sind.

PackageManager kennt nur gemeinsam genutzte Bibliotheken. Die Definition von „freigegeben“ in diesem Zusammenhang unterscheidet sich von der üblichen Bedeutung (wie bei „freigegeben“ im Gegensatz zu „statisch“). In Android sind Java-Bibliotheken diejenigen, die in XML-Konfigurationen aufgeführt sind, die auf dem Gerät installiert sind (/system/etc/permissions/platform.xml). Jeder Eintrag enthält den Namen einer freigegebenen Bibliothek, einen Pfad zu ihrer DEX-JAR-Datei und eine Liste von Abhängigkeiten (andere freigegebene Bibliotheken, die diese zur Laufzeit verwendet, und die in <uses-library>-Tags in ihrem Manifest angegeben sind).

Mit anderen Worten: Es gibt zwei Informationsquellen, die es PackageManager ermöglichen, die CLC zur Laufzeit zu erstellen: <uses-library>-Tags im Manifest und Abhängigkeiten von gemeinsam genutzten Bibliotheken in XML-Konfigurationen.

On-Host-CLC (Build-Zeit)

CLC ist nicht nur beim Laden einer Bibliothek oder App erforderlich, sondern auch beim Kompilieren. Die Kompilierung kann entweder auf dem Gerät (dexopt) oder während des Builds (dexpreopt) erfolgen. Da dexopt auf dem Gerät ausgeführt wird, sind dieselben Informationen wie für PackageManager verfügbar (Manifeste und Abhängigkeiten von gemeinsam genutzten Bibliotheken). Dexpreopt findet jedoch auf dem Host und in einer völlig anderen Umgebung statt und muss dieselben Informationen vom Build-System abrufen.

Der Build-Time-CLC, der von dexpreopt verwendet wird, und der Laufzeit-CLC, der von PackageManager verwendet wird, sind also identisch, werden aber auf zwei verschiedene Arten berechnet.

Die CLCs für die Build- und Laufzeit müssen übereinstimmen, da der von dexpreopt erstellte AOT-kompilierte Code sonst abgelehnt wird. Um die Gleichheit von Build-Time- und Laufzeit-CLCs zu prüfen, zeichnet der dex2oat-Compiler Build-Time-CLCs in den *.odex-Dateien auf (im Feld classpath des OAT-Dateiheaders). Verwenden Sie den folgenden Befehl, um den gespeicherten CLC zu finden:

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

Während des Bootvorgangs wird in Logcat eine Abweichung zwischen der CLC-Build-Zeit und der CLC-Laufzeit gemeldet. Suchen Sie mit diesem Befehl danach:

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

Eine solche Diskrepanz wirkt sich negativ auf die Leistung aus, da die Bibliothek oder App entweder dexoptiert werden muss oder ohne Optimierungen ausgeführt wird. Beispielsweise muss der Code der App möglicherweise aus dem APK in den Arbeitsspeicher extrahiert werden, was sehr aufwendig ist.

Eine gemeinsam genutzte Mediathek kann optional oder erforderlich sein. Aus Sicht von dexpreopt muss eine erforderliche Bibliothek zur Build-Zeit vorhanden sein. Andernfalls tritt ein Build-Fehler auf. Eine optionale Bibliothek kann zur Build-Zeit entweder vorhanden sein oder nicht. Wenn sie vorhanden ist, wird sie dem CLC hinzugefügt, an dex2oat übergeben und in der Datei *.odex aufgezeichnet. Wenn eine optionale Bibliothek fehlt, wird sie übersprungen und nicht in die CLC aufgenommen. Wenn der Status zur Build-Zeit und zur Laufzeit nicht übereinstimmt (die optionale Bibliothek ist in einem Fall vorhanden, im anderen jedoch nicht), stimmen die CLCs zur Build-Zeit und zur Laufzeit nicht überein und der kompilierte Code wird abgelehnt.

Erweiterte Details zum Build-System (Manifest-Fixer)

Manchmal fehlen <uses-library>-Tags im Quellmanifest einer Bibliothek oder App. Das kann beispielsweise passieren, wenn eine der transitiven Abhängigkeiten der Bibliothek oder App ein anderes <uses-library>-Tag verwendet und das Manifest der Bibliothek oder App nicht aktualisiert wird, um es einzuschließen.

Soong kann einige der fehlenden <uses-library>-Tags für eine bestimmte Bibliothek oder App automatisch berechnen, da die SDK-Bibliotheken im transitiven Abhängigkeitsabschluss der Bibliothek oder App enthalten sind. Der Abschluss ist erforderlich, da die Bibliothek (oder App) möglicherweise von einer statischen Bibliothek abhängig ist, die von einer SDK-Bibliothek abhängig ist, und möglicherweise wieder transitiv von einer anderen Bibliothek abhängig ist.

Nicht alle <uses-library>-Tags können auf diese Weise berechnet werden. Wenn möglich, ist es jedoch besser, wenn Soong Manifesteinträge automatisch hinzufügt. Das ist weniger fehleranfällig und vereinfacht die Wartung. Wenn beispielsweise viele Apps eine statische Bibliothek verwenden, die eine neue <uses-library>-Abhängigkeit hinzufügt, müssen alle Apps aktualisiert werden, was schwierig zu verwalten ist.