Korzystanie z nowszych interfejsów API

Na tej stronie wyjaśniamy, jak aplikacja może korzystać z nowych funkcji systemu operacyjnego w wersjach nowszych, zachowując przy tym zgodność ze starszymi urządzeniami.

Domyślnie odwołania do interfejsów NDK w aplikacji są odwołaniami silnymi. Dynamiczny ładowarkę Androida chętnie je rozwiąże, gdy biblioteka zostanie załadowana. Jeśli nie znajdzie symboli, aplikacja przerwie działanie. Jest to sprzeczne z zachowaniem języka Java, w którym wyjątek nie zostanie rzucony, dopóki nie zostanie wywołany brakujący interfejs API.

Z tego powodu NDK uniemożliwi tworzenie silnych odwołań do interfejsów API, które są nowsze niż te w Twojej aplikacji minSdkVersion. Dzięki temu nie wyślesz przez pomyłkę kodu, który działał podczas testowania, ale nie wczytuje się (UnsatisfiedLinkError zostanie wyrzucony z System.loadLibrary()) na starszych urządzeniach. Z drugiej strony, pisanie kodu, który korzysta z interfejsów API nowszych niż minSdkVersion Twojej aplikacji, jest trudniejsze, ponieważ musisz wywoływać interfejsy API za pomocą funkcji dlopen()dlsym(), a nie zwykłego wywołania funkcji.

Alternatywą dla silnych odniesień są słabe odniesienia. Słaba referencja, która nie została znaleziona podczas wczytywania biblioteki, powoduje, że adres tego symbolu jest ustawiony na nullptr, a nie nie wczytuje się. Nadal nie można ich bezpiecznie wywoływać, ale dopóki strony wywołania są chronione, aby zapobiec wywoływaniu interfejsu API, gdy nie jest on dostępny, pozostała część kodu może być uruchamiana, a interfejs API można wywoływać normalnie bez konieczności używania dlopen()dlsym().

Słabe odwołania do interfejsów API nie wymagają dodatkowego wsparcia od dynamicznego linkera, więc można ich używać w dowolnej wersji Androida.

Włączanie w kompilacji niepełnych odwołań do interfejsu API

CMake

Przekazywanie parametru -DANDROID_WEAK_API_DEFS=ON podczas uruchamiania CMake. Jeśli używasz CMake za pomocą externalNativeBuild, dodaj do pliku build.gradle.kts (lub jego odpowiednika w Groovy, jeśli nadal używasz build.gradle) te wiersze kodu:

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 katalogu co plik Android.mk. W przypadku ndk-build nie trzeba wprowadzać dodatkowych zmian w pliku build.gradle.kts (lub build.gradle).

Inne systemy kompilacji

Jeśli nie używasz CMake ani ndk-build, zapoznaj się z dokumentacją dotyczącą systemu kompilacji, aby sprawdzić, czy istnieje zalecany sposób włączenia tej funkcji. Jeśli Twój system kompilacji nie obsługuje tej opcji, możesz ją włączyć, podając te flagi podczas kompilacji:

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

Pierwszy skonfiguruje nagłówki NDK, aby zezwolić na słabe odwołania. Drugi z nich zmienia ostrzeżenie dotyczące niebezpiecznych wywołań interfejsu API na błąd.

Więcej informacji znajdziesz w przewodniku dla administratorów systemu kompilacji.

Chronione wywołania interfejsu API

Ta funkcja nie powoduje automatycznie, że wywołania nowych interfejsów API są bezpieczne. Jedynym skutkiem jest to, że błąd występujący w czasie wczytywania jest przekształcany w błąd występujący w czasie wywołania. Dzięki temu możesz zabezpieczyć ten wywołanie w czasie wykonywania i łagodnie przejść do funkcji zapasowej, np. używając alternatywnej implementacji lub informując użytkownika, że dana funkcja aplikacji jest niedostępna na jego urządzeniu, albo całkowicie unikając tego ścieżki kodu.

Clang może wyświetlić ostrzeżenie (unguarded-availability), gdy wykonasz nieosłonięte wywołanie interfejsu API, który jest niedostępny dla minSdkVersion Twojej aplikacji. Jeśli używasz ndk-build lub naszego pliku łańcucha narzędzi CMake, to ostrzeżenie zostanie automatycznie włączone i przekształcone w błąd po włączeniu tej funkcji.

