Neuere APIs verwenden

Auf dieser Seite wird erläutert, wie Ihre App neue Funktionen des Betriebssystems nutzen kann, wenn sie auf neuen Betriebssystemversionen ausgeführt wird, wobei die Kompatibilität mit älteren Geräten aufrechterhalten wird.

Standardmäßig sind Verweise auf NDK APIs in Ihrer Anwendung aussagekräftige Referenzen. Das dynamische Ladeprogramm von Android wird diese Probleme schnellstmöglich auflösen, wenn deine Bibliothek geladen wird. Werden die Symbole nicht gefunden, wird die App abgebrochen. Dies steht im Gegensatz zum Verhalten von Java, bei dem erst dann eine Ausnahme ausgelöst wird, wenn die fehlende API aufgerufen wird.

Aus diesem Grund verhindert das NDK, dass Sie starke Verweise auf APIs erstellen, die neuer als das minSdkVersion Ihrer Anwendung sind. So schützen Sie sich vor dem versehentlichen Versand von Code, der während des Tests funktioniert hat, aber nicht geladen wird (UnsatisfiedLinkError wird von System.loadLibrary() auf älteren Geräten gelöscht). Andererseits ist es schwieriger, Code zu schreiben, der APIs verwendet, die neuer als der minSdkVersion Ihrer Anwendung sind, da Sie die APIs mit dlopen() und dlsym() statt mit einem normalen Funktionsaufruf aufrufen müssen.

Die Alternative zur Verwendung starker Referenzen sind schwache Referenzen. Ein schwacher Verweis, der beim Laden der Bibliothek nicht gefunden wird, führt dazu, dass die Adresse dieses Symbols auf nullptr gesetzt wird, anstatt beim Laden fehlschlägt. Sie können immer noch nicht sicher aufgerufen werden. Solange Aufrufwebsites jedoch so geschützt sind, dass die API nicht aufgerufen wird, wenn sie nicht verfügbar ist, kann der Rest des Codes ausgeführt werden. Sie können die API dann normal aufrufen, ohne dlopen() und dlsym() verwenden zu müssen.

Schwache API-Referenzen erfordern keine zusätzliche Unterstützung durch die dynamische Verknüpfung und können daher mit jeder Android-Version verwendet werden.

Schwache API-Referenzen im Build aktivieren

CMake

Übergeben Sie -DANDROID_WEAK_API_DEFS=ON, wenn Sie CMake ausführen. Wenn Sie CMake über externalNativeBuild verwenden, fügen Sie Folgendes zu build.gradle.kts (oder dem Groovy-Äquivalent, falls Sie noch build.gradle verwenden) hinzu:

