Dexpreopt i sprawdzanie atrybutu <uses-library>

W Androidzie 12 wprowadzono zmiany w systemie kompilacji AOT plików DEX (dexpreopt) w przypadku modułów Javy, które mają zależności <uses-library>. W niektórych przypadkach zmiany w systemie kompilacji mogą spowodować awarię kompilacji. Na tej stronie możesz się przygotować na przypadki awarii i zapoznać się z metodami ich rozwiązywania.

Dexpreopt to proces wstępnej kompilacji bibliotek i aplikacji Java. Narzędzie dexpreopt działa na hoście w czasie kompilacji (w przeciwieństwie do narzędzia dexopt, które działa na urządzeniu). Struktura zależności biblioteki współdzielonej używanej przez moduł Java (biblioteka lub aplikacja) jest nazywana kontekstem ładowarki klas (CLC). Aby zagwarantować poprawność dexpreopt, CLC w czasie kompilacji i w czasie wykonywania muszą być takie same. CLC w czasie kompilacji to wartość używana przez kompilator dex2oat w czasie dexpreopt (jest zapisana w plikach ODEX), a CLC w czasie wykonywania to kontekst, w którym skompilowany kod jest ładowany na urządzeniu.

Te CLC w czasie kompilacji i w czasie wykonywania muszą być zgodne ze względu na poprawność i wydajność. Ze względu na poprawność konieczne jest uwzględnienie zduplikowanych zajęć. Jeśli zależności od współdzielonej biblioteki w czasie wykonywania są inne niż te używane do kompilacji, niektóre klasy mogą być rozwiązywane inaczej, co może powodować drobne błędy w czasie wykonywania. Na wydajność wpływają też kontrole czasu wykonywania dotyczące duplikatów klas.

Przypadki użycia, których dotyczy problem

Pierwsze uruchomienie jest głównym przypadkiem użycia, na który mają wpływ te zmiany: jeśli ART wykryje niezgodność między CLC w czasie kompilacji a w czasie wykonywania, odrzuca artefakty dexpreopt i zamiast tego uruchamia dexopt. W przypadku kolejnych rozruchów nie ma to znaczenia, ponieważ aplikacje mogą być dexoptowane w tle i przechowywane na dysku.

Dotknięte obszary Androida

Ma to wpływ na wszystkie aplikacje i biblioteki Java, które mają zależności w czasie wykonywania od innych bibliotek Java. Na Androida jest tysiące aplikacji, a setki z nich korzysta z bibliotek współdzielonych. Dotyczy to również partnerów, którzy mają własne biblioteki i aplikacje.

Zmiana powodująca niezgodność

Zanim system kompilacji wygeneruje reguły kompilacji dexpreopt, musi poznać zależności <uses-library>. Nie może jednak uzyskać bezpośredniego dostępu do pliku manifestu ani odczytać w nim tagów <uses-library>, ponieważ system kompilacji nie może odczytywać dowolnych plików podczas generowania reguł kompilacji (ze względu na wydajność). Manifest może być również zapakowany w pliku APK lub w wersji wstępnie skompilowanej. Dlatego w plikach kompilacji (Android.bp lub Android.mk) muszą znajdować się informacje <uses-library>.

Wcześniej ART stosował obejście, które ignorowało zależności bibliotek współdzielonych (znane jako &-classpath). Było to niebezpieczne i powodowało drobne błędy, dlatego w Androidzie 12 zostało usunięte.

W rezultacie moduły Java, które nie podają prawidłowych informacji <uses-library>w plikach kompilacji, mogą powodować błędy kompilacji (spowodowane przez niezgodność CLC w czasie kompilacji) lub regresje podczas pierwszego uruchomienia (spowodowane przez niezgodność CLC w czasie uruchamiania, po której następuje dexopt).

Ścieżka migracji

