Korzystanie z nowszych interfejsów API

Z tego artykułu dowiesz się, jak aplikacja może korzystać z nowych funkcji systemu operacyjnego na nowym urządzeniu wersje systemu operacyjnego przy zachowaniu zgodności ze starszymi urządzeniami.

Domyślnie odniesienia do interfejsów API NDK w aplikacji są silnymi odniesieniami. Dynamiczne ładowanie Androida szybko je rozwiąże, gdy biblioteka wczytano. Jeśli symbole nie zostaną znalezione, aplikacja przerwie działanie. Jest to sprzeczne z jak działa Java, gdzie wyjątek nie zostanie zgłoszony, dopóki brakujący interfejs API nie zostanie .

Z tego powodu NDK uniemożliwi tworzenie silnych odniesień do Interfejsy API nowsze niż minSdkVersion Twojej aplikacji. Chroni to przed przypadkowy kod dostawy, który zadziałał podczas testowania, ale nie zostanie wczytany. (UnsatisfiedLinkError zostanie zgłoszone z System.loadLibrary()) w przypadku starszych urządzenia. Z kolei napisanie kodu korzystającego z interfejsów API jest trudniejsze. jest nowsza niż minSdkVersion Twojej aplikacji, ponieważ musisz wywoływać interfejsy API za pomocą dlopen() i dlsym() zamiast normalnego wywołania funkcji.

Alternatywą dla silnych odwołań są używanie słabych plików referencyjnych. Słaby który nie zostanie znaleziony, gdy biblioteka wczyta wyniki w adresie jest ustawiony na nullptr, a nie może się załadować. Nadal nie można bezpiecznie wywołać, ale można zabezpieczyć się przed nawiązaniem połączenia z lokalizacjami, gdy interfejs API nie jest dostępny, można uruchomić resztę kodu wywoływać ten interfejs API w normalny sposób bez konieczności używania elementów dlopen() i dlsym().

Słabe odniesienia do interfejsu API nie wymagają dodatkowej obsługi dynamicznego tagu łączącego, działają z każdą wersją Androida.

Włączanie w kompilacji słabych odwołań do interfejsu API

CMake

Przekazuj -DANDROID_WEAK_API_DEFS=ON podczas uruchamiania CMake. Jeśli używasz CMake przez externalNativeBuild, dodaj do build.gradle.kts (lub świetny odpowiednik, jeśli nadal używasz build.gradle):

android {
    // Other config...

    defaultConfig {
        // Other config...

        externalNativeBuild {
            cmake {
                arguments.add("-DANDROID_WEAK_API_DEFS=ON")
                // Other config...
            }
        }
    }
}

ndk-build

Dodaj do pliku Application.mk te informacje:

APP_WEAK_API_DEFS := true

Jeśli nie masz jeszcze pliku Application.mk, utwórz go w tym samym miejscu jako plik Android.mk. Dodatkowe zmiany na Twoim koncie Plik build.gradle.kts (lub build.gradle) nie jest wymagany do polecenia ndk-build.

Inne systemy kompilacji

Jeśli nie używasz CMake ani ndk-build, zapoznaj się z dokumentacją kompilacji aby sprawdzić, czy istnieje zalecany sposób jej włączenia. Jeśli Twoja kompilacja System nie obsługuje tej opcji natywnie, możesz ją włączyć, przekazując te flagi podczas kompilacji:

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

Pierwsza konfiguruje nagłówki NDK tak, aby zezwalały na słabe odwołania. Drugie etapy ostrzeżenie o niebezpiecznych wywołaniach interfejsu API w komunikat o błędzie.

Więcej informacji znajdziesz w przewodniku dla właścicieli systemów tworzenia systemów.

Chronione wywołania interfejsu API

Ta funkcja nie zapewnia magicznego bezpieczeństwa wywoływania nowych interfejsów API. Jedyne, powoduje odroczenie błędu czasu wczytywania do błędu związanego z czasem połączenia. Zaletą jest to, może je zabezpieczyć w czasie działania i praktycznie się wycofać, alternatywną implementację lub powiadamiać użytkownika, że taka funkcja niedostępny na ich urządzeniach lub całkowicie unikając tej ścieżki kodu.

Przy blokadzie może pojawić się ostrzeżenie (unguarded-availability). do interfejsu API, który jest niedostępny dla: minSdkVersion Twojej aplikacji. Jeśli przy użyciu narzędzia ndk-build lub pliku narzędzia CMake, to ostrzeżenie włączona i przełączona na błąd podczas włączania tej funkcji.

