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 twardymi. Dynamiczny ładownik Androida chętnie spróbuje je rozwiązać, gdy biblioteka zostanie załadowana. Jeśli nie znajdzie symboli, aplikacja przerwie działanie. Jest to sprzeczne z działaniem Javy, w której wyjątki nie są zgłaszane, 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 będziesz przypadkowo wysyłać 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()
i dlsym()
, a nie zwykłego wywołania funkcji.
Alternatywą dla silnych odniesień są słabe odniesienia. Jeśli podczas wczytywania biblioteki nie uda się znaleźć słabego odwołania, adres tego symbolu zostanie ustawiony na nullptr
, a nie spowoduje to niepowodzenia wczytywania biblioteki. Nie można ich bezpiecznie wywoływać, ale dopóki miejsca wywołania są chronione, aby zapobiec wywoływaniu interfejsu API, gdy nie jest on dostępny, reszta kodu może być uruchamiana, a interfejs API można wywoływać normalnie bez konieczności używania funkcji dlopen()
i dlsym()
.
Słabe odwołania do interfejsu API nie wymagają dodatkowego wsparcia od linkera dynamicznego, więc można ich używać w dowolnej wersji Androida.
Włączanie w kompilacji niepełnych odwołań do interfejsu API
CMake
Przekaż parametr -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 Groovie, 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
. Dodatkowe zmiany w pliku build.gradle.kts
(lub build.gradle
) nie są konieczne do użycia narzędzia ndk-build.
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 działaniem, jakie wykonuje, jest odroczenie błędu czasu wczytywania do błędu czasu wywołania. Dzięki temu możesz zabezpieczyć ten wywołanie w czasie wykonywania i łatwo 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 nie jest dostę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, używając instrukcji 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);
}
}
Jest to trochę nieczytelne, ponieważ występują w nim duplikaty nazw funkcji (a jeśli piszesz w C, to także podpisy). Kompilacja przebiegnie prawidłowo, ale podczas wykonywania zawsze będzie używany element zastępczy, jeśli przez przypadek podasz nieprawidłową nazwę funkcji, np. dlsym
. Musisz używać tego wzorca w przypadku każdego interfejsu API.
W przypadku 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
(czyli poziomem interfejsu API, który wprowadził funkcję AImageDecoder_resultToString()
).
Najprostszym sposobem na określenie wartości do użycia w przypadku __builtin_available
jest próba skompilowania kodu bez warunku (lub warunku __builtin_available(android 1, *)
) i podążanie za wskazówkami zawartymi 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 takim 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 znajdziesz informacje o każdym interfejsie API, np. „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 podejścia, prawdopodobnie masz w aplikacji sekcje kodu, które można używać tylko na nowszych urządzeniach. Zamiast powtarzać weryfikację __builtin_available()
w każdej funkcji, możesz dodać do swojego kodu adnotację, ż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 proste 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, różnica między mocnymi a słabymi odwołaniami do interfejsu API polega na tym, że pierwsze zawiedzą szybko i wyraźnie, a drugie – dopiero gdy użytkownik wykona działanie, które spowoduje wywołanie brakującego interfejsu API. W takim przypadku komunikat o błędzie nie będzie jasny, ponieważ nie będzie to błąd kompilacji „AFoo_bar() is not available”. Będzie to błąd segfault. Dzięki mocnym referencjom komunikat o błędzie jest znacznie bardziej przejrzysty, a szybkie niepowodzenie jest bezpieczniejszym domyślnym zachowaniem.
Ponieważ jest to nowa funkcja, istnieje bardzo niewiele kodu, który bezpiecznie obsługuje to zachowanie. 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.
Uwagi
Ta funkcja działa w przypadku większości interfejsów API, ale są pewne sytuacje, w których nie działa.
Przed NDK r28 nie działało to w przypadku interfejsów API libc ani libm.
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ć dlopen
i dlsym
, 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 go w dowolnym kodzie w nagłówkach (np. w funkcjach wstawianych lub definicjach szablonów), zmuszasz wszystkich użytkowników do włączenia tej funkcji.__builtin_available
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 taki sposób, aby użytkownicy wiedzieli, że muszą włączyć tę funkcję, i by byli świadomi związanych z tym zagrożeń.