Dexpreopt i sprawdzanie atrybutu <uses-library>

Android 12 wprowadza zmiany w systemie kompilacji, które dotyczą kompilacji AOT plików DEX (dexpreopt) w przypadku modułów Java z zależnościami <uses-library>. W niektórych przypadkach zmiany w systemie kompilacji mogą powodować błędy kompilacji. Na tej stronie znajdziesz informacje, które pomogą Ci przygotować się na awarie. Skorzystaj z podanych tu wskazówek, aby je naprawić i zminimalizować ich skutki.

Dexpreopt to proces kompilacji z wyprzedzeniem bibliotek i aplikacji Java. Dexpreopt odbywa się na hoście w czasie kompilacji (w przeciwieństwie do dexopt, które odbywa się na urządzeniu). Struktura zależności bibliotek współdzielonych używanych przez moduł Java (bibliotekę lub aplikację) jest znana jako kontekst ładowania klas (CLC). Aby zagwarantować poprawność dexpreopt, CLC w czasie kompilacji i w czasie działania muszą być zgodne. CLC w czasie kompilacji to kontekst, którego kompilator dex2oat używa w czasie wstępnej optymalizacji DEX (jest on zapisywany w plikach ODEX), a CLC w czasie działania to kontekst, w którym wstępnie skompilowany kod jest wczytywany na urządzeniu.

Te CLC w czasie kompilacji i w czasie działania muszą się pokrywać ze względu na poprawność i wydajność. Aby zapewnić poprawność, należy obsługiwać zduplikowane klasy. Jeśli zależności biblioteki współdzielonej w czasie działania różnią się od tych używanych do kompilacji, niektóre klasy mogą być rozwiązywane w inny sposób, co może powodować subtelne błędy w czasie działania. Na wydajność wpływają też sprawdzania w czasie działania pod kątem zduplikowanych klas.

Zastosowania, których dotyczy problem

Pierwsze uruchomienie to główny przypadek użycia, na który wpływają te zmiany: jeśli ART wykryje niezgodność między CLC w czasie kompilacji a CLC w czasie działania, odrzuci artefakty dexpreopt i zamiast nich uruchomi dexopt. W przypadku kolejnych uruchomień nie stanowi to problemu, ponieważ aplikacje można zoptymalizować w tle i zapisać na dysku.

Obszary Androida, których dotyczy problem

Ma to wpływ na wszystkie aplikacje i biblioteki Java, które w czasie działania są zależne od innych bibliotek Java. Android ma tysiące aplikacji, a setki z nich korzystają z bibliotek współdzielonych. Dotyczy to również partnerów, ponieważ mają oni własne biblioteki i aplikacje.

Zmiany powodujące niezgodność

System kompilacji musi znać <uses-library> zależności, zanim wygeneruje reguły kompilacji dexpreopt. 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ść). Ponadto manifest może być spakowany w pliku APK lub w wersji wstępnej. Dlatego w plikach kompilacji (Android.bp lub Android.mk) muszą znajdować się informacje <uses-library>.

Wcześniej ART używał obejścia, które ignorowało zależności biblioteki współdzielonej (znane jako &-classpath). Było to niebezpieczne i powodowało subtelne błędy, więc obejście zostało usunięte w Androidzie 12.

W rezultacie moduły Java, które nie podają prawidłowych informacji w plikach kompilacji, mogą powodować błędy kompilacji (spowodowane niezgodnością CLC w czasie kompilacji) lub regresję czasu pierwszego uruchomienia (spowodowaną niezgodnością CLC w czasie uruchomienia, a następnie optymalizacją dex).<uses-library>

Ścieżka migracji

Aby naprawić uszkodzoną kompilację, wykonaj te czynności:

  1. Globalne wyłączanie sprawdzania w czasie kompilacji w przypadku konkretnego produktu przez ustawienie

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    w pliku makefile produktu. Rozwiązuje to błędy kompilacji (z wyjątkiem specjalnych przypadków wymienionych w sekcji Naprawianie błędów). Jest to jednak tymczasowe obejście problemu, które może powodować niezgodność CLC w czasie rozruchu, a następnie optymalizację DEX.

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

  3. Wyłącz sprawdzanie w czasie kompilacji i dexpreopt w przypadku problematycznych modułów. Wyłącz dexpreopt, aby nie marnować czasu kompilacji i miejsca na dane na artefakty, które zostaną odrzucone podczas uruchamiania.

  4. Ponownie włącz globalnie sprawdzanie w czasie kompilacji, usuwając ustawienie PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, które zostało skonfigurowane w kroku 1. Po tej zmianie kompilacja nie powinna się nie powieść (z powodu kroków 2 i 3).

  5. Napraw moduły wyłączone w kroku 3, po kolei, a następnie ponownie włącz dexpreopt i sprawdzanie <uses-library>. W razie potrzeby zgłaszaj błędy.

