想象你的 Android App 是一座繁忙的城市。
- Kotlin/Java 写的业务层,是城市的办公楼:流程清晰、办事方便,适合做 UI、网络、业务逻辑。
- C/C++ 写的原生库,是城市边缘的一座重工业工坊:机器轰鸣、效率极高,擅长干“重活”——音视频编解码、图像处理、加密、数学运算、AI 推理、游戏物理。
但办公楼和工坊语言不通:
一个说“JVM 的普通话”,一个说“CPU 的方言”。
**JNI(Java Native Interface)**就是那扇“传话窗口”,而 NDK 是给你建工坊、修道路、装吊车的一整套工具。
所谓“安卓应用直接调用原生库”,本质就是:
在 APK 中打包
.so(共享库),在运行时由系统加载它,然后用 JNI 在 Kotlin/Java 与 C/C++ 之间传递数据、调用函数。
下面我们按“修路—建窗—运货—验收”的顺序,搭一套从 0 到可上线的调用体系。
一、先把概念摆平:我们说的“原生库”一般是 .so
在 Android 上,最常见的原生库形式是 ELF 共享库:
libxxx.so:共享库文件- 放在 APK 的特定目录:
lib/<abi>/libxxx.so - 由系统动态链接器加载(
dlopen机制)
1.1 ABI:同一座工坊要给不同“口音”的工人准备不同工具
Android 设备 CPU 架构不同,需要不同 ABI 的 .so:
arm64-v8a(主流)armeabi-v7a(一些老设备)x86_64(模拟器常见)x86(更老)
同一个库通常要产出多份:每个 ABI 一份 .so。
二、两条主路线:JNI(最常见) vs 直接 dlopen(高级但小众)
把“调用原生库”分成两种玩法:
2.1 路线 A:JNI(官方正统路线,99% 项目用它)
- Kotlin/Java 声明
external方法 - C/C++ 实现 JNI 函数
- Java 调用像普通函数一样调用 native 方法
优点:标准、稳定、工具链完善
缺点:需要写 JNI glue code(“胶水层”)
2.2 路线 B:dlopen/dlsym 动态加载(像自己拿钥匙开工坊门)
- 你可以在 native 层
dlopen("libxxx.so") - 用
dlsym找函数指针再调用
优点:可按需加载、插件化
缺点:更底层,容易踩 ABI/符号/安全策略坑;Java 层仍通常通过 JNI 进入 native
结论:如果你不是做插件化/热插拔,先选 JNI。
三、最常见工程做法:Kotlin/Java 调 C/C++(JNI)一步步走
3.1 先在 Java/Kotlin 写“敲门口令”:external + System.loadLibrary
就像你告诉门卫:“我一会要进工坊找某个师傅干活。”
object NativeBridge {
init {
System.loadLibrary("mylib") // 对应 libmylib.so
}
external fun add(a: Int, b: Int): Int
}
调用:
val r = NativeBridge.add(1, 2)
注意:loadLibrary 里写的是逻辑名 mylib,文件名是 libmylib.so。
3.2 再在 C/C++ 写“接线员”:JNI 函数实现
你需要一个 native 文件,例如 native-lib.cpp:
#include <jni.h>
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_app_NativeBridge_add(JNIEnv* env, jobject thiz, jint a, jint b) {
return a + b;
}
这里的方法名像一串“暗号”:
Java_包名_类名_方法名
它要与 Kotlin/Java 的声明匹配。
3.3 用 CMake 把工坊搭起来:生成 .so
在 CMakeLists.txt 里:
cmake_minimum_required(VERSION 3.22.1)
project("myapp")
add_library(mylib SHARED native-lib.cpp)
find_library(log-lib log)
target_link_libraries(mylib ${log-lib})
Gradle(Kotlin DSL 举例)里启用 externalNativeBuild:
android {
defaultConfig {
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
}
externalNativeBuild {
cmake {
cppFlags += "-std=c++17"
}
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
}
}
}
这样构建时 Gradle 会帮你产出对应 ABI 的 libmylib.so 并打进 APK。
四、另一种常见方式:你已经有第三方 .so,我只想“直接用”
有时候你拿到的是别人给的 libfoo.so(可能还带头文件 .h),你只想在 App 中调用它。
这就像工坊已经建好了,你只需要:
- 把工坊搬进城市(把
.so放进工程并打包) - 在 JNI 胶水层里“转接电话”(调用库函数)
- Java/Kotlin 通过胶水层调用
4.1 如何把现成 .so 放进 APK?
把文件放到:
app/src/main/jniLibs/<abi>/libfoo.so
例如:
app/src/main/jniLibs/arm64-v8a/libfoo.soapp/src/main/jniLibs/armeabi-v7a/libfoo.so
Gradle 默认会把 jniLibs 打包进去。
4.2 CMake 如何链接已有 .so?
在 CMake 中把它当作“导入库”:
add_library(foo SHARED IMPORTED)
set_target_properties(foo PROPERTIES
IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libfoo.so)
add_library(mylib SHARED native-lib.cpp)
target_link_libraries(mylib foo)
然后在 native-lib.cpp 里 #include "foo.h" 并调用 foo 的 API(前提是你有头文件和正确的函数声明)。
4.3 Java/Kotlin 依旧只需要 load 你的桥接库
通常你只 loadLibrary("mylib"),它会间接依赖 libfoo.so。
但如果依赖关系复杂,可能需要显式 loadLibrary("foo") 再 load mylib(视链接方式而定)。
五、数据怎么过桥?——“运货”才是 JNI 的难点
调用 native 最难的不在“能不能调用”,而在“怎么安全高效地传数据”。
把 Java/Kotlin 到 C++ 的数据传递想象成过海关:
- 你能带什么(类型支持)
- 你怎么申报(转换)
- 你带的货要不要打包(拷贝)
- 你离开时要不要把箱子带回去(释放引用)
5.1 基础类型:最省事
int↔jintlong↔jlongfloat/double↔jfloat/jdoubleboolean↔jboolean
5.2 字符串:像“需要翻译的文件”
Java 字符串是 UTF-16,C++ 常用 UTF-8 char*。JNI 提供:
GetStringUTFChars(转 UTF-8,可能拷贝)- 用完必须
ReleaseStringUTFChars
示意:
JNIEXPORT void JNICALL
Java_xxx_print(JNIEnv* env, jobject, jstring s) {
const char* c = env->GetStringUTFChars(s, nullptr);
// use c
env->ReleaseStringUTFChars(s, c);
}
5.3 数组/ByteBuffer:性能关键
如果你要传图像、音频、模型输入等大块数据,尽量用:
byte[]:简单但可能拷贝DirectByteBuffer:可零拷贝拿到指针(常用于高性能)
DirectByteBuffer 像“集装箱直达码头”:减少搬运。
六、库加载与依赖:为什么有时会崩在 UnsatisfiedLinkError?
这类错误像“门没开、钥匙不对、工坊不在”。
常见原因清单:
- ABI 不匹配:你只打了 arm64,设备是 armeabi-v7a(或反过来)
- 库名不对:loadLibrary(“mylib”) 但实际是 libMyLib.so 或没打包
- 依赖库缺失:libmylib.so 依赖 libfoo.so,但 foo 没放进 APK
- 符号找不到:链接时没把某个函数导出/名称被 C++ name mangling 改了
- Android 版本限制:某些旧 NDK API 或使用了不允许的系统私有库
排查建议:
adb logcat看具体缺哪个 so 或符号- 用
readelf -d libmylib.so看依赖项(NEEDED) - 确认 APK 内
lib/<abi>/是否真的有对应文件
七、线程与回调:原生层能不能“反过来调用 Java”?能,但要按规矩办
有时你希望 native 在某个事件发生时回调 Java,比如:
- 音频解码完一帧
- 网络库收到了数据
- 计算完成通知 UI
可以做到,但要注意:
- native 线程如果不是从 Java 进来的,需要
AttachCurrentThread才能用 JNIEnv - 回调要避免频繁跨 JNI(会有开销)
- UI 操作要切回主线程(Android 的基本法)
把它理解为:工坊里的人要打电话回办公楼,必须先去总机登记分机号。
八、为什么有些项目更喜欢用 JNA/反射来“调用原生库”?在 Android 上通常不推荐
JNA(Java Native Access)在桌面 Java 生态较常见,但在 Android 上:
- 体积与性能成本更高
- 兼容性与安全策略更复杂
- 大部分 Android 项目仍以 JNI 为主
所以如果你目标是可控、可上线、可维护:JNI 是主路。
九、发布与安全:把工坊“合法运营”的注意事项
- 只带必要 ABI:减少包体(arm64-v8a 通常必带)
- 符号与崩溃定位:保留 unstripped so 或单独保留符号文件用于 native 崩溃解析
- 混淆与安全:Java 层 Proguard/R8 + native 层符号剥离(但要留调试符号映射)
- 第三方库许可证:很多
.so涉及开源协议,需合规 - 隐私合规:原生库如果访问设备信息/权限,依旧受 Android 隐私政策约束
十、把整个流程一句话串起来(记忆版)
Kotlin/Java 用
System.loadLibrary把.so载入内存 → 用external声明门口的“窗口” → C/C++ 用 JNI 实现这个窗口并在里面调用真正的原生库函数 → 通过 CMake/Gradle 把各 ABI 的.so打包进 APK → 运行时按 ABI 自动选对库加载执行。

2万+

被折叠的 条评论
为什么被折叠?