Aby naprawić uszkodzoną wersję kompilacji:

  1. Wyłączanie globalnie sprawdzania w czasie kompilacji w przypadku konkretnego produktu za pomocą ustawienia

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    w pliku make produktu. Rozwiązuje to błędy kompilacji (z wyjątkiem szczególnych przypadków wymienionych w sekcji Rozwiązywanie problemów z błędami). Jest to jednak tymczasowe obejście problemu, które może powodować niezgodność CLC podczas uruchamiania, a następnie dexopt.

  2. Napraw moduły, które nie działały przed wyłączeniem globalnego sprawdzania w czasie kompilacji, dodając do ich plików kompilacji niezbędne informacje <uses-library> (szczegóły znajdziesz w sekcji Naprawianie błędów). W przypadku większości modułów wymaga to dodania kilku wierszy w pliku Android.bp lub Android.mk.

  3. W problematycznych przypadkach wyłącz sprawdzanie w czasie kompilacji i dexpreoptowanie na poziomie poszczególnych modułów. Wyłącz dexpreopt, aby nie marnować czasu kompilacji i miejsca na dane na artefakty, które są odrzucane podczas uruchamiania.

  4. Ponownie włącz globalnie sprawdzanie w czasie kompilacji, usuwając ustawienie PRODUCT_BROKEN_VERIFY_USES_LIBRARIES zdefiniowane w kroku 1. Po tej zmianie kompilacja nie powinna się zakończyć niepowodzeniem (z powodu kroków 2 i 3).

  5. Po kolei napraw moduły, które zostały wyłączone w kroku 3, a potem ponownie włącz <uses-library> i moduł dexpreopt. W razie potrzeby zgłaszaj błędy.

W Androidzie 12 są wymuszane kontrole <uses-library> w czasie kompilacji.

Naprawianie uszkodzeń

W sekcjach poniżej dowiesz się, jak naprawić konkretne typy uszkodzeń.

Błąd kompilacji: niezgodność CLC

System kompilacji przeprowadza kontrolę spójności na etapie kompilacji między informacjami w plikach Android.bp lub Android.mk a pliku manifestu. System kompilacji nie może odczytać pliku manifestu, ale może wygenerować reguły kompilacji, aby odczytać manifest (w razie potrzeby wyodrębniając go z pliku APK), i porównać tagi <uses-library> w pliku manifestu z informacjami <uses-library> w plikach kompilacji. Jeśli weryfikacja się nie powiedzie, błąd będzie wyglądać tak:

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

Jak sugeruje komunikat o błędzie, istnieje kilka rozwiązań, w zależności od pilności:

  • Aby wprowadzić tymczasową poprawkę na poziomie wszystkich produktów, ustaw opcję PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true w makefile produktu. Sprawdzanie spójności w czasie kompilacji jest nadal wykonywane, ale jego niepowodzenie nie oznacza niepowodzenia kompilacji. Zamiast tego, gdy sprawdzanie zakończy się niepowodzeniem, system kompilacji obniża poziom filtra kompilatora dex2oat do verify w dexpreopt, co całkowicie wyłącza kompilację AOT w tym module.
  • Aby w szybki, globalny sposób rozwiązać problem za pomocą wiersza poleceń, użyj zmiennej środowiskowej RELAX_USES_LIBRARY_CHECK=true. Ma taki sam efekt jak PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, ale jest przeznaczony do użycia w wierszu poleceń. Zmienne środowiskowe zastępują zmienne usługi.
  • Aby naprawić błąd u źródła, wskaż systemowi kompilacji tagi <uses-library> w pliku manifestu. Z komunikatu o błędzie można się dowiedzieć, które biblioteki powodują problem (podobnie jak w przypadku AndroidManifest.xml lub pliku manifestu w pliku APK, który można sprawdzić za pomocą polecenia `aapt dump badging $APK | grep uses-library`).

