Dexpreopt i <uses-library> czeki

W systemie Android 12 wprowadzono zmiany w systemie kompilacji w zakresie kompilacji AOT plików DEX (dexpreopt) dla modułów Java, które mają zależności <uses-library> . W niektórych przypadkach te zmiany w systemie kompilacji mogą spowodować uszkodzenie kompilacji. Użyj tej strony, aby przygotować się na awarie i postępuj zgodnie z zawartymi na niej przepisami, aby je naprawić i złagodzić.

Dexpreopt to proces kompilacji bibliotek i aplikacji Java z wyprzedzeniem. Dexpreopt dzieje się na hoście w czasie kompilacji (w przeciwieństwie do dexopt , który dzieje się na urządzeniu). Struktura zależności bibliotek współdzielonych używana przez moduł Java (bibliotekę lub aplikację) jest nazywana kontekstem modułu ładującego klasy (CLC). Aby zagwarantować poprawność dexpreopt, wartości CLC czasu kompilacji i czasu wykonania muszą się pokrywać. CLC w czasie kompilacji to coś, czego kompilator dex2oat używa w czasie dexpreopt (jest to zapisane w plikach ODEX), a CLC w czasie wykonywania to kontekst, w którym prekompilowany kod jest ładowany na urządzenie.

Te CLC czasu kompilacji i wykonania muszą się pokrywać ze względu zarówno na poprawność, jak i wydajność. Dla poprawności konieczna jest obsługa zduplikowanych klas. Jeśli zależności bibliotek współdzielonych w czasie wykonywania różnią się od tych używanych do kompilacji, niektóre klasy mogą zostać rozwiązane inaczej, powodując subtelne błędy w czasie wykonywania. Na wydajność wpływają także kontrole środowiska wykonawczego pod kątem zduplikowanych klas.

Dotknięte przypadki użycia

Pierwsze uruchomienie jest głównym przypadkiem użycia, na który wpływają te zmiany: jeśli ART wykryje niezgodność między CLC w czasie kompilacji i w czasie wykonywania, odrzuca artefakty dexpreopt i zamiast tego uruchamia dexopt. W przypadku kolejnych rozruchów jest to w porządku, ponieważ aplikacje można deksoptować w tle i przechowywać na dysku.

Dotknięte obszary Androida

Dotyczy to wszystkich aplikacji i bibliotek Java, których środowisko wykonawcze jest zależne od innych bibliotek Java. Na Androida działają 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.

Przerwij zmiany

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 manifestu i odczytać zawartych w nim znaczników <uses-library> , ponieważ system kompilacji nie może czytać dowolnych plików podczas generowania reguł kompilacji (ze względu na wydajność). Co więcej, manifest może być spakowany w pliku APK lub w postaci gotowej. Dlatego w plikach kompilacji ( Android.bp lub Android.mk ) musi znajdować się informacja <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 subtelne błędy, dlatego obejście zostało usunięte w Androidzie 12.

W rezultacie moduły Java, które nie dostarczają poprawnych informacji <uses-library> w swoich plikach kompilacji, mogą powodować awarie kompilacji (spowodowane niedopasowaniem CLC w czasie kompilacji) lub regresję czasu przy pierwszym uruchomieniu (spowodowane przez CLC w czasie rozruchu mismatch, po którym następuje dexopt).

Ścieżka migracji

