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. Der dynamische Loader von Android löst sie beim Laden der Bibliothek sofort. Wenn die Symbole nicht gefunden werden, wird die App abgebrochen. Das ist im Gegensatz zu Java, wo eine Ausnahme erst geworfen wird, wenn die fehlende API aufgerufen wird.

Aus diesem Grund verhindert das NDK, dass Sie starke Verweise auf APIs erstellen, die neuer sind als die minSdkVersion Ihrer App. 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 die minSdkVersion Ihrer App sind, da Sie die APIs mit dlopen() und dlsym() statt mit einem normalen Funktionsaufruf aufrufen müssen.

Die Alternative zu starken 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 weiterhin nicht sicher aufgerufen werden. Solange die Aufrufstellen jedoch so gesichert sind, dass die API nicht aufgerufen wird, wenn sie nicht verfügbar ist, kann der Rest Ihres Codes ausgeführt werden und Sie können die API wie gewohnt aufrufen, ohne dlopen() und dlsym() verwenden zu müssen.

Schwache API-Referenzen erfordern keine zusätzliche Unterstützung durch den dynamischen Linker und können daher mit jeder Android-Version verwendet werden.

Schwache API-Referenzen in Ihrem Build aktivieren

CMake

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

android {
    // Other config...

    defaultConfig {
        // Other config...

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

ndk-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 Ihre Android.mk-Datei. Für ndk-build sind keine zusätzlichen Änderungen an der Datei build.gradle.kts (oder build.gradle) erforderlich.

Andere Build-Systeme

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

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

Mit der ersten werden die NDK-Header so konfiguriert, dass schwache Verweise 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. Es verschiebt lediglich einen Fehler bei der Ladezeit zu einem Fehler bei der Aufrufzeit. Der Vorteil besteht darin, dass Sie diesen Aufruf zur Laufzeit schützen und einen reibungslosen Übergang vornehmen können, sei es durch eine alternative Implementierung, durch Benachrichtigung des Nutzers, dass diese Funktion der App auf seinem Gerät nicht verfügbar ist, oder durch vollständiges Vermeiden dieses Codepfads.

Clang kann eine Warnung (unguarded-availability) ausgeben, wenn Sie einen ungeschützten Aufruf einer API ausführen, die für die minSdkVersion Ihrer App nicht verfügbar ist. Wenn Sie ndk-build oder unsere CMake-Toolchain-Datei verwenden, wird diese Warnung automatisch aktiviert und in einen Fehler umgewandelt, wenn Sie diese Funktion aktivieren.

Hier ist ein Beispiel für Code, in dem eine API bedingt verwendet wird, ohne dass diese Funktion aktiviert ist. Dabei werden dlopen() und dlsym() verwendet:

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 ist etwas unübersichtlich, da einige Funktionsnamen doppelt vorkommen (und bei C-Code auch die Signaturen). Die Funktion wird zwar erfolgreich erstellt, aber bei Laufzeitfehlern wird immer der Fallback verwendet, wenn Sie versehentlich einen Tippfehler in den an dlsym übergebenen Funktionsnamen machen. Außerdem müssen Sie 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;
    }
}

Im Hintergrund ruft __builtin_available(android 31, *) android_get_device_api_level() auf, speichert das Ergebnis im Cache und vergleicht es mit 31 (die API-Ebene, auf der AImageDecoder_resultToString() eingeführt wurde).

Am einfachsten lässt sich der richtige Wert für __builtin_available ermitteln, indem Sie versuchen, den Build ohne Guard (oder mit einer Guard von __builtin_available(android 1, *)) auszuführen und der Fehlermeldung folgen. Ein ungeschü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 Aufruf durch __builtin_available(android 30, *) geschützt werden. Wenn kein Buildfehler auftritt, ist die API entweder immer für Ihre minSdkVersion verfügbar und es ist kein Guard erforderlich oder Ihr Build ist falsch konfiguriert und die unguarded-availability-Warnung ist deaktiviert.

Alternativ wird in der NDK API-Referenz für jede API etwa „Introduced in API 30“ (In API 30 eingeführt) angezeigt. 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 Ihrer Funktionen zu wiederholen, können Sie Ihren eigenen Code mit dem Hinweis versehen, dass eine bestimmte API-Ebene erforderlich ist. Beispielsweise wurden die ImageDecoder APIs in API 30 hinzugefügt. Für Funktionen, in denen diese APIs häufig verwendet werden, können Sie 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

