本页介绍了如何让应用在运行新版操作系统时使用新操作系统功能,同时保持与旧版设备的兼容性。
默认情况下,应用中对 NDK API 的引用是强引用。当您的库加载后,Android 的动态加载器会迫切地解析它们。如果未找到这些符号,应用将中止。这与 Java 的行为方式相反,在 Java 中,除非调用缺失的 API,否则不会抛出异常。
因此,NDK 会阻止您创建对比应用的 minSdkVersion
更高版本的 API 的强引用。这可防止您不小心交付在测试期间有效但在旧设备上无法加载(System.loadLibrary()
将抛出 UnsatisfiedLinkError
)的代码。另一方面,编写使用比应用的 minSdkVersion
更高版本的 API 的代码会更困难,因为您必须使用 dlopen()
和 dlsym()
调用 API,而不是使用常规函数调用。
使用强引用的替代方案是使用弱引用。如果在库加载时未找到弱引用,则会导致该符号的地址被设置为 nullptr
,而不是加载失败。它们仍然无法安全调用,但只要调用点受到保护,以防止在 API 不可用时调用 API,则可以运行其余代码,并且您可以正常调用 API,而无需使用 dlopen()
和 dlsym()
。
弱 API 引用不需要动态链接器的额外支持,因此可与任何版本的 Android 搭配使用。
在 build 中启用弱 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 工具链文件,系统会自动启用该警告,并在启用此功能时提升为错误。
以下示例代码使用 dlopen()
和 dlsym()
,在未启用此功能的情况下有条件地使用 API:
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
使用,且无需任何守卫;或者您的 build 配置错误,并且 unguarded-availability
警告已停用。
或者,对于每个 API,NDK API 参考文档中都会显示“Introduced in API 30”(在 API 30 中引入)之类的文字。如果没有该文本,则表示该 API 适用于所有受支持的 API 级别。
避免重复使用 API 守卫
如果您使用此方法,应用中可能有一些代码段仅适用于足够新的设备。您可以为自己的代码添加注解,指明其需要特定的 API 级别,而不是在每个函数中重复执行 __builtin_available()
检查。例如,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 问题 33161。
对 unguarded-availability
的检查仅适用于其使用的函数作用域。即使只从受保护的作用域内调用通过 API 调用的函数,Clang 也会发出该警告。如需避免在自己的代码中重复执行保护程序,请参阅避免重复 API 防护程序。
为什么这不是默认设置?
除非使用得当,否则强引用 API 和弱引用 API 之间的区别在于,前者会快速且明显地失败,而后者在用户执行导致调用缺失 API 的操作之前不会失败。在这种情况下,错误消息不会是明确的编译时“AFoo_bar() 不可用”错误,而是会出现段错误。使用强引用后,错误消息会更加清晰,并且快速失败是一种更安全的默认设置。
由于这是一项新功能,因此很少编写现有代码来安全地处理此行为。未以 Android 为目标平台编写的第三方代码可能永远都会存在此问题,因此目前没有任何更改默认行为的计划。
我们确实建议您使用此方法,但由于这会使问题更难检测和调试,因此您应有意识地接受这些风险,而不是在不知情的情况下让行为发生变化。
注意事项
此功能适用于大多数 API,但在少数情况下不适用。
最不太可能出现问题的是较新的 libc API。与其他 Android API 不同,这些 API 在头文件中使用 #if __ANDROID_API__ >= X
(而不仅仅是 __INTRODUCED_IN(X)
)进行保护,这会阻止其他人看到弱声明。由于新版 NDK 支持的最旧 API 级别为 r21,因此最常用的 libc API 已经可用。每个版本都会添加新的 libc API(请参阅 status.md),但这些 API 越新,就越有可能成为很少有开发者需要的极端情况。不过,如果您是其中一位开发者,那么目前,如果您的 minSdkVersion
版本低于相应 API 的版本,您需要继续使用 dlsym()
调用这些 API。这是一个可以解决的问题,但这样做可能会破坏所有应用的源代码兼容性(由于 libc 和本地声明上的 availability
属性不匹配,任何包含 libc API 的 polyfill 的代码都将无法编译,因此我们不确定是否或何时修复此问题。
更多开发者可能会遇到的情况是,包含新 API 的库版本比您的 minSdkVersion
版本更高。此功能仅启用弱符号引用;不存在弱库引用。例如,如果您的 minSdkVersion
为 24,则可以链接 libvulkan.so
并对 vkBindBufferMemory2
进行受保护调用,因为 libvulkan.so
适用于 API 24 及更高级别的设备。另一方面,如果 minSdkVersion
为 23,您必须回退到 dlopen
和 dlsym
,因为仅支持 API 23 的设备上不会存在该库。我们不知道如何解决此问题,但从长远来看,此问题会自行解决,因为我们(尽可能)不再允许新 API 创建新库。
对于库作者
如果您正在开发要在 Android 应用中使用的库,则应避免在公共头文件中使用此功能。它可以安全地用在行外代码中,但如果您在标头中的任何代码(例如内联函数或模板定义)中依赖于 __builtin_available
,则必须强制所有使用者启用此功能。出于同样的原因,我们在 NDK 中默认不启用此功能,因此您应避免代表使用方做出此选择。
如果您确实需要在公共头文件中实现此行为,请务必记录这一点,以便用户知道他们需要启用此功能,并了解这样做的风险。