W przypadku modułów Android.bp:

  1. Sprawdź brakujące biblioteki w właściwości libs modułu. Jeśli tak, Soong zwykle dodaje takie biblioteki automatycznie, z wyjątkiem tych specjalnych przypadków:

    • Biblioteka nie jest biblioteką pakietu SDK (jest zdefiniowana jako java_library, a nie java_sdk_library).
    • Biblioteka ma inną nazwę biblioteki (w pliku manifestu) niż nazwa modułu (w systemie kompilacji).

    Aby tymczasowo rozwiązać ten problem, dodaj provides_uses_lib: "<library-name>" w definicji biblioteki Android.bp. Długoterminowym rozwiązaniem jest usunięcie podstawowego problemu: przekonwertuj bibliotekę na bibliotekę pakietu SDK lub zmień nazwę jej modułu.

  2. Jeśli poprzedni krok nie rozwiązał problemu, do definicji Android.bp modułu dodaj uses_libs: ["<library-module-name>"] w przypadku wymaganych bibliotek lub optional_uses_libs: ["<library-module-name>"] w przypadku opcjonalnych bibliotek. Te właściwości mogą zawierać listę nazw modułów. Kolejność bibliotek na liście musi być taka sama jak w pliku manifestu.

W przypadku modułów Android.mk:

  1. Sprawdź, czy nazwa biblioteki (w pliku manifestu) różni się od nazwy modułu (w systemie kompilacji). Jeśli tak, tymczasowo rozwiąż ten problem, dodając LOCAL_PROVIDES_USES_LIBRARY := <library-name> do pliku Android.mk w bibliotece lub dodając provides_uses_lib: "<library-name>" do pliku Android.bp w bibliotece (oba przypadki są możliwe, ponieważ moduł Android.mk może zależeć od biblioteki Android.bp). Aby rozwiązać problem na stałe, usuń podstawową przyczynę: zmień nazwę modułu biblioteki.

  2. Dodaj LOCAL_USES_LIBRARIES := <library-module-name> dla wymaganych bibliotek; dodaj LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name> dla opcjonalnych bibliotek do definicji Android.mk modułu. Te właściwości akceptują listę nazw modułów. Kolejność bibliotek na liście musi być taka sama jak w pliku manifestu.

Błąd kompilacji: nieznany ścieżka biblioteki

Jeśli system kompilacji nie może znaleźć ścieżki do pliku <uses-library> DEX jar (ścieżki kompilacji na hoście lub ścieżki instalacji na urządzeniu), kompilacja zwykle się nie powiedzie. Niemożność znalezienia ścieżki może wskazywać, że biblioteka jest skonfigurowana w nieoczekiwanym sposób. Aby tymczasowo rozwiązać problem z kompilacją, wyłącz dexpreopt w przypadku problematycznego modułu.

Android.bp (właściwości modułu):

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

Android.mk (zmienna modułu):

LOCAL_ENFORCE_USES_LIBRARIES := false
LOCAL_DEX_PREOPT := false

Zgłoś błąd, aby zbadać nieobsługiwane scenariusze.

Błąd kompilacji: brak zależności biblioteki

Próba dodania do pliku kompilacji dla Y pakietu <uses-library> X z pliku manifestu modułu Y może spowodować błąd kompilacji z powodu braku zależności X.

Oto przykładowy komunikat o błędzie w przypadku modułów Android.bp:

"Y" depends on undefined module "X"

Oto przykładowy komunikat o błędzie w przypadku modułów 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

Typowym źródłem takich błędów jest sytuacja, gdy biblioteka ma inną nazwę niż odpowiadający jej moduł w systemie kompilacji. Jeśli na przykład wpis <uses-library> w pliku manifestu to com.android.X, ale nazwa modułu biblioteki to tylko X, powoduje to błąd. Aby rozwiązać ten problem, powiedz systemowi kompilacji, że moduł o nazwie X udostępnia <uses-library> o nazwie com.android.X.

Oto przykład bibliotek Android.bp (właściwość modułu):

provides_uses_lib: “com.android.X”,

Oto przykład bibliotek (zmienne modułu) w pliku Android.mk:

LOCAL_PROVIDES_USES_LIBRARY := com.android.X