Oto przykład kodu, który warunkowo korzysta z interfejsu API bez ta funkcja została włączona przy użyciu interfejsów dlopen() i dlsym():

void LogImageDecoderResult(int result) {
    void* lib = dlopen("libjnigraphics.so", RTLD_LOCAL);
    CHECK_NE(lib, nullptr) << "Failed to open libjnigraphics.so: " << dlerror();
    auto func = reinterpret_cast<decltype(&AImageDecoder_resultToString)>(
        dlsym(lib, "AImageDecoder_resultToString")
    );
    if (func == nullptr) {
        LOG(INFO) << "cannot stringify result: " << result;
    } else {
        LOG(INFO) << func(result);
    }
}

Trudno się to czyta. Nazwy funkcji są zduplikowane (i jeśli C i podpisy), komponuje się użyj kreacji zastępczej w czasie działania, jeśli przypadkowo popełnisz literówkę w przesłanej nazwie funkcji do dlsym i trzeba użyć tego wzorca dla każdego interfejsu API.

W przypadku słabych odwołań do interfejsu API funkcję powyższą można zapisać tak:

void LogImageDecoderResult(int result) {
    if (__builtin_available(android 31, *)) {
        LOG(INFO) << AImageDecoder_resultToString(result);
    } else {
        LOG(INFO) << "cannot stringify result: " << result;
    }
}

__builtin_available(android 31, *) – połączenia telefoniczne android_get_device_api_level(), zapisuje wynik w pamięci podręcznej i porównuje go z wartością 31 (jest to poziom API, który umożliwił korzystanie z AImageDecoder_resultToString()).

Najprostszym sposobem określenia, której wartości użyć dla funkcji __builtin_available, jest budować bez zabezpieczenia (lub __builtin_available(android 1, *)) i postępuj zgodnie z komunikatem o błędzie. Na przykład niechronione połączenie z numerem AImageDecoder_createFromAAsset() z minSdkVersion 24 wygeneruje:

error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]

W tym przypadku połączenie powinno być chronione przez __builtin_available(android 30, *). Jeśli nie wystąpi błąd kompilacji, oznacza to, że interfejs API jest zawsze dostępny dla minSdkVersion i ochrona nie jest potrzebna lub kompilacja jest nieprawidłowo skonfigurowana i Ostrzeżenie: unguarded-availability jest wyłączone.

Odwołanie do interfejsu API NDK będzie też zawierać tekst podobny do „Wprowadzono do interfejsu API 30” dla każdego interfejsu API. Brak takiego tekstu oznacza, że interfejs API jest dostępny dla wszystkich obsługiwanych poziomów API.

Unikanie powtarzania zabezpieczeń interfejsu API

Jeśli go używasz, prawdopodobnie masz w aplikacji sekcje kodu, Można ich używać tylko na nowych urządzeniach. Zamiast powtarzać __builtin_available() sprawdź każdą funkcję, dodaj adnotacje jako wymagające określonego poziomu interfejsu API. Na przykład interfejsy ImageDecoder API zostały dodane w interfejsie API 30. W przypadku funkcji, które intensywnie korzystają z tych Interfejsy API możesz wykonać na przykład:

#define REQUIRES_API(x) __attribute__((__availability__(android,introduced=x)))
#define API_AT_LEAST(x) __builtin_available(android x, *)

void DecodeImageWithImageDecoder() REQUIRES_API(30) {
    // Call any APIs that were introduced in API 30 or newer without guards.
}

void DecodeImageFallback() {
    // Pay the overhead to call the Java APIs via JNI, or use third-party image
    // decoding libraries.
}

void DecodeImage() {
    if (API_AT_LEAST(30)) {
        DecodeImageWithImageDecoder();
    } else {
        DecodeImageFallback();
    }
}

Dziwności zabezpieczeń interfejsów API

W języku angielskim bardzo ważny jest sposób korzystania z usługi __builtin_available. Tylko literał (choć może zastąpić makro) if (__builtin_available(...)) działa. Równomierny Proste operacje, takie jak if (!__builtin_available(...)), nie będą działać (Clang wyświetli ostrzeżenie unsupported-availability-guard, a także unguarded-availability). Może to poprawić się w przyszłej wersji Clang. Zobacz Problem z LLVM 33161, aby dowiedzieć się więcej.

Kontrole stanu unguarded-availability mają zastosowanie tylko do zakresu funkcji, w którym i ich zastosowania. Clang wyświetli ostrzeżenie, nawet jeśli funkcja z wywołaniem API to tylko z zastrzeżonego zakresu. Aby uniknąć powtórzenia strażników w własny kod, przeczytaj artykuł Unikanie powtórzeń zabezpieczeń interfejsu API.

