针对中间件供应商的建议

分发使用 NDK 构建的中间件会引发其他问题,但应用开发者无需对此感到担心。预构建库会为用户提供一些实现选项。

选择 API 级别和 NDK 版本

您的用户无法使用低于您的级别的 minSdkVersion。如果用户的应用需要在 API 21 上运行,您就无法针对 API 24 构建应用。您可以针对低于用户级别的 API 级别构建库。您可以针对 API 16 构建应用,并保持与 API 21 用户兼容。

各个 NDK 版本大部分是相互兼容的,但版本间的变更偶尔也会破坏兼容性。如果您确定您的所有用户使用的都是相同版本的 NDK,那么您最好也使用与用户相同的版本。否则,请使用最新版本。

使用 STL

如果您正在编写 C++ 和使用 STL,并且要分发共享库,那么您在 libc++_sharedlibc++_static 之间的选择会对您的用户产生影响。如果您要分发共享库,则必须使用 libc++_shared,或确保共享库不会公开 libc++ 的符号。要实现此目标,最好的方法是通过版本脚本明确声明您的 ABI 界面(这也有助于将实现细节保持私有状态)。例如,一个简单的算术库可能拥有以下版本脚本:

LIBMYMATH {
global:
    add;
    sub;
    mul;
    div;
    # C++ symbols in an extern block will be mangled automatically. See
    # https://stackoverflow.com/a/21845178/632035 for more examples.
    extern "C++" {
        "pow(int, int)";
    }
local:
    *;
};

首选方式应为版本脚本,因为这是控制符号可见性的最可靠方式。这对于所有共享库(无论是否是中间件)来说都是最佳做法,因为这可以防止您的实现细节被公开,并能够缩短加载时间。

另一个可靠性较低的方式是在关联时使用 -Wl,--exclude-libs,libc++_static.a -Wl,--exclude-libs,libc++abi.a。这种方式的可靠性较低是因为它只会隐藏库中明确指定的符号,并且不会为未使用的库报告诊断信息(库名称中的拼写错误不属于错误,并且用户应负责及时更新库列表)。此方式也不会隐藏您自己的实现细节。

在 AAR 中分发原生库

Android Gradle 插件可以导入在 AAR 中分发的原生依赖项。如果您的用户使用的是 Android Gradle 插件,这将是他们使用您的库的最简单的方式。

原生库可以通过 AGP 打包到 AAR 中。如果您的库已经通过 externalNativeBuild 构建,这便是最简单的方法。

非 AGP build 可以使用 ndkports,或按照 Prefab 文档中的说明执行手动打包,以便创建 AAR 的 prefab/ 子目录。

包含 JNI 库的 Java 中间件

包含 JNI 库(即包含 jniLibs 的 AAR)的 Java 库需要小心谨慎地处理,这样它们包含的 JNI 库才不会与用户应用中的其他库发生冲突。例如,如果 AAR 包含的 libc++_shared.so 版本不同于应用使用的 libc++_shared.so 版本,那么只有其中一个版本会安装到 APK,并且这可能会引发不可靠的行为。

最可靠的解决方案是确保 Java 库中的 JNI 库不超过一个(这个建议也适用于应用)。包括 STL 在内的所有依赖项都应静态关联到实现库,并且应通过版本脚本强制执行 ABI 界面。例如,包含 JNI 库 libfooimpl.so 的 Java 库 com.example.foo 应使用以下版本脚本:

LIBFOOIMPL {
global:
    JNI_OnLoad;
local:
    *;
};

本示例按照 JNI 提示中所述的说明,在 JNI_OnLoad 中使用 registerNatives 来确保尽量少公开 ABI 界面,并最大限度缩短库的加载时间。