Niezgodność jednostki sterującej w czasie rozruchu

Podczas pierwszego rozruchu przeszukaj logcat pod kątem komunikatów związanych z niezgodnością CLC, jak pokazano poniżej:

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

Dane wyjściowe mogą zawierać komunikaty w takim formacie:

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

Jeśli pojawi się ostrzeżenie o niezgodności CLC, poszukaj polecenia dexopt dla wadliwego modułu. Aby rozwiązać ten problem, sprawdź, czy moduł przeszedł weryfikację w czasie kompilacji. Jeśli to nie zadziała, być może Twój przypadek jest szczególny i nie jest obsługiwany przez system kompilacji (np. aplikacja, która wczytuje inny plik APK, a nie bibliotekę). System kompilacji nie obsługuje wszystkich przypadków, ponieważ w momencie kompilacji nie można dokładnie wiedzieć, co aplikacja wczytuje w czasie działania.

Kontekst ładowarki klas

CLC to struktura w kształcie drzewa, która opisuje hierarchię ładowarek klas. System kompilacji używa CLC w wąskim znaczeniu (dotyczy tylko bibliotek, a nie plików APK ani ładowarek klas niestandardowych): jest to drzewo bibliotek, które reprezentuje zależność przechodnią wszystkich zależności <uses-library> biblioteki lub aplikacji. Elementy najwyższego poziomu CLC to bezpośrednie zależności <uses-library> określone w pliku manifestu (ścieżka klas). Każdy węzeł drzewa CLC to węzeł <uses-library>, który może mieć własne węzły podrzędne <uses-library>.

Ponieważ zależności <uses-library> to skierowany graf acykliczny, a niekoniecznie drzewo, CLC może zawierać wiele poddrzew w ramach tej samej biblioteki. Inaczej mówiąc, CLC to wykres zależności „rozwinięty” w drzewo. Duplikaty są tylko na poziomie logicznym; rzeczywiste ładowarki klas nie są duplikowane (w czasie wykonywania dla każdej biblioteki jest pojedyncza instancja ładowarki klas).

CLC określa kolejność wyszukiwania bibliotek podczas rozwiązywania klas Java używanych przez bibliotekę lub aplikację. Kolejność wyszukiwania jest ważna, ponieważ biblioteki mogą zawierać zduplikowane klasy, a klasa jest rozwiązywana do pierwszego dopasowania.

Na urządzeniu (czas wykonywania) CLC

PackageManager (w frameworks/base) tworzy CLC, aby załadować moduł Java na urządzeniu. Dodaje biblioteki wymienione w tagach <uses-library> w pliku manifestu modułu jako elementy CLC najwyższego poziomu.

W przypadku każdej użytej biblioteki PackageManager pobiera wszystkie <uses-library>zależności (określone jako tagi w pliku manifestu tej biblioteki) i dodaje do nich zagnieżdżoną CLC. Ten proces jest powtarzany rekurencyjnie, dopóki wszystkie liście węzły skonstruowanego drzewa CLC nie będą bibliotekami bez zależności <uses-library>.

PackageManager zna tylko biblioteki udostępnione. Definicja udostępniania w tym przypadku różni się od zwykłego znaczenia tego słowa (w opozycji do stałego). W Androidzie biblioteki współdzielone Javy to te, które są wymienione w konfiguracjach XML i zainstalowane na urządzeniu (/system/etc/permissions/platform.xml). Każdy wpis zawiera nazwę biblioteki współdzielonej, ścieżkę do jej pliku JAR DEX oraz listę zależności (innych bibliotek współdzielonych, których używa ta biblioteka w czasie wykonywania, i które są określone w tagach <uses-library> w pliku manifestu).

Innymi słowy, istnieją 2 źródła informacji, które umożliwiają PackageManagertworzenie CLC w czasie wykonywania: <uses-library>tagi w pliku manifestu i zależności biblioteki współdzielonej w konfiguracjach XML.

CLC na hoście (w czasie kompilacji)

