Dexpreopt i <uses-library> sprawdzić

W Androidzie 12 wprowadzono zmiany w systemie kompilacji, które dotyczą kompilacji AOT plików DEX (dexpreopt) w przypadku modułów Javy z zależnościami <uses-library>. W niektórych przypadkach takie zmiany w systemie kompilacji mogą spowodować zakłócenie kompilacji. Skorzystaj z tej strony, aby przygotować się na awarie, i postępuj zgodnie z przepisami na tej stronie, aby je naprawić i wyeliminować ewentualne awarie.

Dexpreopt to proces wstępnej kompilacji bibliotek i aplikacji Java. Dexpreopt odbywa się na hoście podczas kompilacji (w przeciwieństwie do metody dexopt, która odbywa się 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 zapewnić 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 zapisywana w plikach ODEX), a CLC w czasie wykonywania to kontekst, w którym skompilowany kod jest ładowany na urządzeniu.

Te interfejsy CLC w czasie kompilacji i uruchamianiu muszą się pokrywać ze względu zarówno na poprawność, jak i wydajność. Aby zapewnić poprawność, należy obsłużyć zduplikowane klasy. Jeśli zależności bibliotek udostępnionych w czasie działania różnią się od tych używanych do kompilacji, niektóre klasy mogą zostać rozwiązane inaczej, co spowoduje drobne błędy środowiska wykonawczego. Na wydajność wpływają też sprawdzanie w czasie działania duplikatów klas.

Przypadki użycia, których dotyczy problem

Pierwszy rozruch to główny przypadek użycia, którego dotyczą te zmiany. Jeśli ART wykryje niezgodność między interfejsami CLC w czasie kompilacji i w czasie działania, odrzuca artefakty dexpreopt i uruchamia dexopt. Nie stanowi to żadnego problemu w przypadku kolejnych rozruchów, bo aplikacje można pobierać w tle i zapisywać 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 Androidzie są tysiące aplikacji, z których setki korzystają z bibliotek współdzielonych. Dotyczy to także partnerów, którzy mają własne biblioteki i aplikacje.

Przerwanie zmian

System kompilacji musi znać zależności <uses-library>, 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ść). Poza tym plik manifestu może być spakowany w pliku APK lub gotowym pliku. Dlatego w plikach kompilacji (Android.bp lub Android.mk) muszą znajdować się informacje <uses-library>.

Wcześniej usługa ART używała obejścia, które ignorowało zależności bibliotek udostępnionych (znane jako &-classpath). Było to niebezpieczne i spowodowało drobne błędy, dlatego to obejście zostało usunięte w Androidzie 12.

W związku z tym moduły Javy, które nie zawierają prawidłowych informacji o <uses-library> w plikach kompilacji, mogą powodować awarie kompilacji (powodowane przez niezgodność CLC podczas kompilacji) lub regresje podczas pierwszego uruchomienia (powodowane przez niezgodność CLC podczas rozruchu, po której następuje dexopt).

Ścieżka migracji

