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ążą.