CLC jest potrzebny nie tylko podczas wczytywania biblioteki lub aplikacji, ale też podczas jej kompilowania. Kompilacja może odbywać się na urządzeniu (dexopt) lub podczas kompilacji (dexpreopt). Ponieważ dexopt działa na urządzeniu, ma te same informacje co PackageManager (pliki manifestu i zależności od bibliotek). Narzędzie dexpreopt działa jednak na hoście i w zupełnie innym środowisku, a dodatkowo musi pobierać te same informacje z systemu kompilacji.

W związku z tym CLC w czasie kompilacji używany przez dexpreopt i CLC w czasie wykonywania używany przez PackageManager to ta sama wartość, ale obliczana na 2 różne sposoby.

CLC w czasie kompilacji i w czasie wykonywania muszą być takie same, w przeciwnym razie kod skompilowany w trybie AOT utworzony przez dexpreopt zostanie odrzucony. Aby sprawdzić, czy CLC w czasie kompilacji i w czasie wykonywania są równe, kompilator dex2oat zapisuje CLC w czasie kompilacji w plikach *.odex (w polu classpath nagłówka pliku OAT). Aby znaleźć zapisany plik CLC, użyj tego polecenia:

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

Niezgodność CLC w czasie kompilacji i w czasie działania jest zgłaszana w logcat podczas uruchamiania. Znajdź je za pomocą tego polecenia:

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

Niezgodność ma negatywny wpływ na wydajność, ponieważ zmusza bibliotekę lub aplikację do dexoptymalizacji albo do działania bez optymalizacji (np. kod aplikacji może wymagać wyodrębnienia w pamięci z pliku APK, co jest bardzo kosztowną operacją).

Biblioteka współużytkowana może być opcjonalna lub wymagana. Z punktu widzenia dexpreopt wymagana biblioteka musi być obecna w momencie kompilacji (jej brak powoduje błąd kompilacji). Opcjonalna biblioteka może być obecna lub nieobecna w momencie kompilacji: jeśli jest obecna, jest dodawana do CLC, przekazywana do dex2oat i rejestrowana w pliku *.odex. Jeśli opcjonalna biblioteka jest nieobecna, jest pomijana i nie jest dodawana do CLC. Jeśli stan biblioteki na etapie kompilacji i w czasie wykonywania się nie zgadza (w jednym przypadku biblioteka opcjonalna jest obecna, a w drugim nie), CLC na etapie kompilacji i w czasie wykonywania się nie zgadzają, a skompilowany kod zostaje odrzucony.

Szczegóły zaawansowanego systemu kompilacji (narzędzia do naprawy pliku manifestu)

Czasami w pliku manifestu źródłowego biblioteki lub aplikacji brakuje znaczników <uses-library>. Może się tak zdarzyć, jeśli jedna z zależności cząstkowych biblioteki lub aplikacji zaczyna używać innego znacznika <uses-library>, a plik manifestu biblioteki lub aplikacji nie jest aktualizowany, aby uwzględnić ten znacznik.

Soong może automatycznie obliczać niektóre brakujące tagi <uses-library> dla danej biblioteki lub aplikacji, ponieważ biblioteki SDK w zamkniętej liście zależności pośrednich biblioteki lub aplikacji. Zamknięta lista jest potrzebna, ponieważ biblioteka (lub aplikacja) może być zależna od biblioteki statycznej, która jest zależna od biblioteki pakietu SDK, a ta z kolei może być zależna pośrednio od innej biblioteki.

Nie wszystkie tagi <uses-library> można obliczyć w ten sposób, ale jeśli to możliwe, lepiej pozwolić Soong na automatyczne dodawanie wpisów w pliku manifestu. Dzięki temu zmniejszysz liczbę błędów i uproszczając konserwację. Jeśli na przykład wiele aplikacji używa statycznej biblioteki, która dodaje nową zależność <uses-library>, wszystkie aplikacje muszą zostać zaktualizowane, co jest trudne do utrzymania.