Wykonaj poniższe kroki, aby naprawić uszkodzoną kompilację:

  1. Globalnie wyłącz sprawdzanie czasu kompilacji dla konkretnego produktu, ustawiając

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    w pliku makefile produktu. Naprawia to błędy kompilacji (z wyjątkiem specjalnych przypadków wymienionych w sekcji Naprawianie usterek ). Jest to jednak rozwiązanie tymczasowe i może powodować niezgodność CLC podczas rozruchu, a następnie dexopt.

  2. Napraw moduły, które uległy awarii, zanim globalnie wyłączyłeś sprawdzanie czasu kompilacji, dodając niezbędne informacje <uses-library> do ich plików kompilacji (więcej szczegółów znajdziesz w Naprawianie usterek ). W przypadku większości modułów wymaga to dodania kilku linii w Android.bp lub Android.mk .

  3. Wyłącz sprawdzanie w czasie kompilacji i wybierz opcję dexpreopt dla problematycznych przypadków dla poszczególnych modułów. Wyłącz dexpreopt, aby nie marnować czasu kompilacji i pamięci na artefakty, które są odrzucane podczas uruchamiania.

  4. Globalnie włącz ponownie sprawdzanie czasu kompilacji, usuwając ustawienie PRODUCT_BROKEN_VERIFY_USES_LIBRARIES ustawione w kroku 1; kompilacja nie powinna zakończyć się niepowodzeniem po tej zmianie (z powodu kroków 2 i 3).

  5. Napraw moduły wyłączone w kroku 3, jeden po drugim, a następnie włącz ponownie dexpreopt i zaznaczenie <uses-library> . Jeśli to konieczne, zgłoś błędy.

W systemie Android 12 egzekwowane są kontrole <uses-library> w czasie kompilacji.

Napraw uszkodzenia

W poniższych sekcjach opisano, jak naprawić określone typy uszkodzeń.

Błąd kompilacji: niedopasowanie CLC

System kompilacji sprawdza spójność w czasie kompilacji między informacjami zawartymi w plikach Android.bp lub Android.mk a manifestem. System kompilacji nie może odczytać manifestu, ale może wygenerować reguły kompilacji w celu odczytania manifestu (w razie potrzeby wyodrębnienia go z pliku APK) i porównania tagów <uses-library> w manifeście z informacjami <uses-library> w pliki kompilacji. Jeśli sprawdzenie się nie powiedzie, błąd wygląda następująco:

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 wiele rozwiązań, w zależności od pilności:

  • Aby uzyskać tymczasową poprawkę obejmującą cały produkt , ustaw PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true w pliku makefile produktu. Kontrola spójności w czasie kompilacji jest nadal wykonywana, ale niepowodzenie kontroli nie oznacza niepowodzenia kompilacji. Zamiast tego niepowodzenie sprawdzania powoduje, że system kompilacji obniża wersję filtra kompilatora dex2oat w celu verify w dexpreopt, co całkowicie wyłącza kompilację AOT dla tego modułu.
  • Aby uzyskać szybką, globalną poprawkę z 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ń. Zmienna środowiskowa zastępuje zmienną produktu.
  • Aby rozwiązać przyczynę problemu, powiadom system kompilacji o znacznikach <uses-library> w manifeście. Kontrola komunikatu o błędzie pokazuje, które biblioteki powodują problem (podobnie jak sprawdzanie AndroidManifest.xml lub manifestu wewnątrz pliku APK, który można sprawdzić za pomocą ` aapt dump badging $APK | grep uses-library `).

Dla modułów Android.bp :

  1. Poszukaj brakującej biblioteki we właściwości libs modułu. Jeśli tam jest, Soong zwykle dodaje takie biblioteki automatycznie, z wyjątkiem tych specjalnych przypadków:

    • Biblioteka nie jest biblioteką SDK (jest zdefiniowana jako java_library zamiast java_sdk_library ).
    • Biblioteka ma inną nazwę (w manifeście) niż nazwa modułu (w systemie kompilacji).

    Aby tymczasowo rozwiązać ten problem, dodaj provides_uses_lib: "<library-name>" w definicji biblioteki Android.bp . Aby uzyskać rozwiązanie długoterminowe, napraw podstawowy problem: przekonwertuj bibliotekę na bibliotekę SDK lub zmień nazwę jej modułu.

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

Dla modułów Android.mk :

  1. Sprawdź, czy biblioteka ma inną nazwę biblioteki (w manifeście) niż nazwa modułu (w systemie kompilacji). Jeśli tak, napraw to 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 są możliwe, ponieważ moduł Android.mk może zależeć od biblioteki Android.bp ). Aby uzyskać rozwiązanie długoterminowe, napraw podstawowy problem: zmień nazwę modułu bibliotecznego.

  2. Dodaj LOCAL_USES_LIBRARIES := <library-module-name> dla wymaganych bibliotek; dodaj LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name> dla 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 manifeście.

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

