Utiliser des API plus récentes

Cette page explique comment votre application peut utiliser les nouvelles fonctionnalités du système d'exploitation lorsqu'elle s'exécute sur de nouvelles versions d'OS tout en préservant la compatibilité avec les appareils plus anciens.

Par défaut, les références aux API du NDK dans votre application sont des références fortes. Le Dynamic Loader d'Android les résoudra avec impatience une fois votre bibliothèque chargée. Si les symboles sont introuvables, l'application est abandonnée. Ce comportement est contraire au comportement de Java, où aucune exception n'est générée tant que l'API manquante n'est pas appelée.

Pour cette raison, le NDK vous empêche de créer des références fortes à des API plus récentes que le minSdkVersion de votre application. Cela vous évite d'envoyer accidentellement du code qui a fonctionné pendant vos tests, mais qui ne se chargera pas (UnsatisfiedLinkError sera généré à partir de System.loadLibrary()) sur les appareils plus anciens. D'autre part, il est plus difficile d'écrire du code qui utilise des API plus récentes que le minSdkVersion de votre application, car vous devez appeler les API à l'aide de dlopen() et dlsym() plutôt qu'un appel de fonction normal.

L'alternative à l'utilisation de références fortes consiste à utiliser des références faibles. Une référence faible introuvable lors du chargement de la bibliothèque entraîne la définition de l'adresse de ce symbole sur nullptr au lieu de l'échec du chargement. Ils ne peuvent toujours pas être appelés en toute sécurité, mais tant que les sites d'appel sont protégés pour empêcher l'appel de l'API lorsqu'elle n'est pas disponible, le reste de votre code peut être exécuté. Vous pouvez appeler l'API normalement sans avoir à utiliser dlopen() et dlsym().

Les références d'API faibles ne nécessitent pas de prise en charge supplémentaire de la part de l'éditeur de liens dynamique. Elles peuvent donc être utilisées avec n'importe quelle version d'Android.

Activer les références d'API faibles dans votre build

CMake

Transmettez -DANDROID_WEAK_API_DEFS=ON lors de l'exécution de CMake. Si vous utilisez CMake via externalNativeBuild, ajoutez ce qui suit à votre build.gradle.kts (ou l'équivalent Groovy si vous utilisez toujours build.gradle):