Aby naprawić niedziałającą kompilację, wykonaj te czynności:

  1. Globalnie wyłącz kontrolę w czasie kompilacji konkretnej usługi przez ustawienie

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    w pliku z marką produktu. To rozwiązuje błędy kompilacji (z wyjątkiem przypadków specjalnych wymienionych w sekcji Usuwanie usterek). Jest to jednak tymczasowe obejście problemu, które może spowodować niezgodność CLC w czasie uruchamiania, a następnie dexopt.

  2. Napraw moduły, w przypadku których nie udało się globalnie wyłączyć kontroli w czasie kompilacji, dodając niezbędne informacje <uses-library> do plików kompilacji (szczegóły znajdziesz w artykule Usuwanie usterek). 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. Włącz globalnie ponownie kontrolę w czasie kompilacji, odznaczając ustawienie PRODUCT_BROKEN_VERIFY_USES_LIBRARIES ustawione w kroku 1. Kompilacja nie powinna kończyć się niepowodzeniem po tej zmianie (w krokach 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łoś błędy.

Kontrole <uses-library> podczas kompilacji są egzekwowane w Androidzie 12.

Naprawianie awarii

W sekcjach poniżej dowiesz się, jak naprawić określone typy awarii.

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 generować reguły kompilacji, które odczytują plik manifestu (w razie potrzeby wyodrębniają go z pliku APK) i porównują tagi <uses-library> 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 wartość PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true w pliku make produktu. Nadal wykonywany jest test spójności w czasie kompilacji, ale jego niepowodzenie nie oznacza niepowodzenia kompilacji. Zamiast tego w przypadku błędu sprawdzania system kompilacji zmniejsza w narzędziu dexpreopt filtr kompilatora dex2oat na verify, co całkowicie wyłącza kompilację AOT dla tego modułu.
  • Aby szybko wprowadzić globalną poprawkę w wierszu poleceń, użyj zmiennej środowiskowej RELAX_USES_LIBRARY_CHECK=true. Ma taki sam efekt jak PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, ale jest przeznaczona do użycia w wierszu poleceń. Zmienne środowiskowe zastępują zmienne produktu.
  • Aby rozwiązać główną przyczynę problemu, powiadom system kompilacji o tagach <uses-library> w pliku manifestu. Sprawdzenie komunikatu o błędzie pokazuje, które biblioteki powodują problem (podobnie jak sprawdzanie AndroidManifest.xml czy pliku manifestu w pliku APK, który można sprawdzić za pomocą „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 takie biblioteki są dostępne, Soong zwykle dodaje je automatycznie, z wyjątkiem tych szczególnych przypadków:

    • Nie jest ona 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 rozwiązanie problemu podstawowego: przekonwertuj bibliotekę na bibliotekę pakietu SDK lub zmień nazwę jej modułu.

  2. Jeśli w poprzednim kroku nie udało się znaleźć rozwiązania, dodaj uses_libs: ["<library-module-name>"] w przypadku wymaganych bibliotek lub optional_uses_libs: ["<library-module-name>"] w przypadku bibliotek opcjonalnych do definicji Android.bp modułu. Te właściwości mogą zawierać 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 nazwa biblioteki (w pliku manifestu) różni się od nazwy modułu (w systemie kompilacji). Jeśli tak, rozwiąż ten problem tymczasowo, dodając LOCAL_PROVIDES_USES_LIBRARY := <library-name> w pliku Android.mk biblioteki, lub dodaj provides_uses_lib: "<library-name>" w pliku Android.bp biblioteki (w obu przypadkach jest to możliwe, ponieważ moduł Android.mk może zależeć od biblioteki Android.bp). Aby rozwiązać długoterminowe rozwiązanie, rozwiąż podstawowy problem: zmień nazwę modułu biblioteki.

  2. Dodaj atrybut LOCAL_USES_LIBRARIES := <library-module-name> w przypadku bibliotek wymaganych; dodaj LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name> dla bibliotek opcjonalnych do definicji Android.mk modułu. Akceptujemy tylko nazwy 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 kompilacji na hoście lub ścieżki instalacji na urządzeniu), kompilacja zwykle się nie powiedzie. Brak ścieżki może oznaczać, że biblioteka jest skonfigurowana w nieoczekiwany sposób. Tymczasowo napraw kompilację, wyłączając plik 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 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 dotyczący 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 nazwa biblioteki innej niż odpowiadająca 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, poinformuj system kompilacji, że moduł o nazwie X udostępnia zasób <uses-library> o nazwie com.android.X.

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

provides_uses_lib: “com.android.X”,

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

LOCAL_PROVIDES_USES_LIBRARY := com.android.X

Niezgodność jednostki sterującej w czasie rozruchu

Podczas pierwszego rozruchu wyszukiwaj w logcat wiadomości 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, upewnij się, że kontrola czasu kompilacji modułu zakończyła 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 z pewnością stwierdzić, co aplikacja wczytuje w czasie działania.

Kontekst modułu wczytywania klas

CLC to struktura w kształcie drzewa, która opisuje hierarchię modułu ładowania klas. System kompilacji używa CLC w wąskim sensie (obejmuje tylko biblioteki, a nie pliki APK czy moduły ładowania klasy niestandardowej): jest to drzewo bibliotek, które reprezentuje przechodnie zamykanie 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żce klasy). Każdy węzeł drzewa CLC jest węzłem <uses-library>, który może mieć własne węzły podrzędne <uses-library>.

Zależności <uses-library> są skierowanych grafem acyklicznym, a nie drzewem, więc CLC może zawierać wiele poddrzew dla tej samej biblioteki. Inaczej mówiąc, CLC jest grafem zależności „rozwiniętym” na drzewo. Podwójne ładowanie występuje tylko na poziomie logicznym. Rzeczywiste ładowarki klas nie są dublowane (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.

CLC na urządzeniu (czas wykonywania)

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

Dla każdej używanej biblioteki PackageManager pobiera wszystkie zależności <uses-library> (określone jako tagi w pliku manifestu tej biblioteki) i dodaje zagnieżdżony interfejs CLC dla każdej zależności. Ten proces będzie kontynuowany, dopóki wszystkie węzły liści utworzonego 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 jej zwykłego znaczenia (np. w przypadku udostępniania i plików statycznych). 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 ona 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 bibliotek współdzielonych 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). Dexopt odbywa się na urządzeniu, więc zawiera te same informacje co PackageManager (pliki manifestu i zależności zasobów wspólnych). 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 interfejs CLC w czasie kompilacji używane przez dexpreopt i środowisko CLC działające w czasie działania używane przez PackageManager są takie same, ale obliczane na 2 różne sposoby.

CLC w czasie kompilacji i w czasie wykonywania muszą być takie same, w przeciwnym razie kod kompilowany 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 = '

W logcat podczas uruchamiania zgłaszana jest niezgodność CLC w czasie kompilacji i w czasie działania. Wyszukaj ją 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ć dostępna w czasie kompilacji (jej brak oznacza 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 biblioteki wspólnej. 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 tagów <uses-library>. Może się tak zdarzyć, np. gdy jedna z przechodnich zależności biblioteki lub aplikacji zaczyna korzystać z innego tagu <uses-library>, a biblioteka lub plik manifestu aplikacji nie jest aktualizowany.

Soong może automatycznie obliczać niektóre brakujące tagi <uses-library> w przypadku danej biblioteki lub aplikacji, ponieważ są to biblioteki SDK w zamknięciu przechodnie zależności biblioteki lub aplikacji. Zamknięcie biblioteki (lub aplikacji) jest konieczne, ponieważ biblioteka (lub aplikacja) może korzystać z biblioteki statycznej, która jest zależna od biblioteki SDK, i ewentualnie w zależności 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 korzysta z statycznej biblioteki, która dodaje nową zależność <uses-library>, wszystkie aplikacje muszą zostać zaktualizowane, co jest trudne do utrzymania.