Jeśli system kompilacji nie może znaleźć ścieżki do słoika DEX <uses-library> (ani ścieżki kompilacji na hoście, ani ścieżki instalacji na urządzeniu), kompilacja zwykle kończy się niepowodzeniem. Niepowodzenie znalezienia ścieżki może wskazywać, ż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 sprawdzić nieobsługiwane scenariusze.

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

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

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

"Y" depends on undefined module "X"

Oto przykładowy komunikat o błędzie dla 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 inna niż nazwa odpowiadającego jej modułu w systemie kompilacji. Na przykład, jeśli wpis manifestu <uses-library> to com.android.X , ale nazwa modułu bibliotecznego to po prostu 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 Android.mk (zmienna modułu):

LOCAL_PROVIDES_USES_LIBRARY := com.android.X

Niezgodność czasu rozruchu CLC

Przy pierwszym uruchomieniu wyszukaj w logcat komunikaty 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 postaci pokazanej tutaj:

[...] 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 to naprawić, upewnij się, że kontrola czasu kompilacji modułu przebiegła pomyślnie. Jeśli to nie zadziała, może to być szczególny przypadek, który nie jest obsługiwany przez system kompilacji (taki jak aplikacja ładująca inny plik APK, a nie bibliotekę). System kompilacji nie obsługuje wszystkich przypadków, ponieważ w czasie kompilacji nie można mieć pewności, co aplikacja ładuje w czasie wykonywania.

Kontekst modułu ładującego klasy

CLC to struktura przypominająca drzewo, która opisuje hierarchię modułu ładującego klasy. System kompilacji używa CLC w wąskim znaczeniu (obejmuje tylko biblioteki, a nie pliki APK lub programy ładujące klasy niestandardowe): jest to drzewo bibliotek reprezentujące przechodnie zamknięcie 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 manifeście (ścieżce klas). 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ą ukierunkowanym grafem acyklicznym, a niekoniecznie drzewem, CLC może zawierać wiele poddrzew dla tej samej biblioteki. Innymi słowy, CLC jest wykresem zależności „rozwiniętym” w drzewo. Powielanie zachodzi tylko na poziomie logicznym; rzeczywiste podstawowe programy ładujące klasy nie są duplikowane (w czasie wykonywania istnieje jedna instancja modułu ładującego klasy dla każdej biblioteki).

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

Na urządzeniu (w czasie wykonywania) CLC

PackageManager (w frameworks/base ) tworzy CLC do ładowania modułu Java na urządzeniu. Dodaje biblioteki wymienione w tagach <uses-library> w manifeście 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 znaczniki w manifeście tej biblioteki) i dodaje zagnieżdżone CLC dla każdej zależności. Proces ten jest kontynuowany rekurencyjnie, aż wszystkie węzły-liście skonstruowanego drzewa CLC będą bibliotekami bez zależności <uses-library> .

PackageManager zna tylko biblioteki współdzielone. Definicja współdzielonego w tym użyciu różni się od jego zwykłego znaczenia (jak w przypadku współdzielonego i statycznego). W systemie Android udostępnione biblioteki Java to te wymienione w konfiguracjach XML, które są zainstalowane na urządzeniu ( /system/etc/permissions/platform.xml ). Każdy wpis zawiera nazwę biblioteki współdzielonej, ścieżkę do jej pliku jar DEX i listę zależności (inne biblioteki współdzielone, których ta biblioteka używa w czasie wykonywania i które określa w znacznikach <uses-library> w swoim manifeście).

Innymi słowy, istnieją dwa źródła informacji, które umożliwiają PackageManager konstruowanie CLC w czasie wykonywania: znaczniki <uses-library> w manifeście i zależności bibliotek współdzielonych w konfiguracjach XML.