Dlaczego nie jest to ustawienie domyślne?

Różnica między silnymi odwołaniami do interfejsu API a słabymi odwołaniami do interfejsów API (o ile nie jest używana prawidłowo) że pierwszy z nich szybko i wyraźnie zakończy się niepowodzeniem, w drugim przypadku nie uda się rozwiązać problemu, dopóki użytkownik nie wykona działania, które spowoduje brakujący interfejs API. do wywołania. W takim przypadku komunikat o błędzie nie będzie zrozumiały. czas kompilacji „AFoo_bar() jest niedostępny” będzie to segfault. Na silne referencje, komunikat o błędzie jest znacznie bardziej zrozumiały, a błędy często bezpieczniejsze domyślne.

W związku z tym, że jest to nowa funkcja, pisze się bardzo mało istniejącego kodu do obsłużenia. w ten sposób. kod innej firmy, który nie został napisany z myślą o Androidzie; prawdopodobnie zawsze będzie mieć ten problem, więc obecnie nie ma żadnych planów zachowanie domyślne na zawsze.

Zalecamy korzystanie z tej metody, ale powoduje to więcej problemów trudne do wykrycia i debugowania, należy raczej świadomie podejść do tego ryzyka. niż ich zachowanie, które zmieniło się bez Twojej wiedzy.

Uwagi

Ta funkcja działa w przypadku większości interfejsów API, ale w niektórych przypadkach nie w naszej pracy.

Najmniejszym problemem są nowsze interfejsy API libc. W przeciwieństwie do pozostałych Interfejsy API Androida; są one chronione za pomocą etykiety #if __ANDROID_API__ >= X w nagłówkach a nie tylko __INTRODUCED_IN(X), co uniemożliwia nawet słabą deklarację jest widoczna. Ponieważ najstarszy nowoczesny poziom obsługi pakietów NDK na poziomie interfejsu API to r21, często potrzebne interfejsy API libc są już dostępne. Dodawane są nowe interfejsy API libc. wersji (patrz status.md), ale im jest nowsze, tym większe prawdopodobieństwo, że może być skrajnym przypadkiem, którego potrzebuje niewielu programistom. Jeśli jesteś jednym z na razie do wywoływania tych programistów musisz używać dlsym() Interfejsy API, jeśli minSdkVersion jest starszy niż API. Jest to problem, który można rozwiązać, ale to wiąże się z ryzykiem naruszenia zgodności źródeł w przypadku wszystkich aplikacji (i kodu zawierającego elementy polyfill interfejsów API libc nie uda się skompilować z powodu niezgodne atrybuty availability w bibliotece libc i deklaracjach lokalnych), więc nie wiemy, czy i kiedy go naprawimy.

Większość deweloperów ma okazję zetknąć się z biblioteką zawierający nowy interfejs API jest nowszy niż minSdkVersion. Tylko ta funkcja umożliwia słabe odwołania do symboli; nie ma czegoś takiego jak słaba biblioteka odwołania. Jeśli na przykład minSdkVersion ma 24 lata, możesz połączyć libvulkan.so i zadzwoń z strzeżonym numerem pod numer vkBindBufferMemory2, ponieważ Usługa libvulkan.so jest dostępna na urządzeniach od interfejsu API 24. Z drugiej strony, jeśli Twoja wartość minSdkVersion wynosiła 23, musisz wrócić do dlopen i dlsym bo biblioteka nie będzie istnieć na urządzeniach, które obsługują tylko Interfejs API 23. Nie znamy dobrego rozwiązania tego problemu, ale w przyszłości rozwiąże się on samoczynnie, ponieważ (gdy tylko jest to możliwe) nie zezwalamy na interfejsów API do tworzenia nowych bibliotek.

Dla autorów z biblioteki

Tworząc bibliotekę do użytku w aplikacjach na Androida, unikaj używania tej funkcji w nagłówkach publicznych. Można go bezpiecznie używać w: poza wierszem, ale jeśli korzystasz z kodu __builtin_available w kodzie takich jak funkcje wbudowane czy definicje szablonów, wymuszane konsumentów, aby włączyć tę funkcję. Z tych samych powodów nie włączamy tej funkcji domyślnie w NDK, należy unikać dokonywania tego wyboru w imieniu konsumentów.

Jeśli wymagasz tego zachowania w nagłówkach publicznych, upewnij się, że aby użytkownicy wiedzieli, że trzeba włączyć tę funkcję zdając sobie sprawę z ryzyka, jakie się z tym wiążą.