Dexpreopt i sprawdzanie atrybutu <uses-library>

Android 12 wprowadza zmiany w systemie kompilacji dotyczące kompilacji AOT plików DEX (dexpreopt) w przypadku modułów Java, które mają <uses-library> zależności. W niektórych przypadkach te zmiany w systemie kompilacji mogą powodować błędy kompilacji. Na tej stronie dowiesz się, jak przygotować się na takie sytuacje, a także jak je naprawić i im zapobiegać.

Dexpreopt to proces kompilacji bibliotek i aplikacji Java z wyprzedzeniem. Dexpreopt odbywa się na hoście w czasie kompilacji (w przeciwieństwie do dexopt, który odbywa się na urządzeniu). Struktura zależności bibliotek współużytkowanych używanych przez moduł Java (bibliotekę lub aplikację) jest nazywana kontekstem wczytywania klas (CLC). Aby zagwarantować prawidłowość dexpreopt, CLC w czasie kompilacji i CLC w czasie działania muszą się pokrywać. CLC w czasie kompilacji to kontekst, którego kompilator dex2oat używa w czasie dexpreopt (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 CLC w czasie działania muszą się pokrywać zarówno ze względu na poprawność, jak i wydajność. Aby zapewnić poprawność, konieczne jest obsługiwanie zduplikowanych klas. Jeśli zależności bibliotek współużytkowanych w czasie działania różnią się od tych używanych do kompilacji, niektóre klasy mogą być rozwiązywane inaczej, co może powodować subtelne błędy w czasie działania. Na wydajność wpływają też kontrole zduplikowanych klas w czasie działania.

Zastosowania, których to dotyczy

Głównym zastosowaniem, na które wpływają te zmiany, jest pierwsze uruchomienie urządzenia. Jeśli ART wykryje niezgodność między CLC w czasie kompilacji a CLC w czasie działania, odrzuci artefakty dexpreopt i zamiast tego uruchomi dexopt. W przypadku kolejnych uruchomień nie ma to znaczenia, ponieważ aplikacje można zoptymalizować za pomocą dexopt w tle i zapisać na dysku.

Obszary Androida, których to dotyczy

Dotyczy to wszystkich aplikacji i bibliotek Java, które mają zależności w czasie działania od innych bibliotek Java. Android ma tysiące aplikacji, a setki z nich korzystają z bibliotek współużytkowanych. Dotyczy to też partnerów, ponieważ mają oni własne biblioteki i aplikacje.

Zmiany powodujące niezgodność

System kompilacji musi znać zależności <uses-library>, zanim wygeneruje reguły kompilacji dexpreopt. Nie może jednak bezpośrednio uzyskać dostępu do manifestu i 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 prebuilt. Dlatego <uses-library> informacje muszą być obecne w plikach kompilacji (Android.bp lub Android.mk).

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

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

Ścieżka migracji

Aby naprawić uszkodzoną kompilację:

  1. Wyłącz globalnie sprawdzanie w czasie kompilacji w przypadku konkretnego produktu, ustawiając

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    w pliku makefile produktu. Naprawia to błędy kompilacji (z wyjątkiem szczególnych przypadków, wymienionych w sekcji Naprawianie błędów). Jest to jednak tymczasowe obejście, które może powodować niezgodność CLC w czasie uruchomienia, a następnie dexopt.

  2. Napraw moduły, które nie działały, zanim globalnie wyłączysz sprawdzanie w czasie kompilacji dodając niezbędne informacje<uses-library> do ich plików kompilacji (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. Wyłącz sprawdzanie w czasie kompilacji i dexpreopt w przypadku problematycznych przypadków na poziomie modułu. Wyłącz dexpreopt, aby nie tracić 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 z kroku 1. Po tej zmianie kompilacja nie powinna się nie powieść (ze względu na kroki 2 i 3).

  5. Napraw moduły, które zostały wyłączone w kroku 3, jeden po drugim, a następnie ponownie włącz dexpreopt i sprawdzanie <uses-library>. W razie potrzeby zgłoś błędy.

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

Naprawianie błędów

W kolejnych sekcjach dowiesz się, jak naprawić konkretne typy błędów.

Błąd kompilacji: niezgodność CLC

System kompilacji sprawdza w czasie kompilacji spójność informacji w plikach Android.bp lub Android.mk oraz w manifeście. System kompilacji nie może odczytać manifestu, ale może wygenerować reguły kompilacji, aby go odczytać (w razie potrzeby wyodrębniając go z pliku APK), i porównać <uses-library> tagi w manifeście z informacjami <uses-library> w plikach kompilacji. Jeśli sprawdzanie 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:

  • Aby tymczasowo naprawić problem w całym produkcie, ustaw PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true w pliku makefile produktu. Sprawdzanie spójności w czasie kompilacji jest nadal wykonywane, ale niepowodzenie sprawdzania nie oznacza niepowodzenia kompilacji. Zamiast tego niepowodzenie sprawdzania powoduje, że system kompilacji obniża filtr kompilatora dex2oat do verify w dexpreopt, co całkowicie wyłącza kompilację AOT w przypadku tego modułu.
  • Aby szybko naprawić problem globalnie w wierszu poleceń, użyj zmiennej środowiskowej RELAX_USES_LIBRARY_CHECK=true. Ma ona taki sam efekt jak PRODUCT_BROKEN_VERIFY_USES_LIBRARIES, ale jest przeznaczona do użycia w wierszu poleceń. Zmienna środowiskowa zastępuje zmienną produktu.
  • Aby naprawić błąd u źródła, poinformuj system kompilacji o tagach <uses-library> w manifeście. Sprawdzenie komunikatu o błędzie pokazuje, które biblioteki powodują problem (podobnie jak sprawdzenie 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 tam jest, Soong zwykle dodaje takie biblioteki automatycznie, z wyjątkiem tych szczególnych przypadków:

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

    Aby tymczasowo rozwiązać ten problem, dodaj provides_uses_lib: "<library-name>" w definicji biblioteki Android.bp. Aby znaleźć długoterminowe rozwiązanie, napraw podstawowy problem: 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 bibliotek opcjonalnych do definicji modułu Android.bp. Te właściwości akceptują listę nazw modułów. Kolejność bibliotek na liście musi być taka sama jak w manifeście.

W przypadku modułów Android.mk:

  1. Sprawdź, czy biblioteka ma inną nazwę biblioteki (w manifeście) niż nazwę modułu (w systemie kompilacji). Jeśli tak, 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, napraw podstawowy problem: zmień nazwę modułu biblioteki.

  2. Dodaj LOCAL_USES_LIBRARIES := <library-module-name> w przypadku wymaganych bibliotek lub dodaj LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name> w przypadku bibliotek opcjonalnych do definicji modułu Android.mk. Te właściwości akceptują listę nazw modułów. 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 pliku JAR DEX <uses-library> (ścieżki w czasie kompilacji na hoście lub ścieżki instalacji na urządzeniu), zwykle powoduje to niepowodzenie kompilacji. Niepowodzenie znalezienia ścieżki może wskazywać, że biblioteka jest skonfigurowana w nieoczekiwany sposób. Tymczasowo napraw kompilację, wyłączając dexpreopt w przypadku 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ć wszelkie nieobsługiwane scenariusze.

Błąd kompilacji: brak zależności 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 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, gdy biblioteka ma inną nazwę niż odpowiadający jej moduł w systemie kompilacji. Jeśli na przykład wpis w manifeście <uses-library> 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 <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ść CLC w czasie uruchomienia

Podczas pierwszego uruchomienia 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 formacie pokazanym tutaj:

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

Jeśli otrzymasz ostrzeżenie o niezgodności CLC, poszukaj polecenia dexopt dla wadliwego modułu. Aby rozwiązać ten problem, upewnij się, że sprawdzanie w czasie kompilacji w przypadku modułu zakończyło się powodzeniem. Jeśli to nie zadziała, może to być szczególny przypadek, który 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 czasie kompilacji nie można z całą pewnością wiedzieć, co aplikacja wczytuje w czasie działania.

Kontekst wczytywania klas

CLC to struktura drzewiasta, która opisuje hierarchię wczytywania klas. System kompilacji używa CLC w wąskim znaczeniu (obejmuje tylko biblioteki, a nie pliki APK ani niestandardowe wczytywanie klas): jest to drzewo bibliotek, które reprezentuje domknięcie przechodnie 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żka klasy). Każdy węzeł drzewa CLC to węzeł <uses-library>, który może mieć własne <uses-library> podwęzły.

Ponieważ <uses-library> zależności są skierowanym grafem acyklicznym, a nie koniecznie drzewem, CLC może zawierać wiele poddrzew dla tej samej biblioteki. Innymi słowy, CLC to graf zależności „rozwinięty” do drzewa. Duplikacja występuje tylko na poziomie logicznym. Rzeczywiste moduły wczytujące klasy nie są duplikowane (w czasie działania dla każdej biblioteki jest tylko 1 instancja modułu wczytują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 do 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 manifeście 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 manifeście tej biblioteki) i dodaje zagnieżdżony CLC dla każdej zależności. Proces ten jest kontynuowany rekursywnie, aż wszystkie węzły liści skonstruowanego drzewa CLC będą bibliotekami bez <uses-library> zależności.

PackageManager zna tylko biblioteki współużytkowane. Definicja współużytkowanej w tym przypadku różni się od jej zwykłego znaczenia (jak w przypadku współużytkowanej i statycznej). W Androidzie, biblioteki współużytkowane Java 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ółużytkowanej, ścieżkę do jej pliku JAR DEX i listę zależności (inne biblioteki współużytkowane, których ta biblioteka używa w czasie działania i które określa w <uses-library> tagach w swoim manifeście).

Innymi słowy, istnieją 2 źródła informacji, które umożliwiają PackageManager skonstruowanie CLC w czasie działania: <uses-library> tagi w manifeście i zależności bibliotek współużytkowanych 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 odbywa się na urządzeniu, ma te same informacje co PackageManager (manifesty i zależności bibliotek współużytkowanych). 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 CLC w czasie działania muszą się pokrywać, w przeciwnym razie kod skompilowany przez AOT utworzony przez dexpreopt zostanie odrzucony. Aby sprawdzić równość CLC w czasie kompilacji i CLC w czasie działania, kompilator dex2oat zapisuje CLC w czasie kompilacji w plikach *.odex (w polu classpath nagłówka pliku OAT). Aby znaleźć zapisany CLC, użyj tego polecenia:

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

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

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

Niezgodność wpływa negatywnie na wydajność, ponieważ zmusza bibliotekę lub aplikację do zoptymalizowania za pomocą dexopt albo do działania bez optymalizacji (np. kod aplikacji może wymagać wyodrębnienia z pliku APK w pamięci, 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 czasie kompilacji (jej brak jest błędem kompilacji). Biblioteka opcjonalna może być obecna lub nieobecna w czasie kompilacji. Jeśli jest obecna, jest dodawana do CLC, przekazywana do dex2oat i zapisywana w pliku *.odex. Jeśli biblioteka opcjonalna jest nieobecna, jest pomijana i nie jest dodawana do CLC. Jeśli występuje niezgodność między stanem w czasie kompilacji a stanem w czasie działania (biblioteka opcjonalna jest obecna w jednym przypadku, ale nie w drugim), CLC w czasie kompilacji i CLC w czasie działania nie są zgodne, a skompilowany kod jest odrzucany.

Szczegóły zaawansowanego systemu kompilacji (naprawianie manifestu)

Czasami brakuje tagów <uses-library> w manifeście źródłowym biblioteki lub aplikacji. Może się tak zdarzyć na przykład, 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, aby go uwzględnić.

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

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