android {
    // Other config...

    defaultConfig {
        // Other config...

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

NK-Build

Fügen Sie der Datei Application.mk Folgendes hinzu:

APP_WEAK_API_DEFS := true

Wenn Sie noch keine Application.mk-Datei haben, erstellen Sie sie im selben Verzeichnis wie die Datei Android.mk. Zusätzliche Änderungen an der Datei build.gradle.kts (oder build.gradle) sind für „ndk-build“ nicht erforderlich.

Andere Build-Systeme

Wenn Sie CMake oder ndk-build nicht verwenden, sehen Sie in der Dokumentation zu Ihrem Build-System nach, ob es eine empfohlene Methode zum Aktivieren dieser Funktion gibt. Wenn Ihr Build-System diese Option nativ nicht unterstützt, können Sie die Funktion aktivieren, indem Sie bei der Kompilierung die folgenden Flags übergeben:

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

Im ersten Schritt werden die NDK-Header so konfiguriert, dass schwache Referenzen zulässig sind. Die zweite wandelt die Warnung vor unsicheren API-Aufrufen in einen Fehler um.

Weitere Informationen finden Sie im Build System Maintenanceers Guide (nur auf Englisch verfügbar).

Geschützte API-Aufrufe

Diese Funktion sorgt dafür, dass Aufrufe neuer APIs nicht sicher sind. Er dient lediglich dazu, einen Ladezeitfehler auf einen Aufrufzeitfehler zu übertragen. Der Vorteil besteht darin, dass Sie diesen Aufruf während der Laufzeit absichern und Fallbacks anschaulich darstellen können, entweder durch Verwendung einer alternativen Implementierung, durch Benachrichtigung des Nutzers, dass diese Funktion der App auf seinem Gerät nicht verfügbar ist, oder indem Sie diesen Codepfad ganz vermeiden.

Clang kann eine Warnung ausgeben (unguarded-availability), wenn Sie einen nicht geschützten Aufruf an eine API senden, die für die minSdkVersion Ihrer Anwendung nicht verfügbar ist. Wenn Sie „ndk-build“ oder unsere CMake-Toolchain-Datei verwenden, wird diese Warnung beim Aktivieren dieses Features automatisch aktiviert und zu einem Fehler hochgestuft.

Hier sehen Sie ein Beispiel für Code, der dlopen() und dlsym() verwendet, um eine API bedingt zu verwenden, ohne dass diese Funktion aktiviert ist:

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

Das Lesen ist etwas chaotisch. Funktionsnamen sind mehrfach vorhanden (und wenn Sie C schreiben, auch die Signaturen), wird es erfolgreich erstellt, aber das Fallback wird immer zur Laufzeit verwendet, wenn Sie versehentlich den an dlsym übergebenen Funktionsnamen vertippen. Sie müssen dieses Muster für jede API verwenden.

Mit schwachen API-Referenzen kann die obige Funktion wie folgt umgeschrieben werden:

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

Intern ruft __builtin_available(android 31, *) android_get_device_api_level() auf, speichert das Ergebnis im Cache und vergleicht es mit 31 (dem API-Level, mit dem AImageDecoder_resultToString() eingeführt wurde).

Die einfachste Möglichkeit, den Wert für __builtin_available zu ermitteln, besteht darin, den Build ohne die Guard-Funktion (oder den Guard von __builtin_available(android 1, *)) zu erstellen und dann dem zu folgen, was Ihnen in der Fehlermeldung mitgeteilt wird. Ein nicht geschützter Aufruf von AImageDecoder_createFromAAsset() mit minSdkVersion 24 führt beispielsweise zu:

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

In diesem Fall sollte der Anruf durch __builtin_available(android 30, *) überwacht werden. Wenn kein Build-Fehler vorliegt, ist entweder die API immer für minSdkVersion verfügbar und es ist kein Guard erforderlich. Oder Ihr Build ist falsch konfiguriert und die Warnung unguarded-availability ist deaktiviert.

Alternativ enthält die NDK API-Referenz für jede API einen Hinweis wie „In API 30 eingeführt“. Wenn dieser Text nicht vorhanden ist, ist die API für alle unterstützten API-Ebenen verfügbar.

Wiederholung von API-Guards vermeiden

Wenn Sie diese Option verwenden, werden Sie wahrscheinlich Code-Abschnitte in Ihrer App haben, die nur auf ausreichenden Geräten nutzbar sind. Anstatt die __builtin_available()-Prüfung in jeder Funktion zu wiederholen, können Sie Ihren eigenen Code so annotieren, dass ein bestimmtes API-Level erforderlich ist. Beispielsweise wurden die ImageDecoder-APIs selbst in API 30 hinzugefügt. Für Funktionen, die diese APIs intensiv nutzen, können Sie beispielsweise Folgendes tun:

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

Quirks von API-Guards

Bei Clang wird besonders __builtin_available verwendet. Es funktioniert nur ein literales if (__builtin_available(...)) (auch wenn es möglicherweise mit einem Makro ersetzt wird). Selbst einfache Vorgänge wie if (!__builtin_available(...)) werden nicht funktionieren (Clang gibt sowohl die unsupported-availability-guard-Warnung als auch unguarded-availability aus). Dies kann sich in einer zukünftigen Version von Clang verbessern. Weitere Informationen finden Sie unter LLVM-Problem 33161.

Die Prüfung auf unguarded-availability gilt nur für den Funktionsbereich, in dem sie verwendet werden. Clang gibt die Warnung auch dann aus, wenn die Funktion mit dem API-Aufruf nur innerhalb eines überwachten Bereichs aufgerufen wird. Informationen dazu, wie Sie die Wiederholung von Guards in Ihrem eigenen Code vermeiden, finden Sie unter Wiederholung von API-Guards vermeiden.

Warum ist das nicht die Standardeinstellung?

Wenn sie nicht richtig verwendet werden, besteht der Unterschied zwischen starken und schwachen API-Referenzen darin, dass erste Referenzen schnell und offensichtlich fehlschlagen, während letztere erst fehlschlagen, wenn der Nutzer eine Aktion ausführt, die zum Aufrufen der fehlenden API führt. In diesem Fall ist die Fehlermeldung keine eindeutige Kompilierungszeit des Fehlers „AFoo_bar() is not available“, sondern ein Segmentfehler. Mit starken Verweisen sind die Fehlermeldungen viel klarer und „failing-fast“ eine sicherere Standardeinstellung.

Da dies eine neue Funktion ist, wird bisher nur sehr wenig Code geschrieben, um dieses Verhalten sicher zu handhaben. Bei Code von Drittanbietern, der nicht für Android geschrieben wurde, tritt wahrscheinlich immer dieses Problem auf. Es gibt derzeit also keine Pläne, das Standardverhalten irgendwann zu ändern.

Wir empfehlen, dass Sie diese Methode verwenden. Da Probleme jedoch das Erkennen und Beheben von Problemen erschweren, sollten Sie diese Risiken wissentlich akzeptieren, anstatt das Verhalten ohne Ihr Wissen zu ändern.

Einschränkungen

Diese Funktion funktioniert mit den meisten APIs, es gibt jedoch auch Fälle, in denen sie nicht funktioniert.

Am wenigsten problematisch sind neuere libc-APIs. Im Gegensatz zu den anderen Android-APIs werden diese mit #if __ANDROID_API__ >= X in den Headern und nicht nur mit __INTRODUCED_IN(X) geschützt, wodurch selbst die schwache Deklaration nicht sichtbar ist. Da r21 die älteste moderne NDKs auf API-Ebene ist, sind die am häufigsten benötigten libc APIs bereits verfügbar. Mit jedem Release werden neue libc-APIs hinzugefügt (siehe status.md). Je neuer sie jedoch sind, desto wahrscheinlicher ist es, dass sie ein Grenzfall sind, den nur wenige Entwickler benötigen. Wenn Sie zu diesen Entwicklern gehören, müssen Sie jedoch derzeit weiterhin dlsym() zum Aufrufen dieser APIs verwenden, wenn Ihr minSdkVersion älter als die API ist. Dies ist ein lösbares Problem, birgt aber das Risiko, dass die Kompatibilität der Quelle für alle Apps beeinträchtigt wird. Code, der polyfills von libc-APIs enthält, kann aufgrund von nicht übereinstimmenden availability-Attributen in der libc- und lokalen Deklaration nicht kompiliert werden. Wir sind uns also nicht sicher, ob oder wann wir das Problem beheben werden.

Wahrscheinlich werden mehr Entwickler auf sie stoßen, wenn die Bibliothek, die die neue API enthält, neuer ist als deine minSdkVersion. Diese Funktion ermöglicht nur Verweise mit schwachen Symbolen. Es gibt keine schwache Bibliotheksreferenz. Wenn deine minSdkVersion beispielsweise 24 ist, kannst du libvulkan.so verknüpfen und einen geschützten Aufruf an vkBindBufferMemory2 starten, da libvulkan.so auf Geräten ab API 24 verfügbar ist. Wenn Ihre minSdkVersion hingegen 23 ist, müssen Sie auf dlopen und dlsym zurückgreifen, da die Bibliothek auf Geräten, die nur API 23 unterstützen, nicht auf dem Gerät vorhanden ist. Wir kennen keine gute Lösung zur Behebung dieses Falls, aber auf lange Sicht löst sich das Problem von selbst, da wir (wenn möglich) keine neuen APIs mehr erlauben, neue Bibliotheken zu erstellen.

Für Bibliotheksautoren

Wenn Sie eine Bibliothek für die Verwendung in Android-Anwendungen entwickeln, sollten Sie diese Funktion in Ihren öffentlichen Headern vermeiden. Es kann sicher in Out-of-Line-Code verwendet werden. Wenn Sie jedoch in Code in Ihren Headern (z. B. Inline-Funktionen oder Vorlagendefinitionen) __builtin_available verwenden, zwingen Sie alle Nutzer dazu, diese Funktion zu aktivieren. Aus den gleichen Gründen, aus denen wir diese Funktion nicht standardmäßig im NDK aktivieren, sollten Sie diese Entscheidung nicht im Namen Ihrer Nutzer treffen.

Wenn Sie dies in Ihren öffentlichen Headern tun müssen, dokumentieren Sie dies, damit Ihre Nutzer wissen, dass sie die Funktion aktivieren müssen, und sich der Risiken bewusst sind.