CLC na hoście (w czasie kompilacji).

CLC jest potrzebne nie tylko podczas ładowania biblioteki lub aplikacji, ale także podczas jej kompilacji. Kompilacja może odbywać się na urządzeniu (dexopt) lub podczas kompilacji (dexpreopt). Ponieważ dexopt odbywa się na urządzeniu, zawiera te same informacje co PackageManager (manifesty i zależności bibliotek współdzielonych). Jednakże Dexpreopt odbywa się na hoście, w zupełnie innym środowisku i musi uzyskać te same informacje z systemu kompilacji.

Zatem CLC w czasie kompilacji używane przez dexpreopt i CLC w czasie wykonywania używane przez PackageManager to to samo, ale obliczane na dwa różne sposoby.

Wartości CLC czasu kompilacji i wykonania muszą się pokrywać, w przeciwnym razie kod skompilowany za pomocą narzędzia AOT utworzony przez dexpreopt zostanie odrzucony. Aby sprawdzić równość CLC czasu kompilacji i wykonania, kompilator dex2oat rejestruje CLC czasu kompilacji w plikach *.odex (w polu classpath w nagłówku pliku OAT). Aby znaleźć zapisaną CLC, użyj tego polecenia:

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

Niezgodność czasu kompilacji i czasu wykonywania CLC jest zgłaszana w logcat podczas rozruchu. Wyszukaj go za pomocą tego polecenia:

logcat | grep -E 'ClassLoaderContext [az ]+ mismatch'

Niedopasowanie ma niekorzystny wpływ na wydajność, ponieważ wymusza dekodowanie biblioteki lub aplikacji lub działanie bez optymalizacji (na przykład kod aplikacji może wymagać wyodrębnienia z pamięci pliku APK do pamięci, co jest bardzo kosztowną operacją).

Biblioteka współdzielona może być opcjonalna lub wymagana. Z punktu widzenia dexpreopt wymagana biblioteka musi być obecna w czasie kompilacji (jej brak jest błędem kompilacji). Opcjonalna biblioteka może być obecna lub nieobecna w czasie kompilacji: jeśli jest, jest dodawana do CLC, przekazywana do dex2oat i zapisywana w pliku *.odex . Jeśli nie ma opcjonalnej biblioteki, jest ona pomijana i nie dodawana do CLC. Jeśli występuje rozbieżność między statusem czasu kompilacji i wykonania (opcjonalna biblioteka jest obecna w jednym przypadku, ale nie w drugim), wówczas CLC czasu kompilacji i czasu wykonania nie są zgodne, a skompilowany kod zostaje odrzucony.

Szczegóły zaawansowanego systemu kompilacji (naprawa manifestu)

Czasami w manifeście źródłowym biblioteki lub aplikacji brakuje tagów <uses-library> . Może się to na przykład zdarzyć, jeśli jedna z zależności przechodnich biblioteki lub aplikacji zacznie używać innego tagu <uses-library> , a manifest biblioteki lub aplikacji nie zostanie zaktualizowany w celu uwzględnienia go.

Soong może automatycznie obliczyć część brakujących tagów <uses-library> dla danej biblioteki lub aplikacji, tak jak biblioteki SDK w przypadku przechodniego zamykania zależności biblioteki lub aplikacji. Zamknięcie jest konieczne, ponieważ biblioteka (lub aplikacja) może zależeć od biblioteki statycznej, która zależy od biblioteki SDK i prawdopodobnie może ponownie zależeć przechodnio od innej biblioteki.

Nie wszystkie znaczniki <uses-library> można obliczyć w ten sposób, ale jeśli to możliwe, lepiej pozwolić Soongowi automatycznie dodawać wpisy manifestu; jest mniej podatny na błędy i upraszcza konserwację. Na przykład, gdy wiele aplikacji korzysta z biblioteki statycznej, która dodaje nową zależność <uses-library> , wszystkie aplikacje muszą zostać zaktualizowane, co jest trudne w utrzymaniu.