Oto przykład kodu, który korzysta z interfejsu API warunkowo bez włączenia tej funkcji za pomocą elementów dlopen()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);
    }
}

Jest to trochę nieczytelne, ponieważ występują w nim duplikaty nazw funkcji (a jeśli piszesz w C, to także podpisy), ale kompilacja przebiegnie prawidłowo. Podczas wykonywania kodu zawsze będzie używana funkcja zastępcza, jeśli przez przypadek podasz nieprawidłową nazwę funkcji, np. dlsym. Musisz używać tego wzorca w przypadku każdego interfejsu API.

Przy użyciu słabych odwołań do interfejsu API powyższą funkcję można zapisać w postaci:

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

Pod spodem __builtin_available(android 31, *) wywołuje android_get_device_api_level(), zapisuje wynik w pamięci podręcznej i porównuje go z 31 (co jest poziomem interfejsu API, który wprowadził funkcję AImageDecoder_resultToString()).

Najprostszym sposobem na określenie wartości parametru __builtin_available jest próba skompilowania kodu bez warunku (lub warunku dla __builtin_available(android 1, *)) i wykonywanie instrukcji podanych w komunikacie o błędzie. Na przykład niechronione wywołanie funkcji AImageDecoder_createFromAAsset() z argumentem minSdkVersion 24 zwróci:

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

W tym przypadku rozmowa powinna być chroniona przez __builtin_available(android 30, *). Jeśli nie ma błędu kompilacji, oznacza to, że interfejs API jest zawsze dostępny dla użytkownika minSdkVersion i nie trzeba stosować zabezpieczeń, albo kompilacja jest źle skonfigurowana i ostrzeżenie unguarded-availability jest wyłączone.

W dokumentacji interfejsu NDK API w przypadku każdego interfejsu API znajdziesz informację „Wprowadzony w interfejsie API 30”. Jeśli go nie ma, oznacza to, że interfejs API jest dostępny na wszystkich obsługiwanych poziomach interfejsu API.

Unikanie powtarzania zabezpieczeń interfejsu API

Jeśli korzystasz z tego rozwiązania, prawdopodobnie masz w aplikacji sekcje kodu, które są przydatne tylko na nowszych urządzeniach. Zamiast powtarzać weryfikację __builtin_available() w każdej funkcji, możesz dodać do swojego kodu adnotacje informujące, że wymaga on określonego poziomu interfejsu API. Na przykład interfejsy API ImageDecoder zostały dodane w wersji 30, więc w przypadku funkcji, które intensywnie korzystają z tych interfejsów API, możesz wykonać takie czynności:

#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();
    }
}

Specyfikatyka zabezpieczeń interfejsu API

Clang jest bardzo wymagający, jeśli chodzi o sposób używania __builtin_available. Działa tylko dosłowne if (__builtin_available(...)) (chociaż może być zastąpione przez makro). Nawet trywialne operacje, takie jak if (!__builtin_available(...)), nie będą działać (Clang wyemituje ostrzeżenie unsupported-availability-guard, a także unguarded-availability). Ta sytuacja może ulec poprawie w przyszłej wersji Clang. Więcej informacji znajdziesz w problemie LLVM 33161.

Sprawdzanie funkcji unguarded-availability ma zastosowanie tylko do zakresu funkcji, w którym są używane. Clang wyemituje ostrzeżenie, nawet jeśli funkcja z wywołaniem interfejsu API jest wywoływana tylko w ramach chronionego zakresu. Aby uniknąć powtarzania zabezpieczeń w swoim kodzie, zapoznaj się z artykułem Unikanie powtarzania zabezpieczeń interfejsu API.

Dlaczego nie jest to domyślne ustawienie?