Sprawdzanie <uses-library> w czasie kompilacji jest wymuszane w Androidzie 12.

Naprawianie uszkodzeń

W sekcjach poniżej znajdziesz informacje o tym, jak naprawić konkretne typy uszkodzeń.

Błąd kompilacji: niezgodność CLC

System kompilacji przeprowadza w czasie kompilacji kontrolę spójności między informacjami w plikach Android.bp lub Android.mk a manifestem. System kompilacji nie może odczytać pliku manifestu, ale może wygenerować reguły kompilacji, aby go odczytać (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, pojawi się taki błąd:

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:

  • W przypadku tymczasowej poprawki dotyczącej wszystkich produktów ustaw PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true w pliku makefile produktu. Sprawdzanie spójności w czasie kompilacji jest nadal przeprowadzane, ale niepowodzenie weryfikacji nie oznacza niepowodzenia kompilacji. Zamiast tego w przypadku niepowodzenia sprawdzania system kompilacji obniża filtr kompilatora dex2oat do poziomu verify w dexpreopt, co całkowicie wyłącza kompilację AOT w tym module.
  • Aby zastosować szybką, globalną poprawkę w wierszu poleceń, użyj zmiennej środowiskowej RELAX_USES_LIBRARY_CHECK=true. Działa tak samo jak PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, ale jest przeznaczony do używania w wierszu poleceń. Zmienna środowiskowa zastępuje zmienną usługi.
  • Aby trwale rozwiązać ten problem, poinformuj system kompilacji o tagach <uses-library> w pliku manifestu. Analiza komunikatu o błędzie pokazuje, które biblioteki powodują problem (podobnie jak analiza AndroidManifest.xml lub 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. Poszukaj brakującej biblioteki we właściwości libs modułu. Jeśli jest, Soong zwykle dodaje takie biblioteki automatycznie, z wyjątkiem tych szczególnych przypadków:

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

    Aby tymczasowo rozwiązać ten problem, dodaj provides_uses_lib: "<library-name>" do definicji biblioteki Android.bp. Aby znaleźć rozwiązanie długoterminowe, rozwiąż problem u źródła: przekonwertuj bibliotekę na bibliotekę SDK lub zmień nazwę jej modułu.

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

W przypadku modułów Android.mk:

  1. Sprawdź, czy biblioteka ma inną nazwę (w pliku manifestu) niż nazwę modułu (w systemie kompilacji). Jeśli tak jest, tymczasowo rozwiąż ten problem, dodając LOCAL_PROVIDES_USES_LIBRARY := <library-name> w pliku Android.mk biblioteki lub dodając provides_uses_lib: "<library-name>" w pliku Android.bp biblioteki (oba przypadki są możliwe, ponieważ moduł Android.mk może zależeć od biblioteki Android.bp). Aby znaleźć długoterminowe rozwiązanie, rozwiąż problem u źródła: zmień nazwę modułu biblioteki.

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

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

Jeśli system kompilacji nie może znaleźć ścieżki do pliku <uses-library> DEX JAR (ścieżki w czasie kompilacji na hoście lub ścieżki instalacji na urządzeniu), zwykle kończy kompilację niepowodzeniem. Brak ścieżki może oznaczać, że biblioteka jest skonfigurowana w nieoczekiwany sposób. Tymczasowo napraw kompilację, wyłączając dexpreopt dla problematycznego modułu.

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

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

Android.mk (zmienne 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 <uses-library> X z pliku manifestu modułu Y do pliku kompilacji 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

Częstym źródłem takich błędów jest sytuacja, w której biblioteka ma inną nazwę niż odpowiadający jej moduł w systemie kompilacji. Jeśli na przykład wpis w pliku manifestu <uses-library> to com.android.X, ale nazwa modułu biblioteki to tylko X, wystąpi błąd. Aby rozwiązać ten problem, poinformuj system 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”,

To jest przykład bibliotek Android.mk (zmienna modułu):

LOCAL_PROVIDES_USES_LIBRARY := com.android.X

Niezgodność kodu CLC w czasie rozruchu

Przy pierwszym uruchomieniu wyszukaj w logcat wiadomości związane 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 formacie pokazanym poniżej:

[...] 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, upewnij się, że test modułu w czasie kompilacji zakończył się powodzeniem. 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 mieć pewności, co aplikacja wczyta w czasie działania.

Kontekst wczytywania klas

CLC to struktura drzewiasta, która opisuje hierarchię modułów ładujących klasy. System kompilacji używa CLC w wąskim znaczeniu (obejmuje tylko biblioteki, a nie pliki APK ani niestandardowe programy wczytujące klasy): jest to drzewo bibliotek, które reprezentuje domknięcie przechodnie wszystkich zależności <uses-library> biblioteki lub aplikacji. Elementami najwyższego poziomu CLC są bezpośrednie zależności <uses-library> określone w pliku manifestu (ścieżka klasy). Każdy węzeł drzewa CLC jest węzłem <uses-library>, który może mieć własne podwęzły <uses-library>.

Ponieważ zależności <uses-library> są skierowanym grafem acyklicznym, a niekoniecznie drzewem, CLC może zawierać wiele poddrzew dla tej samej biblioteki. Inaczej mówiąc, CLC to wykres zależności „rozwinięty” do postaci drzewa. Duplikacja występuje tylko na poziomie logicznym. Rzeczywiste podstawowe programy ładujące klasy nie są duplikowane (w czasie działania dla każdej biblioteki istnieje jedno wystąpienie programu ładującego klasy).

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 na podstawie pierwszego dopasowania.

CLC na urządzeniu (w czasie działania)

PackageManager (w frameworks/base) tworzy CLC, aby wczytać 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żywanej biblioteki PackageManager pobiera wszystkie jej <uses-library>zależności (określone jako tagi w pliku manifestu tej biblioteki) i dodaje zagnieżdżony CLC dla każdej zależności. Ten proces jest powtarzany rekurencyjnie, aż wszystkie węzły liści skonstruowanego drzewa CLC staną się bibliotekami bez <uses-library>zależności.

PackageManager zna tylko biblioteki udostępnione. Definicja „udostępnionego” w tym kontekście różni się od jego zwykłego znaczenia (np. w odniesieniu do „udostępniony” a „statyczny”). W Androidzie biblioteki współdzielone Javy to te, które są wymienione w konfiguracjach XML zainstalowanych na urządzeniu (/system/etc/permissions/platform.xml). Każdy wpis zawiera nazwę biblioteki współdzielonej, ścieżkę do pliku DEX JAR i listę zależności (inne biblioteki współdzielone, z których ta korzysta w czasie działania i które są określone w tagach <uses-library> w jej pliku manifestu).

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

CLC na hoście (w czasie kompilacji)

CLC jest potrzebny nie tylko podczas ładowania biblioteki lub aplikacji, ale także podczas ich kompilowania. Kompilacja może odbywać się na urządzeniu (dexopt) lub podczas kompilacji (dexpreopt). Proces dexopt odbywa się na urządzeniu, więc ma takie same informacje jak PackageManager (manifesty i zależności bibliotek współdzielonych). Dexpreopt odbywa się jednak na hoście i w zupełnie innym środowisku, a te same informacje musi pobierać z systemu kompilacji.

Dlatego CLC w czasie kompilacji używany przez dexpreopt i CLC w czasie działania używany przez PackageManager to to samo, ale obliczane na 2 różne sposoby.

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

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

Podczas uruchamiania w logcat zgłaszana jest niezgodność CLC w czasie kompilacji i w czasie działania. Wyszukaj go za pomocą tego polecenia:

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

Niezgodność ma negatywny wpływ na wydajność, ponieważ zmusza bibliotekę lub aplikację do przeprowadzenia optymalizacji dexopt lub do działania bez optymalizacji (np. kod aplikacji może wymagać wyodrębnienia z 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 jest błędem kompilacji). Opcjonalna biblioteka może być obecna lub nieobecna w momencie kompilacji: jeśli jest obecna, jest dodawana do CLC, przekazywana do dex2oat i zapisywana w pliku *.odex. Jeśli opcjonalna biblioteka jest nieobecna, jest pomijana i nie jest dodawana do CLC. Jeśli wystąpi niezgodność między stanem w czasie kompilacji a stanem w czasie działania (biblioteka opcjonalna jest obecna w jednym przypadku, ale nie w drugim), kody CLC w czasie kompilacji i w czasie działania nie będą się zgadzać, a skompilowany kod zostanie odrzucony.

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

Czasami w pliku manifestu źródłowego biblioteki lub aplikacji brakuje tagów <uses-library>. Może się tak zdarzyć np. wtedy, gdy jedna z zależności przechodnich biblioteki lub aplikacji zacznie używać innego tagu <uses-library>, a plik manifestu biblioteki lub aplikacji nie zostanie zaktualizowany, aby go uwzględnić.

Soong może automatycznie obliczyć niektóre brakujące tagi <uses-library> dla danej biblioteki lub aplikacji, np. biblioteki SDK w zamknięciu zależności przechodniej biblioteki lub aplikacji. Zamknięcie jest potrzebne, ponieważ biblioteka (lub aplikacja) może zależeć od biblioteki statycznej, która zależy od biblioteki SDK, a także może zależeć przechodnio od innej biblioteki.

Nie wszystkie tagi <uses-library> można obliczyć w ten sposób, ale jeśli to możliwe, lepiej jest pozwolić Soongowi automatycznie dodawać wpisy do pliku manifestu. Jest to mniej podatne na błędy i ułatwia konserwację. Na przykład, gdy wiele aplikacji korzysta ze statycznej biblioteki, która dodaje nowe <uses-library> zależności, wszystkie aplikacje muszą zostać zaktualizowane, co jest trudne do utrzymania.