Na tej stronie wyjaśniamy, jak Twoja 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 ł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 unikniesz przypadkowego wysłania 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. Nadal 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ć wykonywana, a interfejs API można wywoływać normalnie bez konieczności używania dlopen()
i dlsym()
.
Słabe odwołania do interfejsu 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 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 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 w sposób natywny, 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 magicznie, że wywołania nowych interfejsów API są bezpieczne. Jedynym działaniem, jakie wykonuje, jest odroczenie błędu czasu ładowania do błędu czasu wywołania. Dzięki temu możesz zabezpieczyć ten wywołanie w czasie wykonywania i łagodnie przejść do funkcji zastępczej, 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 połączenie do 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, 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), 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.
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
(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 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 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 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
). Ta sytuacja może się poprawić 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, podczas gdy drugie nie zawiedzie, 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 błędem segfault. 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 zostały napisane w niewielkiej części w celu bezpiecznego obsługiwania tego zachowania. 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 pamiętaj, że utrudnia ono wykrywanie i rozwiązywanie problemów. Dlatego musisz świadomie zaakceptować te ryzyko, zamiast 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 nowsze, tym bardziej prawdopodobne, że będą to przypadki szczególne, których potrzebuje 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ć 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 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 taki sposób, aby użytkownicy wiedzieli, że muszą włączyć tę funkcję, i by byli świadomi związanych z tym zagrożeń.