Cette page explique comment votre application peut utiliser les nouvelles fonctionnalités de l'OS 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 NDK dans votre application sont des références fortes. Le chargeur dynamique d'Android tentera de les résoudre dès que votre bibliothèque sera chargée. Si les symboles ne sont pas trouvés, l'application s'arrête. Cela est contraire au comportement de Java, où une exception n'est pas générée tant que l'API manquante n'est pas appelée.
C'est pourquoi 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 de publier accidentellement du code qui a fonctionné lors de vos tests, mais qui ne se charge pas (UnsatisfiedLinkError
est généré à partir de System.loadLibrary()
) sur les appareils plus anciens. En revanche, il est plus difficile d'écrire du code qui utilise des API plus récentes que la 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 aux références fortes est d'utiliser des références faibles. Si aucune référence faible n'est trouvée lors du chargement d'une bibliothèque, l'adresse de ce symbole est définie sur nullptr
au lieu de provoquer l'échec du chargement de la bibliothèque. Ils ne peuvent toujours pas être appelés de manière sécurisée, mais tant que les sites d'appel sont protégés pour éviter d'appeler l'API lorsqu'elle n'est pas disponible, le reste de votre code peut être exécuté et 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 du liant 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 les éléments suivants à 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 ne disposez pas encore d'un fichier Application.mk
, créez-en un dans le même répertoire que votre fichier Android.mk
. Aucune modification supplémentaire de votre fichier build.gradle.kts
(ou build.gradle
) n'est nécessaire pour ndk-build.
Autres systèmes de compilation
Si vous n'utilisez pas CMake ni ndk-build, consultez la documentation de votre système de compilation pour voir s'il existe une méthode recommandée pour activer cette fonctionnalité. Si votre système de compilation n'est pas compatible avec cette option en mode natif, vous pouvez activer la fonctionnalité en transmettant les indicateurs suivants 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. Le second transforme l'avertissement pour les appels d'API non sécurisés en erreur.
Pour en savoir plus, consultez le guide des responsables de compilation du système.
Appels d'API protégés
Cette fonctionnalité ne rend pas automatiquement les appels aux nouvelles API sécurisés. La seule chose qu'il fait est de différer une erreur de temps de chargement à une erreur de temps d'appel. L'avantage est que vous pouvez protéger cet appel au moment de l'exécution et effectuer une dégradation élégante, que ce soit en utilisant une implémentation alternative, 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 transformé en erreur lorsque vous activerez cette fonctionnalité.
Voici un exemple de code qui utilise de manière conditionnelle une API sans que cette fonctionnalité ne soit activée, à l'aide de dlopen()
et 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);
}
}
Il est un peu désordonné à lire, il y a une duplication de noms de fonction (et si vous écrivez du code C, les signatures également), la compilation sera réussie, mais le remplacement sera toujours effectué au moment de l'exécution si vous avez mal orthographié le nom de la fonction transmise à dlsym
, et 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;
}
}
Sous le capot, __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 qui a introduit AImageDecoder_resultToString()
).
Le moyen le plus simple de déterminer la valeur à utiliser pour __builtin_available
consiste à essayer de compiler sans le garde (ou un garde de __builtin_available(android 1, *)
) et à suivre les instructions du message d'erreur.
Par exemple, un appel non protégé à AImageDecoder_createFromAAsset()
avec minSdkVersion 24
génère:
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, *)
.
Si aucune erreur de compilation ne se produit, l'API est toujours disponible pour votre minSdkVersion
et aucune protection n'est nécessaire, ou votre compilation est mal configurée et l'avertissement unguarded-availability
est désactivé.
La documentation de référence de l'API NDK indique également quelque chose comme "Introduite dans l'API 30" pour chaque API. 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 d'API
Si vous utilisez cette méthode, votre application contiendra probablement des sections de code qui ne sont 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 comme nécessitant un certain niveau d'API. Par exemple, les API ImageDecoder elles-mêmes ont été ajoutées dans l'API 30. Pour les fonctions qui utilisent beaucoup ces API, vous pouvez procéder comme suit:
#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();
}
}
Particularités des protections d'API
Clang est très particulier sur la façon dont __builtin_available
est utilisé. Seul un if (__builtin_available(...))
littéral (bien que pouvant être remplacé par une macro) fonctionne. Même les opérations triviales telles que if (!__builtin_available(...))
ne fonctionneront pas (Clang émet l'avertissement unsupported-availability-guard
, ainsi que unguarded-availability
). Cela pourrait s'améliorer dans une future 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 dans laquelle elles sont utilisées. Clang émet l'avertissement même si la fonction avec l'appel d'API n'est jamais appelée qu'à partir d'un champ d'application protégé. Pour éviter la répétition des protections dans votre propre code, consultez Éviter la répétition des protections d'API.
Pourquoi ce n'est-il pas le paramètre par défaut ?
Sauf si elles sont 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 échouent rapidement et de manière évidente, tandis que les secondes ne le font que lorsque l'utilisateur effectue une action qui entraîne l'appel de l'API manquante. Dans ce cas, le message d'erreur ne sera pas une erreur claire au moment de la compilation "AFoo_bar() n'est pas disponible", mais un segfault. Avec des références fortes, le message d'erreur est beaucoup plus clair, et l'échec rapide est un paramètre par défaut plus sûr.
Comme il s'agit d'une nouvelle fonctionnalité, très peu de code existant est écrit pour gérer ce comportement de manière sécurisée. Le code tiers qui n'a pas été écrit en pensant à Android présentera probablement toujours ce problème. Il n'est donc pas prévu de modifier le comportement par défaut.
Nous vous recommandons de l'utiliser, mais comme cela rend les problèmes plus difficiles à détecter et à déboguer, vous devez accepter ces risques en connaissance de cause plutôt que de voir le comportement changer à votre insu.
Mises en garde
Cette fonctionnalité fonctionne pour la plupart des API, mais elle ne fonctionne pas dans certains cas.
Avant la version r28 du NDK, cela ne fonctionnait pas pour les API libc ou libm.
Le cas le plus susceptible d'être rencontré par les développeurs est lorsque la bibliothèque contenant la nouvelle API est plus récente que votre minSdkVersion
. Cette fonctionnalité n'active 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 24, vous pouvez associer libvulkan.so
et effectuer un appel protégé à vkBindBufferMemory2
, car libvulkan.so
est disponible sur les appareils à partir de l'API 24. D'un autre côté, si votre minSdkVersion
était 23, vous devez utiliser dlopen
et dlsym
, car la bibliothèque n'existera pas sur les appareils qui ne prennent en charge que l'API 23. Nous ne connaissons pas de bonne solution pour résoudre ce problème, mais à long terme, il se résoudra de lui-même, car nous n'autorisons plus (dans la mesure du possible) les nouvelles API à créer de nouvelles bibliothèques.
Pour les auteurs de bibliothèques
Si vous développez une bibliothèque à utiliser dans des applications Android, vous devez éviter d'utiliser cette fonctionnalité dans vos en-têtes publics. Il peut être utilisé en toute sécurité dans le code hors ligne, mais si vous vous appuyez sur __builtin_available
dans le code de vos en-têtes, tels que les fonctions intégrées ou les définitions de modèles, vous forcez tous vos consommateurs à activer cette fonctionnalité. Pour les mêmes raisons que nous n'activons pas cette fonctionnalité par défaut dans le NDK, vous devez éviter de faire ce choix au nom de vos consommateurs.
Si vous avez besoin de 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 encourus.