Clang ist sehr genau bei der Verwendung von __builtin_available. Es funktioniert nur ein literaler (ggf. durch ein Makro ersetzter) if (__builtin_available(...)). Selbst triviale Vorgänge wie if (!__builtin_available(...)) funktionieren nicht. Clang gibt sowohl die Warnung unsupported-availability-guard als auch unguarded-availability aus. Dies wird in einer zukünftigen Version von Clang möglicherweise verbessert. Weitere Informationen finden Sie unter LLVM-Problem 33161.

Prüfungen für unguarded-availability gelten nur für den Funktionsumfang, in dem sie verwendet werden. Clang gibt die Warnung auch aus, wenn die Funktion mit dem API-Aufruf nur innerhalb eines geschützten Bereichs aufgerufen wird. Wie Sie Wiederholungen von Guards in Ihrem eigenen Code vermeiden, erfahren Sie unter Wie Sie Wiederholungen von API-Guards vermeiden.

Warum ist das nicht die Standardeinstellung?

Bei unsachgemäßer Verwendung besteht der Unterschied zwischen starken und schwachen API-Verweisen darin, dass bei ersteren schnell und offensichtlich ein Fehler auftritt, während bei letzteren erst ein Fehler auftritt, wenn der Nutzer eine Aktion ausführt, die den Aufruf der fehlenden API auslöst. In diesem Fall ist die Fehlermeldung nicht klar und bezieht sich nicht auf die Kompilierungszeit, sondern auf einen Segfault. Bei starken Referenzen ist die Fehlermeldung viel verständlicher und „Fail Fast“ ist eine sicherere Standardeinstellung.

Da dies eine neue Funktion ist, gibt es nur sehr wenig vorhandenen Code, der dieses Verhalten sicher handhabt. Bei Drittanbietercode, der nicht für Android entwickelt wurde, wird dieses Problem wahrscheinlich immer auftreten. Daher ist derzeit nicht geplant, das Standardverhalten 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 bei den meisten APIs, in einigen Fällen jedoch nicht.

Die Wahrscheinlichkeit, dass Probleme auftreten, ist bei neueren libc-APIs am geringsten. Im Gegensatz zu den anderen Android APIs werden diese in den Headern nicht nur mit __INTRODUCED_IN(X), sondern mit #if __ANDROID_API__ >= X geschützt, wodurch selbst die schwache Deklaration nicht sichtbar ist. Da das älteste API-Level, das moderne NDKs unterstützen, r21 ist, sind die am häufigsten benötigten libc-APIs bereits verfügbar. Mit jeder Version werden neue libc-APIs hinzugefügt (siehe status.md). Je neuer sie sind, desto wahrscheinlicher handelt es sich um einen Grenzfall, 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. Dieses Problem lässt sich beheben, aber dadurch besteht das Risiko, dass die Quellkompatibilität für alle Apps beeinträchtigt wird. Code, der Polyfills von libc-APIs enthält, kann aufgrund der nicht übereinstimmenden availability-Attribute in der libc und den lokalen Deklarationen nicht kompiliert werden. Wir sind uns nicht sicher, ob und wann wir das Problem beheben werden.

Die Situation, die wahrscheinlich häufiger auftritt, ist die, in der die Bibliothek, die die neue API enthält, neuer ist als Ihre minSdkVersion. Diese Funktion ermöglicht nur Verweise mit schwachen Symbolen. Es gibt keine schwache Bibliotheksreferenz. Wenn Ihre minSdkVersion beispielsweise 24 ist, können Sie libvulkan.so verknüpfen und einen abgesicherten Aufruf von vkBindBufferMemory2 ausführen, 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 für dieses Problem, aber auf lange Sicht wird es sich von selbst lösen, da wir (nach Möglichkeit) nicht mehr zulassen, dass mit neuen APIs neue Bibliotheken erstellt werden.

Für Autoren von Bibliotheken

Wenn Sie eine Bibliothek für Android-Anwendungen entwickeln, sollten Sie diese Funktion in Ihren öffentlichen Headern nicht verwenden. Sie kann bedenkenlos in Code außerhalb der Codezeile verwendet werden. Wenn Sie __builtin_available jedoch in Code in Ihren Headern verwenden, z. B. in Inline-Funktionen oder Vorlagendefinitionen, müssen alle Ihre Nutzer diese Funktion 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 dieses Verhalten in Ihren öffentlichen Headern benötigen, müssen Sie dies dokumentieren, damit Ihre Nutzer wissen, dass sie die Funktion aktivieren müssen und sich der damit verbundenen Risiken bewusst sind.