android {
    // Other config...

    defaultConfig {
        // Other config...

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

ndk-build

Ajoutez le code ci-dessous à votre fichier Application.mk :

APP_WEAK_API_DEFS := true

Si vous n'avez pas encore de fichier Application.mk, créez-le dans le même répertoire que votre fichier Android.mk. Il n'est pas nécessaire d'apporter d'autres modifications à votre fichier build.gradle.kts (ou build.gradle) pour ndk-build.

Autres systèmes de compilation

Si vous n'utilisez ni CMake, ni ndk-build, consultez la documentation de votre système de compilation pour savoir s'il existe une méthode recommandée pour activer cette fonctionnalité. Si votre système de compilation n'est pas compatible de manière native avec cette option, vous pouvez l'activer en transmettant les options suivantes lors de la compilation:

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

Le premier configure les en-têtes du NDK pour autoriser les références faibles. La seconde transforme l'avertissement pour les appels d'API non sécurisés en erreur.

Pour en savoir plus, consultez le document Build System Managingers Guide (Créer le guide de gestion du système).

Appels d'API protégés

Cette fonctionnalité ne sécurise pas automatiquement les appels de nouvelles API. Elle permet uniquement de reporter une erreur de temps de chargement à une erreur d'appel. L'avantage est que vous pouvez protéger cet appel au moment de l'exécution et revenir en douceur, que ce soit en utilisant une autre implémentation, en informant l'utilisateur que cette fonctionnalité de l'application n'est pas disponible sur son appareil ou en évitant complètement ce chemin de code.

Clang peut émettre un avertissement (unguarded-availability) lorsque vous effectuez un appel non protégé à une API qui n'est pas disponible pour le minSdkVersion de votre application. Si vous utilisez ndk-build ou notre fichier de chaîne d'outils CMake, cet avertissement sera automatiquement activé et converti en erreur lors de l'activation de cette fonctionnalité.

Voici un exemple de code utilisant dlopen() et dlsym() pour une utilisation conditionnelle d'une API sans cette fonctionnalité:

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

La lecture est un peu désordonnée, les noms de fonctions sont dupliqués (et si vous écrivez en C, les signatures), la compilation réussit, mais la création de remplacement est toujours effectuée au moment de l'exécution si vous saisissez accidentellement le nom de la fonction transmis à dlsym. Vous devez utiliser ce modèle pour chaque API.

Avec des références d'API faibles, la fonction ci-dessus peut être réécrite comme suit:

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

En arrière-plan, __builtin_available(android 31, *) appelle android_get_device_api_level(), met en cache le résultat et le compare à 31 (qui est le niveau d'API ayant introduit AImageDecoder_resultToString()).

Le moyen le plus simple de déterminer la valeur à utiliser pour __builtin_available est d'essayer de compiler sans garde (ou avec __builtin_available(android 1, *)) et de faire ce que le message d'erreur vous indique. Par exemple, un appel non protégé à AImageDecoder_createFromAAsset() avec minSdkVersion 24 produit:

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

Dans ce cas, l'appel doit être protégé par __builtin_available(android 30, *). S'il n'y a pas d'erreur de compilation, soit l'API est toujours disponible pour votre minSdkVersion et aucun dispositif de protection n'est nécessaire, ou votre build est mal configuré et l'avertissement unguarded-availability est désactivé.

Sinon, pour chaque API, la documentation de référence de l'API NDK indiquera quelque chose comme "Introduit dans l'API 30". Si ce texte n'est pas présent, cela signifie que l'API est disponible pour tous les niveaux d'API compatibles.

Éviter la répétition des protections des API

Si vous utilisez cette méthode, votre application comportera probablement des sections de code qui ne seront utilisables que sur des appareils suffisamment récents. Plutôt que de répéter la vérification __builtin_available() dans chacune de vos fonctions, vous pouvez annoter votre propre code pour qu'il nécessite un certain niveau d'API. Par exemple, les API ImageDecoder ont été ajoutées à l'API 30. Par conséquent, pour les fonctions qui font un usage intensif de ces API, vous pouvez effectuer les opérations suivantes:

#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 des API Guards

Clang est très précis dans l'utilisation de __builtin_available. Seul un if (__builtin_available(...)) littéral (bien que macro-remplacée) fonctionne. Même les opérations triviales comme if (!__builtin_available(...)) ne fonctionneront pas (Clang émettra l'avertissement unsupported-availability-guard, ainsi que unguarded-availability). Cela pourrait s'améliorer dans une prochaine version de Clang. Pour en savoir plus, consultez le problème 33161 de LLVM.

Les vérifications de unguarded-availability ne s'appliquent qu'au champ d'application de la fonction où elles sont utilisées. Clang émettra cet avertissement même si la fonction comportant l'appel d'API n'est appelée que depuis un champ d'application protégé. Pour éviter la répétition des protections dans votre code, consultez la section Éviter la répétition des protections d'API.

Pourquoi n'est-ce pas la valeur par défaut ?

À moins d'être utilisées correctement, la différence entre les références d'API fortes et les références d'API faibles est que les premières échoueront rapidement et évidemment, tandis que les autres n'échoueront pas tant que l'utilisateur n'aura pas effectué une action qui entraînera l'appel de l'API manquante. Dans ce cas, le message d'erreur "AFoo_bar() is not available" (AFoo_bar() n'est pas disponible) au moment de la compilation ne sera pas clairement affiché. Il s'agit d'une erreur de segmentation. Avec des références fortes, le message d'erreur est beaucoup plus clair, et l'échec rapide est une valeur par défaut plus sûre.

Comme il s'agit d'une nouvelle fonctionnalité, très peu de code existant est écrit pour gérer ce comportement en toute sécurité. Le code tiers qui n'a pas été écrit pour Android rencontrera probablement toujours ce problème. Il n'est donc pas prévu que le comportement par défaut ne soit jamais modifié.

Nous vous recommandons effectivement d'utiliser cette méthode, mais comme elle rendra les problèmes plus difficiles à détecter et à déboguer, vous devez accepter ces risques en connaissance de cause plutôt que de modifier le comportement à votre insu.

Mises en garde

Cette fonctionnalité est disponible pour la plupart des API, mais dans certains cas, elle ne fonctionne pas.

Les API libc plus récentes sont les moins susceptibles de poser problème. Contrairement aux autres API Android, celles-ci sont protégées par #if __ANDROID_API__ >= X dans les en-têtes et pas seulement __INTRODUCED_IN(X), ce qui empêche même la déclaration faible d'être vue. Étant donné que le niveau d'API le plus ancien compatible avec les NDK modernes est r21, les API libc les plus couramment nécessaires sont déjà disponibles. De nouvelles API libc sont ajoutées à chaque version (voir status.md). Toutefois, plus elles sont récentes, plus elles sont susceptibles de constituer un cas limite dont peu de développeurs auront besoin. Cela dit, si vous faites partie de ces développeurs, pour l'instant, vous devez continuer à utiliser dlsym() pour appeler ces API si votre minSdkVersion est antérieur à l'API. Il s'agit d'un problème pouvant être résolu, mais cela risque de rompre la compatibilité des sources pour toutes les applications (tout code contenant des polyfills d'API libc ne pourra pas être compilé en raison des incohérences des attributs availability dans les déclarations libc et locales. Par conséquent, nous ne savons pas si le problème sera résolu ni quand nous le corrigerons).

De plus en plus de développeurs sont susceptibles de rencontrer lorsque la bibliothèque contenant la nouvelle API est plus récente que votre minSdkVersion. Cette fonctionnalité n'autorise que les références de symboles faibles : il n'existe pas de référence de bibliothèque faible. Par exemple, si votre minSdkVersion est de 24, vous pouvez associer libvulkan.so et effectuer un appel protégé à vkBindBufferMemory2, car libvulkan.so est disponible sur les appareils à partir du niveau d'API 24. En revanche, si votre minSdkVersion était de 23, vous devez revenir à dlopen et dlsym, car la bibliothèque n'existe pas sur l'appareil sur les appareils qui ne sont compatibles qu'avec l'API 23. Nous ne connaissons pas de solution efficace pour résoudre ce problème, mais à long terme, elle se résoudra automatiquement, car nous n'autorisons (dans la mesure du possible) plus les nouvelles API à créer des bibliothèques.

Pour les auteurs de bibliothèques

Si vous développez une bibliothèque à utiliser dans des applications Android, évitez d'utiliser cette fonctionnalité dans vos en-têtes publics. Vous pouvez l'utiliser en toute sécurité dans le code hors ligne, mais si vous utilisez __builtin_available dans le code de vos en-têtes, comme les fonctions intégrées ou les définitions de modèle, vous forcez tous vos clients à activer cette fonctionnalité. Pour les mêmes raisons, nous n'activons pas cette fonctionnalité par défaut dans le NDK. Vous devez donc éviter de faire ce choix au nom de vos utilisateurs.

Si vous exigez ce comportement dans vos en-têtes publics, veillez à le documenter afin que vos utilisateurs sachent qu'ils devront activer la fonctionnalité et qu'ils soient conscients des risques associés.