在 Android 端侧推理项目中,我们通常需要集成各种高性能计算库(如 TNN, MNN, NCNN 或 TensorFlow Lite的自定义 Delegate),这些库都以 .so 动态链接库的形式提供。管理这些 .so 文件面临两大挑战:多架构(ABI)兼容性和 库版本冲突。
解决这些问题不仅能提高应用稳定性,还能有效减小 APK 体积。
挑战一:多架构(ABI)兼容性管理
Android 设备具有不同的处理器架构(ABI)。最常见的是 armeabi-v7a(32位)和 arm64-v8a(64位)。如果 .so 文件没有正确适配所有目标架构,可能导致在特定设备上运行失败(java.lang.UnsatisfiedLinkError)。
解决方案:标准目录结构与 Gradle 过滤
Android 构建系统要求将 .so 文件放置在特定的 JNI 目录下。正确的结构如下:
app/src/main/jniLibs/
├── arm64-v8a/
│ └── libmodel.so
├── armeabi-v7a/
│ └── libmodel.so
└── ...
为了减小 APK 体积,我们应该在 build.gradle 中明确指定支持的 ABI 列表,只打包目标架构的库。
代码示例:在 build.gradle 中配置 ABI 过滤
android {
defaultConfig {
// ...
// 明确指定NDK配置,只保留主流的64位和32位ARM架构
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
// 还可以针对不同的productFlavors设置不同的ABI配置
// productFlavors {
// lite {
// ndk {
// abiFilters 'armeabi-v7a'
// }
// }
// full {
// ndk {
// abiFilters 'arm64-v8a', 'armeabi-v7a'
// }
// }
// }
}
挑战二:动态库版本冲突
版本冲突通常发生在项目中引入了多个包含相同底层依赖(如 libprotobuf.so, libstdc++.so, libopenblas.so)的第三方 AAR 或 SDK 时。Gradle 在打包时会尝试合并这些库,但如果它们版本不同,会导致运行时崩溃或行为异常。
解决方案:使用 packagingOptions 排除冲突文件
解决版本冲突最直接的方法是确保只有一个版本的共享库被打包进最终的 APK 中。我们使用 packagingOptions 在 Gradle 合并阶段排除重复文件。
实操步骤:
- 确定哪个版本(通常是主应用或主要 AI 推理框架依赖的版本)是正确的、需要保留的。
- 在 build.gradle 中,排除掉所有其他来源的冲突文件。
代码示例:在 build.gradle 中排除冲突的 .so 文件
假设我们发现项目中两个 AAR 都打包了 libprotobuf.so,并且我们希望保留由我们核心模型库提供的版本。
android {
// ...
// 解决SO文件版本冲突和重复打包问题
packagingOptions {
// 排除特定架构下的冲突库
exclude 'lib/armeabi-v7a/libprotobuf.so'
exclude 'lib/arm64-v8a/libprotobuf.so'
// 注意:如果文件路径不确定,可以使用通配符(例如排除所有lib目录下的libcommon.so)
// exclude '**/libcommon.so'
// 推荐配置:对于JNI库,避免Gradle默认的Strip操作,这可能导致库无法加载
doNotStrip '*/lib/*.so'
}
}
关键点: 当使用 packagingOptions { exclude … } 时,Gradle 会忽略在合并阶段发现的该路径下的文件。你需要确保被排除的文件路径是来自你不想要的那个 AAR/依赖包内部的路径,而不是你手动放在 jniLibs 目录下的主版本。
进阶提示:C++ 运行时库 (libstdc++) 管理
如果遇到 C++ 运行时库(如 libstdc++.so)冲突,这通常是因为不同 NDK 版本编译的库混合导致的。最佳实践是尽量使用新的 NDK 版本,并依赖 Android 系统自带的 libc++(在较新的 Android 版本中推荐)。
如果必须使用旧的 libstdc++.so,则需要确保所有的依赖库都使用同一版本的 NDK Toolchain 编译,并统一通过 packagingOptions 管理其打包。
汤不热吧