本页介绍了当您的应用程序在新的操作系统版本上运行时如何使用新的操作系统功能,同时保持与旧版设备的兼容性。
默认情况下,应用程序中对 NDK API 的引用是强引用。当您的库加载时,Android 的动态加载器将积极地解析它们。如果找不到符号,应用程序将中止。这与 Java 的行为相反,Java 在调用缺失的 API 之前不会抛出异常。
因此,NDK 将阻止您创建对应用程序的 minSdkVersion
之后版本中的 API 的强引用。这可以保护您免受意外发布在测试期间有效但在旧版设备上无法加载的代码(UnsatisfiedLinkError
将从 System.loadLibrary()
抛出)的影响。另一方面,编写使用比应用程序的 minSdkVersion
更新的 API 的代码更难,因为您必须使用 dlopen()
和 dlsym()
来调用 API,而不是使用正常的函数调用。
使用强引用的替代方法是使用弱引用。库加载时未找到的弱引用会导致该符号的地址被设置为 nullptr
,而不是加载失败。它们仍然不能安全调用,但只要调用站点受到保护,以防止在 API 不可用的情况下调用 API,就可以运行您的其余代码,并且您可以像往常一样调用 API,而无需使用 dlopen()
和 dlsym()
。
弱 API 引用不需要动态链接器的额外支持,因此它们可以与任何版本的 Android 一起使用。
在您的构建中启用弱 API 引用
CMake
运行 CMake 时传递 -DANDROID_WEAK_API_DEFS=ON
。如果您通过 externalNativeBuild
使用 CMake,请将以下内容添加到您的 build.gradle.kts
(如果您仍然使用 build.gradle
,则为 Groovy 等效项)
android {
// Other config...
defaultConfig {
// Other config...
externalNativeBuild {
cmake {
arguments.add("-DANDROID_WEAK_API_DEFS=ON")
// Other config...
}
}
}
}
ndk-build
将以下内容添加到您的 Application.mk
文件中
APP_WEAK_API_DEFS := true
如果您还没有 Application.mk
文件,请在与您的 Android.mk
文件相同的目录中创建它。对于 ndk-build,无需对您的 build.gradle.kts
(或 build.gradle
)文件进行其他更改。
其他构建系统
如果您没有使用 CMake 或 ndk-build,请查阅您构建系统的文档,查看是否有推荐的方法来启用此功能。如果您的构建系统不支持此选项,您可以通过在编译时传递以下标志来启用此功能。
-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability
第一个配置 NDK 头文件以允许弱引用。第二个将不安全 API 调用的警告转换为错误。
有关更多信息,请参阅构建系统维护者指南。
受保护的 API 调用
此功能不会神奇地使对新 API 的调用变得安全。它所做的只是将加载时错误推迟到调用时错误。好处是您可以在运行时保护该调用并优雅地回退,无论是使用替代实现,还是通知用户应用程序的该功能在他们的设备上不可用,或完全避免该代码路径。
当您对应用程序 minSdkVersion
不支持的 API 进行无保护调用时,Clang 会发出警告 (unguarded-availability
)。如果您使用 ndk-build 或我们的 CMake 工具链文件,则在启用此功能时,该警告将自动启用并升级为错误。
以下是一些在未启用此功能的情况下使用 API 的条件使用示例,使用 dlopen()
和 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);
}
}
这有点难以阅读,有一些函数名重复(如果您编写的是 C 代码,签名也是如此),它将成功构建,但如果您不小心将传递给 dlsym
的函数名打错了,则在运行时始终会使用回退,并且您必须对每个 API 使用此模式。
使用弱 API 引用,上面的函数可以改写为
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, *)
调用 android_get_device_api_level()
,缓存结果,并将其与 31
进行比较(这是引入 AImageDecoder_resultToString()
的 API 级别)。
确定 __builtin_available
要使用哪个值的最简单方法是尝试在没有保护的情况下构建(或使用 __builtin_available(android 1, *)
的保护)并执行错误消息告诉您的操作。例如,对 minSdkVersion 24
的 AImageDecoder_createFromAAsset()
进行无保护调用将生成
error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]
在这种情况下,该调用应受 __builtin_available(android 30, *)
的保护。如果没有构建错误,则 API 对您的 minSdkVersion
始终可用,不需要保护,或者您的构建配置错误,并且 unguarded-availability
警告被禁用。
或者,NDK API 参考将为每个 API 说明类似“在 API 30 中引入”的内容。如果没有此文本,则表示该 API 可用于所有支持的 API 级别。
避免 API 保护重复
如果您使用此功能,应用程序中可能会有某些代码段只能在足够新的设备上使用。与其在每个函数中重复 __builtin_available()
检查,不如将您自己的代码注释为需要特定 API 级别。例如,ImageDecoder API 本身是在 API 30 中添加的,因此对于大量使用这些 API 的函数,您可以执行以下操作
#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();
}
}
API 保护的怪癖
Clang 对 __builtin_available
的使用方式非常挑剔。只有文字(可能是宏替换)if (__builtin_available(...))
才能正常工作。即使是像 if (!__builtin_available(...))
这样微不足道的操作也不起作用(Clang 将发出 unsupported-availability-guard
警告,以及 unguarded-availability
)。这可能会在 Clang 的未来版本中得到改进。有关更多信息,请参阅LLVM Issue 33161。
对 unguarded-availability
的检查仅适用于它们使用的函数范围。即使具有 API 调用的函数只在受保护的范围内调用,Clang 也会发出警告。要避免在您自己的代码中重复保护,请参阅避免 API 保护重复。
为什么这不是默认设置?
除非正确使用,强 API 引用和弱 API 引用之间的区别在于,前者会快速而明显地失败,而后者则会在用户执行导致缺少 API 被调用的操作之前不会失败。发生这种情况时,错误消息不会是明确的编译时“AFoo_bar() 不可用”错误,而是一个段错误。使用强引用,错误消息更加清晰,失败快速是更安全的默认设置。
由于这是一个新功能,很少有现有代码被编写为安全地处理此行为。并非针对 Android 编写的第三方代码可能会始终存在此问题,因此目前没有计划更改默认行为。
我们建议您使用此功能,但是由于它会使问题更难检测和调试,因此您应该明知这些风险并接受它们,而不是在您不知情的情况下更改行为。
注意事项
此功能适用于大多数 API,但有一些情况它不起作用。
最不可能出现问题的是较新的 libc API。与其他 Android API 不同,它们在头文件中使用 #if __ANDROID_API__ >= X
而不是仅仅 __INTRODUCED_IN(X)
进行保护,这阻止了甚至弱声明被看到。由于最旧的 API 级别现代 NDK 支持的是 r21,因此最常用的 libc API 已经可用。每个版本都会添加新的 libc API(请参阅status.md),但它们越新,越有可能成为很少开发人员需要使用的情况。也就是说,如果您是这些开发人员中的一员,那么目前您将需要继续使用 dlsym()
来调用这些 API(如果您的 minSdkVersion
早于 API)。这是一个可以解决的问题,但这样做会带来破坏所有应用程序的源代码兼容性的风险(任何包含 libc API polyfill 的代码将因 libc 和本地声明上的不匹配 availability
属性而无法编译),因此我们不确定我们是否或何时会修复它。
更多开发人员可能会遇到的情况是,包含新 API 的库比您的 minSdkVersion
更新。此功能仅启用弱符号引用;没有弱库引用这样的东西。例如,如果您的 minSdkVersion
为 24,您可以链接 libvulkan.so
并对 vkBindBufferMemory2
进行受保护的调用,因为 libvulkan.so
在 API 24 开始的设备上可用。另一方面,如果您的 minSdkVersion
为 23,则必须回退到 dlopen
和 dlsym
,因为该库在仅支持 API 23 的设备上的设备上不存在。我们不知道解决这种情况的良好方法,但从长远来看,它将自行解决,因为我们(尽可能)不再允许新 API 创建新库。
面向库作者
如果您正在开发要在 Android 应用程序中使用的库,则应避免在您的公共头文件中使用此功能。它可以在非内联代码中安全使用,但是如果您在头文件中的任何代码(例如内联函数或模板定义)中依赖 __builtin_available
,则会迫使所有使用者启用此功能。出于我们不默认在 NDK 中启用此功能的相同原因,您应该避免代表使用者做出这种选择。
如果您确实需要在公共头文件中使用此行为,请确保记录它,以便使用者既知道他们需要启用该功能,也知道这样做所带来的风险。