Jeśli nie są używane prawidłowo, silne odwołania do interfejsu API różnią się od słabych odwołań do interfejsu API tym, że pierwsze powodują szybkie i wyraźne niepowodzenie, podczas gdy drugie nie powodują niepowodzenia, dopóki użytkownik nie wykona działania, które spowoduje wywołanie brakującego interfejsu API. W takim przypadku komunikat o błędzie nie będzie jasnym błędem kompilacji „AFoo_bar() is not available”, tylko segfaultem. Dzięki mocnym referencjom komunikat o błędzie jest znacznie bardziej przejrzysty, a szybkie niepowodzenie jest bezpieczniejszym domyślnym działaniem.

Ponieważ jest to nowa funkcja, istniejące kody nie są w większości napisane w sposób bezpieczny. Kod zewnętrzny, który nie został napisany z myślą o Androidzie, będzie prawdopodobnie zawsze miał ten problem, więc obecnie nie planujemy zmiany domyślnego zachowania.

Zalecamy korzystanie z tego rozwiązania, ale ponieważ utrudnia ono wykrywanie i debugowanie problemów, musisz świadomie zaakceptować te zagrożenia, a nie pozwolić, aby zachowanie zmieniło się bez Twojej wiedzy.

Ograniczenia

Ta funkcja działa w przypadku większości interfejsów API, ale są pewne sytuacje, w których nie działa.

Najmniej prawdopodobne jest, że problemy będą dotyczyć nowszych interfejsów libc. W odróżnieniu od pozostałych interfejsów API Androida te są chronione w nagłówkach za pomocą wartości #if __ANDROID_API__ >= X, a nie tylko __INTRODUCED_IN(X), co uniemożliwia wyświetlanie nawet słabych deklaracji. Najstarszy obsługiwany poziom interfejsu API to r21, więc najczęściej używane interfejsy API libc są już dostępne. Nowe interfejsy API libc są dodawane w każdej wersji (patrz status.md), ale im są nowsze, tym bardziej prawdopodobne, że będą to przypadki szczególne, których będzie potrzebować niewielu deweloperów. Jeśli jednak należysz do tej grupy deweloperów, musisz na razie kontynuować używanie interfejsu dlsym() do wywoływania tych interfejsów API, jeśli Twoja wersja minSdkVersion jest starsza niż interfejs API. Jest to problem, który można rozwiązać, ale wiąże się to z ryzykiem utraty zgodności źródłowej we wszystkich aplikacjach (każdy kod zawierający polyfille interfejsów libc nie będzie się kompilować z powodu niezgodnych atrybutów availability w deklaracjach libc i lokalnych), więc nie jesteśmy pewni, czy i kiedy go rozwiążemy.

Deweloperzy najczęściej spotykają się z tą sytuacją, gdy biblioteka zawierająca nowy interfejs API jest nowsza niż Twoja minSdkVersion. Ta funkcja umożliwia tylko słabe odwołania do symboli. Nie ma czegoś takiego jak słabe odwołanie do biblioteki. Jeśli na przykład minSdkVersion to 24, możesz połączyć libvulkan.so i wykonać chronione wywołanie vkBindBufferMemory2, ponieważ libvulkan.so jest dostępne na urządzeniach od poziomu API 24. Jeśli jednak minSdkVersion to 23, musisz użyć dlopendlsym, ponieważ biblioteka nie będzie istnieć na urządzeniach, które obsługują tylko API 23. Nie znamy dobrego rozwiązania tego problemu, ale w długim okresie czasu rozwiąże się on sam, ponieważ (w miarę możliwości) nie zezwalamy już nowym interfejsom API na tworzenie nowych bibliotek.

Dla autorów bibliotek

Jeśli opracowujesz bibliotekę do użycia w aplikacjach na Androida, nie używaj tej funkcji w publicznych nagłówkach. Można go bezpiecznie używać w kodzie offline, ale jeśli używasz funkcji __builtin_available w dowolnym kodzie w nagłówkach, np. w funkcjach wstawianych lub definicjach szablonów, zmuszasz wszystkich użytkowników do włączenia tej funkcji. Z tych samych powodów nie włączamy tej funkcji domyślnie w NDK, dlatego nie należy wybierać tej opcji w imieniu konsumentów.

Jeśli wymagasz takiego zachowania w publicznych nagłówkach, zrób to w dokumentacji, aby użytkownicy wiedzieli, że muszą włączyć tę funkcję i mogli się zapoznać z ryzykiem.