NDK开发实践
# 20.NDK开发实践
# 目录介绍
- 一、概述
- 二、JNI与NDK基础
- 2.1 核心概念
- 2.2 JNI与NDK的关系
- 2.3 NDK使用场景
- 三、NDK架构分层
- 3.1 构建层
- 3.2 Java层
- 3.3 Native层
- 四、JNI基础语法
- 4.1 JNI三种引用
- 4.2 JNI异常处理
- 4.3 JNI签名
- 五、JNI注册方式
- 5.1 静态注册
- 5.2 动态注册
- 六、so库操作实践
- 6.1 so库生成
- 6.2 so库加载流程
- 6.3 CMake工作流程
- 七、实践案例
- 7.1 Java静态调用C/C++
- 7.2 C/C++调用Java
- 7.3 Java调用第三方so中API
- 八、JNI底层原理深入
- 8.1 JNI函数表与间接跳转
- 8.2 JNI引用管理的底层机制
- 8.3 JNI方法查找与绑定原理
- 8.4 JNI与GC的交互
- 8.5 so库加载的完整流程
- 九、JNI性能优化与内存安全
- 9.1 减少JNI调用开销
- 9.2 Native层内存管理
- 9.3 多线程与JNIEnv
- 十、常见问题与调试
- 10.1 混淆问题
- 10.2 字符串编码问题
- 10.3 Native崩溃分析
- 10.4 JNI开发最佳实践
- 十一、学习路线建议
- 十二、面试高频问题
# 一、概述
NDK(Native Development Kit)是Android提供的一套工具包,用于将C/C++代码编译为动态库(.so文件),供Java层通过JNI(Java Native Interface)调用。本文从JNI基础概念到实践案例,系统介绍NDK开发的核心知识。
# 二、JNI与NDK基础
# 2.1 核心概念
- JNI(Java Native Interface):Java本地接口,是Java与C/C++代码交互的桥梁,使Java代码可以调用native代码,native代码也可以调用Java代码
- NDK(Native Development Kit):Android工具开发包,帮助快速开发C/C++动态库,能打包生成.so文件
- .so库:shared object的缩写,是C/C++代码编译后的共享库,机器可以直接运行的二进制代码
三者的协作关系:
开发者编写:
├── Java代码(声明native方法)
└── C/C++代码(实现native方法)
编译阶段(NDK负责):
├── NDK提供交叉编译工具链(clang/clang++)
├── 将C/C++代码编译为目标平台的.so文件
├── 支持多种ABI:arm64-v8a、armeabi-v7a、x86_64等
└── 输出:libnative_lib.so
运行阶段(JNI负责):
├── Java层调用System.loadLibrary("native_lib")
├── JVM通过dlopen加载.so到进程内存
├── JNI建立Java方法与Native函数的映射关系
├── Java调用native方法时,JVM通过JNI跳转到Native代码
└── Native代码通过JNIEnv回调Java方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.2 JNI与NDK的关系
关系图:
Java层代码
↕ JNI(桥梁/代理)
C/C++代码
↕ NDK(编译工具)
.so动态库文件
2
3
4
5
6
7
JNI可以理解为代理模式,Java让JNI代其与C/C++沟通。NDK则是编译工具,类似JDK编译Java程序一样,NDK用于编译C/C++代码并打包生成.so库。
# 2.3 NDK使用场景
- 性能优化:对图形、视频、音频等计算密集型应用,将复杂模块封装在.so中处理
- 第三方库移植:使用C/C++编写的第三方库,如FFmpeg、OpenGL等
- 安全性:为提高数据安全性,封装so实现核心逻辑,增加逆向难度
# 三、NDK架构分层
NDK开发的最终目标是将C/C++代码编译生成.so动态库,并提供给Java代码调用。按架构分为三层:
# 3.1 构建层
构建层负责将C/C++代码编译为动态库.so。常用的构建工具有两种:
ndk-build(早期方式):
- 需要配合Android.mk和Application.mk两个配置文件
- 运行ndk-build相当于运行
$GNUMAKE -f <ndk>/build/core/build-local.mk
CMake(推荐方式):
- 使用CMakeLists.txt配置文件生成对应的makefile
- 优势:自动分析源代码,创建组件依赖关系树,大大缩减编译时间
- 构建过程分两步:生成makefile文件 → 编译为库或可执行文件
Android目前支持的ABI类型:
| ABI | 说明 |
|---|---|
| armeabi-v7a | 第7代及以上ARM处理器 |
| arm64-v8a | 第8代64位ARM处理器 |
| x86 | 一般用在平板、模拟器 |
| x86_64 | 64位平板 |
# 3.2 Java层
Java层通过两种方式加载so文件:
public class Test {
static {
System.load("/data/local/tmp/native_lib.so"); // 完整路径
System.loadLibrary("native_lib"); // 库名称
}
}
2
3
4
5
6
两者区别:
load加载完整路径,loadLibrary加载库名称(自动加前缀lib和后缀.so)load不会自动加载依赖库,loadLibrary会自动加载依赖库
底层流程:无论哪种方式,最终都调用LoadNativeLibrary()方法:
- 通过dlopen打开动态库文件
- 通过dlsym找到JNI_OnLoad符号对应的方法地址
- 通过JNI_OnLoad注册对应的JNI方法
# 3.3 Native层
JNI是连接Java层和Native层的桥梁。开发者可以在native层通过JNI调用Java层代码,也可以在Java层声明native方法的调用入口。
JNI核心原理:
Java代码 ←→ JVM(C/C++编写)←→ C/C++代码
↕
JNIEnv(线程执行环境)
2
3
4
5
JNIEnv:当前Java线程的执行环境,每个线程对应一个JNIEnv,保存在线程本地存储TLS中,不同线程的JNIEnv不能共享
JavaVM:Java虚拟机,一个JVM对应一个JavaVM结构体
# 四、JNI基础语法
# 4.1 JNI三种引用
| 引用类型 | 生命周期 | 跨线程 | 创建方式 |
|---|---|---|---|
| Local局部引用 | 方法结束时GC回收 | 不可 | NewLocalRef |
| Global全局引用 | 需显式释放 | 可以 | NewGlobalRef/ReleaseGlobalRef |
| Weak弱全局引用 | 内存不足时自动回收 | 可以 | NewWeakGlobalRef |
三种引用的使用场景和注意事项:
1. Local局部引用(最常用)
JNI方法中创建的对象默认都是局部引用
方法返回后自动释放(或调用DeleteLocalRef手动释放)
注意:一个JNI方法中局部引用数量有上限(默认512个)
→ 在循环中创建大量对象时必须手动释放
→ env->DeleteLocalRef(obj);
for (int i = 0; i < 10000; i++) {
jstring str = env->NewStringUTF("hello");
// 使用str...
env->DeleteLocalRef(str); // 必须手动释放!
}
2. Global全局引用(缓存常用对象)
跨多次JNI调用和跨线程使用
必须手动调用DeleteGlobalRef释放
典型场景:缓存jclass对象避免重复查找
static jclass g_clazz = NULL;
if (g_clazz == NULL) {
jclass localClazz = env->FindClass("com/example/MyClass");
g_clazz = (jclass) env->NewGlobalRef(localClazz);
env->DeleteLocalRef(localClazz);
}
3. Weak弱全局引用(可被GC回收)
类似Java的WeakReference
使用前必须检查是否被回收:env->IsSameObject(weakRef, NULL)
适用场景:缓存可选对象,内存紧张时允许回收
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 4.2 JNI异常处理
JNI中没有try...catch...throw机制,需要通过env相关接口处理异常:
JNI异常处理接口:
ExceptionOccurred() → 检查是否有待处理异常(返回异常引用)
ExceptionCheck() → 检查是否有待处理异常(返回布尔值)
ExceptionDescribe() → 输出异常及堆栈信息到System.err
ExceptionClear() → 清除当前线程待处理的异常
Throw(jthrowable) → 抛出Java异常
ThrowNew(jclass, msg) → 抛出指定类型的Java异常
FatalError(msg) → 抛出致命错误,结束进程
2
3
4
5
6
7
8
9
关键注意事项:
- JNI层抛出Java异常后,函数不会立即结束,会继续执行直到遇到return
- JNI和Java可以相互捕获并处理对方抛出的Java异常
- FatalError无法被捕获,调用后立即退出进程
# 4.3 JNI签名
Java支持函数重载,仅靠函数名无法定位具体方法,因此JNI引入了"签名"概念——将参数类型和返回值类型组合。
查看签名命令:javap -s -p MainActivity.class
# 五、JNI注册方式
# 5.1 静态注册
按照JNI规范的命名规则,格式为Java_包名_类名_方法名:
// Java层声明
public native String stringFromJNI();
2
然后看一下JNI的实现
// C++层实现
extern "C" JNIEXPORT jstring JNICALL
Java_com_yc_testjnilib_NativeLib_stringFromJNI(JNIEnv *env, jobject) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
2
3
4
5
6
# 5.2 动态注册
通过RegisterNatives方法建立映射关系,无需遵循特定命名格式:
动态注册流程:
System.loadLibrary("testjnilib")
→ 加载so文件
→ 触发Load事件
→ 查找并执行JNI_OnLoad函数
→ env->RegisterNatives(clazz, gMethods, numMethods)
→ 注册完成
2
3
4
5
6
7
8
动态注册优势:
- 方法名更简洁,修改包名类名只需调整签名信息
- 效率更高:在so加载时就建立映射,后续调用直接查找
- 减少so库导出符号数量,优化so文件体积
推荐使用动态注册,更不容易出错,灵活性更高。
# 六、so库操作实践
# 6.1 so库生成
CMake方式(推荐):
- 创建Native C++ Project项目
- 编写CMakeLists.txt配置文件
- 在build.gradle中声明externalNativeBuild
- 编译后从apk中解压获取.so文件
传统ndk-build方式:
- 在Java类中声明本地方法
- 执行javah获得.h文件
- 编写.c文件实现本地方法,配置Android.mk和Application.mk
- 执行ndk-build命令打包
# 6.2 so库加载流程
so库加载流程:
System.loadLibrary("native_lib")
→ Runtime.loadLibrary0()
→ ClassLoader.findLibrary()
→ BaseDexClassLoader.findLibrary()
→ DexPathList.findLibrary()
查找路径:
1. /data/app/${package-name}/lib/arm/ (优先)
2. /vendor/lib64
3. /system/lib64
→ nativeLoad() → JNI层加载
2
3
4
5
6
7
8
9
10
11
12
# 6.3 CMake工作流程
CMake使用CMakeLists.txt配置文件,主要配置项:
add_library:定义要编译的库find_library:查找系统库target_link_libraries:链接库依赖include_directories:指定头文件目录
# CMakeLists.txt 典型配置示例
# 设置最低CMake版本
cmake_minimum_required(VERSION 3.10.2)
# 项目名称
project("native_lib")
# 添加自己的源文件编译为共享库
add_library(
native_lib # 库名称(生成libnative_lib.so)
SHARED # 共享库(STATIC则生成.a静态库)
native_lib.cpp # 源文件列表
utils.cpp
)
# 添加头文件目录
include_directories(${CMAKE_SOURCE_DIR}/include)
# 查找Android NDK提供的系统库
find_library(log-lib log) # liblog.so(日志)
find_library(android-lib android) # libandroid.so
# 添加预编译的第三方.so库
add_library(third_party SHARED IMPORTED)
set_target_properties(third_party PROPERTIES
IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libthird.so)
# 链接所有库
target_link_libraries(
native_lib
${log-lib}
${android-lib}
third_party
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CMake在Android中的构建流程:
1. Gradle配置(build.gradle)
externalNativeBuild {
cmake {
path "CMakeLists.txt"
version "3.10.2"
}
}
ndk { abiFilters 'arm64-v8a', 'armeabi-v7a' }
2. 编译过程
CMake解析CMakeLists.txt
→ 生成Makefile(针对每个ABI)
→ 调用NDK中的clang++编译器
→ 编译.cpp → .o目标文件
→ 链接.o + 系统库 → .so共享库
→ 输出到build/intermediates/cmake/
→ 打包进APK的lib/目录
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 七、实践案例
# 7.1 Java静态调用C/C++
// Java层
public class NativeLib {
static { System.loadLibrary("native_lib"); }
public native String stringFromJNI();
}
2
3
4
5
// C++层(静态注册)
extern "C" JNIEXPORT jstring JNICALL
Java_com_yc_testjnilib_NativeLib_stringFromJNI(JNIEnv *env, jobject) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
2
3
4
5
6
# 7.2 C/C++调用Java
Native层通过JNI接口调用Java层的字段和方法:
// 1. 找到Java类
jclass clazz = env->FindClass("com/yc/testjnilib/CallNativeLib");
// 2. 获取方法ID
jmethodID methodId = env->GetMethodID(clazz, "updateName", "(Ljava/lang/String;)V");
// 3. 创建对象并调用方法
jobject obj = env->NewObject(clazz, constructorId);
jstring name = env->NewStringUTF("Hello from C++");
env->CallVoidMethod(obj, methodId, name);
2
3
4
5
6
7
8
# 7.3 Java调用第三方so中API
- 获取第三方的.so文件和.h头文件
- 在CMakeLists.txt中配置第三方库路径
- 在C++文件中导入头文件并调用第三方API
- 在Java层正常调用native方法
# 八、JNI底层原理深入
# 8.1 JNI函数表与间接跳转
JNI的核心机制是通过函数表(Function Table)实现Java与Native的双向调用。JNIEnv本质是一个指向函数指针表的二级指针:
JNIEnv的底层结构:
C语言视角:
JNIEnv* env → JNINativeInterface* → 函数指针数组
├── [0] reserved0
├── [1] reserved1
├── [2] reserved2
├── [3] reserved3
├── [4] GetVersion
├── [5] DefineClass
├── [6] FindClass
├── ...
└── [228] GetObjectRefType
C++语言视角:
JNIEnv → _JNIEnv结构体(包装了JNINativeInterface*)
└── 提供内联方法:env->FindClass()
等价于 (*env)->FindClass(env, ...)
每次JNI调用都经过函数表间接跳转:
env->FindClass("com/example/MyClass")
→ (*functions)->FindClass(env, "com/example/MyClass")
→ ART Runtime内部实现
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ART中JNI函数表的定义(art/runtime/jni/jni_internal.cc)
const JNINativeInterface gJniNativeInterface = {
nullptr, // reserved0
nullptr, // reserved1
nullptr, // reserved2
nullptr, // reserved3
JNI::GetVersion,
JNI::DefineClass,
JNI::FindClass,
// ... 224个函数指针
};
// FindClass的ART实现
static jclass FindClass(JNIEnv* env, const char* name) {
// 1. 获取当前线程
Thread* self = Thread::ForEnv(env);
// 2. 确定使用哪个ClassLoader
// 如果从Native线程调用,使用系统ClassLoader
// 如果从Java线程调用,使用调用者的ClassLoader
ClassLoader* loader = GetClassLoaderFromCallStack(self);
// 3. 通过ClassLinker加载类
ObjPtr<mirror::Class> c = ClassLinker::FindClass(self,
descriptor.c_str(), loader);
// 4. 创建全局引用返回
return AddLocalReference<jclass>(env, c);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 8.2 JNI引用管理的底层机制
JNI引用表是ART管理Java对象在Native层可达性的关键机制:
JNI引用的底层实现:
Local引用表(每个线程私有):
┌──────────────────────────────────┐
│ Thread::tlsPtr_.jni_env │
│ └── locals_ (IndirectReferenceTable)│
│ ├── table_mem_map_(mmap内存) │
│ ├── segment_state_(段状态) │
│ ├── max_entries_ = 512 │ ← 默认最多512个local引用
│ └── entries[] │
│ ├── [0] → Java对象A │
│ ├── [1] → Java对象B │
│ ├── [2] → null (已释放) │
│ └── ... │
└──────────────────────────────────┘
Global引用表(进程全局共享):
┌──────────────────────────────────┐
│ JavaVMExt::globals_ │
│ └── IndirectReferenceTable │
│ ├── 使用Mutex保护并发访问 │
│ └── 无数量上限(受内存限制) │
└──────────────────────────────────┘
WeakGlobal引用表:
┌──────────────────────────────────┐
│ JavaVMExt::weak_globals_ │
│ └── 类似Global表 │
│ └── GC时不阻止对象被回收 │
│ └── 使用前必须检查是否已被回收 │
└──────────────────────────────────┘
引用泄漏的后果:
Local引用不释放 → 引用表溢出 → JNI ERROR (local reference table overflow)
Global引用不释放 → 内存泄漏 → Java对象永远不会被GC回收
典型错误:在循环中创建大量Local引用
for (int i = 0; i < 10000; i++) {
jstring str = env->NewStringUTF("hello"); // 每次创建一个Local引用
// 如果不调用 DeleteLocalRef(env, str)
// 引用表会溢出!
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 正确的引用管理示例
void processLargeData(JNIEnv* env, jobjectArray array) {
jsize len = env->GetArrayLength(array);
for (jsize i = 0; i < len; i++) {
// 获取数组元素(创建Local引用)
jobject element = env->GetObjectArrayElement(array, i);
// 处理element...
processElement(env, element);
// 重要:手动释放Local引用,避免表溢出
env->DeleteLocalRef(element);
}
}
// 使用PushLocalFrame/PopLocalFrame批量管理引用
void batchProcess(JNIEnv* env) {
// 预留16个Local引用槽位
if (env->PushLocalFrame(16) < 0) {
return; // OutOfMemoryError
}
// 在此范围内创建的所有Local引用
jstring s1 = env->NewStringUTF("hello");
jstring s2 = env->NewStringUTF("world");
jclass cls = env->FindClass("java/lang/String");
// ... 更多Local引用
// PopLocalFrame自动释放所有Local引用
// 参数是需要保留的返回值(会被提升到上一个Frame)
jobject result = env->PopLocalFrame(s1);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 8.3 JNI方法查找与绑定原理
Native方法调用时,ART需要找到对应的C/C++函数地址。静态注册和动态注册的查找路径不同:
Native方法的绑定过程:
1. 静态注册的查找过程:
Java调用 native方法
→ ART发现方法未绑定(entry_point == null)
→ 调用 artFindNativeMethod()
→ dlsym(handle, "Java_包名_类名_方法名")
├── 先尝试短名:Java_com_example_MyClass_nativeMethod
└── 如果失败,尝试长名(含参数签名):
Java_com_example_MyClass_nativeMethod__Ljava_lang_String_2
→ 找到函数地址,绑定到ArtMethod::entry_point_
→ 后续调用直接跳转,无需再查找
2. 动态注册的绑定过程:
System.loadLibrary("mylib")
→ dlopen("libmylib.so")
→ dlsym(handle, "JNI_OnLoad")
→ 调用JNI_OnLoad(JavaVM*, void*)
→ env->RegisterNatives(clazz, methods, count)
→ 遍历JNINativeMethod数组
→ 对每个方法:
├── 通过方法名+签名找到ArtMethod
└── 设置 ArtMethod::entry_point_ = fnPtr
→ 绑定完成,后续调用直接跳转到注册的函数
3. 性能对比:
静态注册首次调用:dlsym查找(~μs级)+ 字符串匹配
动态注册首次调用:直接跳转(已在加载时绑定)
后续调用:两者相同,都是直接函数指针跳转
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 8.4 JNI与GC的交互
JNI代码执行时需要与ART的垃圾回收器协调,这涉及线程状态的切换:
JNI调用时的线程状态转换:
ART线程状态:
kRunnable → 正在执行Java代码或JNI代码
kNative → 正在执行不访问Java堆的Native代码
kSuspended → 被挂起(GC暂停)
Java → Native调用:
线程状态: kRunnable → kNative
此时GC可以并发运行(因为Native代码不直接操作Java堆)
但是:GC需要暂停所有线程时
→ 等待kNative线程回到kRunnable
→ 或在线程尝试访问JNI函数时checkpoint
Native → Java回调:
线程状态: kNative → kRunnable
此时需要检查是否有挂起的GC请求
如果有 → 当前线程被挂起直到GC完成
Critical Region(GetPrimitiveArrayCritical):
获取数组直接指针,禁止GC移动对象
必须尽快释放(ReleasePrimitiveArrayCritical)
期间不能调用其他JNI函数(可能触发GC)
// 危险用法
jint* data = env->GetPrimitiveArrayCritical(array, nullptr);
// 在这里不能调用任何可能触发GC的JNI函数!
// 不能调用NewObject、FindClass等
// 只能做纯计算操作
for (int i = 0; i < len; i++) {
data[i] *= 2; // 直接操作原始内存
}
env->ReleasePrimitiveArrayCritical(array, data, 0);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 8.5 so库加载的完整流程
System.loadLibrary的完整调用链:
Java层:
System.loadLibrary("mylib")
→ Runtime.loadLibrary0(classLoader, "mylib")
→ classLoader.findLibrary("mylib")
→ BaseDexClassLoader.findLibrary()
→ DexPathList.findLibrary()
→ 在nativeLibraryDirectories中查找
→ 返回完整路径: /data/app/.../lib/arm64/libmylib.so
Native层:
→ nativeLoad(path, classLoader)
→ JavaVMExt::LoadNativeLibrary()
│
├── 1. 检查是否已加载(避免重复加载)
│ libraries_中查找该路径
│
├── 2. dlopen打开so文件
│ → 操作系统加载so到内存
│ → 执行.init段和.init_array段的构造函数
│ → 解析符号依赖(依赖的其他so)
│
├── 3. 查找JNI_OnLoad符号
│ → dlsym(handle, "JNI_OnLoad")
│
├── 4. 如果找到JNI_OnLoad,调用它
│ → JNI_OnLoad(vm, reserved)
│ → 在此函数中可以调用RegisterNatives
│ → 返回值必须是JNI版本号(JNI_VERSION_1_6)
│
└── 5. 注册到已加载库列表
→ 后续findLibrary直接返回
so文件的内存映射:
dlopen实际是通过mmap将so文件映射到进程地址空间:
┌─────────────────────────────┐
│ .text (代码段,可执行) │ ← 函数代码
│ .rodata (只读数据) │ ← 常量字符串等
│ .data (已初始化全局变量) │
│ .bss (未初始化全局变量) │
│ .dynamic(动态链接信息) │ ← dlsym查找符号
│ .dynsym (动态符号表) │
│ .dynstr (动态字符串表) │
│ .plt/.got(过程链接表/全局偏移)│ ← 延迟绑定
└─────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 九、JNI性能优化与内存安全
# 9.1 减少JNI调用开销
JNI调用的开销分析:
每次JNI调用的隐藏开销:
├── 线程状态切换(kRunnable ↔ kNative)
├── 参数的装箱/拆箱(jobject ↔ mirror::Object)
├── 引用表操作(添加/删除Local引用)
├── 异常检查
└── 安全点(Safepoint)检查
优化策略:
1. 批量操作代替逐个操作
// 差:逐个获取数组元素
for (int i = 0; i < len; i++) {
jint val = env->GetIntArrayElement(arr, i); // 每次一次JNI调用
}
// 好:一次性获取整个数组
jint* data = env->GetIntArrayElements(arr, nullptr);
for (int i = 0; i < len; i++) {
process(data[i]); // 直接内存访问,无JNI开销
}
env->ReleaseIntArrayElements(arr, data, JNI_ABORT);
2. 缓存jclass、jmethodID、jfieldID
// 差:每次调用都查找
void callback(JNIEnv* env) {
jclass cls = env->FindClass("com/example/Callback"); // 查找开销
jmethodID mid = env->GetMethodID(cls, "onResult", "(I)V"); // 查找开销
env->CallVoidMethod(obj, mid, result);
}
// 好:在JNI_OnLoad时缓存
static jclass gCallbackClass;
static jmethodID gOnResultMethod;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
vm->GetEnv((void**)&env, JNI_VERSION_1_6);
jclass cls = env->FindClass("com/example/Callback");
gCallbackClass = (jclass)env->NewGlobalRef(cls); // 全局引用
gOnResultMethod = env->GetMethodID(cls, "onResult", "(I)V");
// jmethodID和jfieldID不是对象引用,不需要NewGlobalRef
return JNI_VERSION_1_6;
}
3. 使用DirectByteBuffer减少数据拷贝
// Native层分配内存
void* nativeBuffer = malloc(bufferSize);
// 创建DirectByteBuffer,Java层可以直接访问Native内存
jobject directBuffer = env->NewDirectByteBuffer(nativeBuffer, bufferSize);
// Java层通过ByteBuffer直接读写,无需拷贝
// ByteBuffer.get()/put()直接操作Native内存
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 9.2 Native层内存管理
JNI中的内存安全问题:
1. Native内存泄漏(不受Java GC管理)
// C/C++中malloc/new的内存不会被GC回收
// 必须手动释放
char* buffer = (char*)malloc(1024);
// ... 使用buffer
free(buffer); // 必须释放!
// C++推荐使用智能指针
std::unique_ptr<char[]> buffer(new char[1024]);
// 离开作用域自动释放
2. JNI字符串的内存管理
// GetStringUTFChars返回的字符串可能是拷贝
const char* str = env->GetStringUTFChars(jstr, nullptr);
// 使用str...
env->ReleaseStringUTFChars(jstr, str); // 必须释放!
// GetStringCritical直接获取内部指针(禁止GC)
const jchar* chars = env->GetStringCritical(jstr, nullptr);
// 快速操作,不能调用其他JNI函数
env->ReleaseStringCritical(jstr, chars);
3. 数组的内存管理
// GetArrayElements可能返回拷贝或直接指针
jint* elements = env->GetIntArrayElements(arr, &isCopy);
// isCopy == JNI_TRUE: 返回的是拷贝
// isCopy == JNI_FALSE: 返回的是直接指针
// Release时的mode参数:
// 0 : 拷贝回去并释放
// JNI_COMMIT : 拷贝回去但不释放(可继续使用)
// JNI_ABORT : 不拷贝回去,直接释放(只读场景)
env->ReleaseIntArrayElements(arr, elements, JNI_ABORT);
4. GetPrimitiveArrayCritical的风险
// 直接获取数组内存指针(零拷贝)
// 但在持有期间GC被禁止
// 如果持有时间过长,会导致:
// - GC无法执行 → 内存压力增大
// - 其他线程的内存分配被阻塞
// - 严重时导致OOM
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 9.3 多线程与JNIEnv
JNI多线程的关键规则:
1. JNIEnv是线程绑定的
// 每个线程有自己的JNIEnv
// 不能将一个线程的JNIEnv传给另一个线程使用
// 错误:跨线程使用JNIEnv
JNIEnv* savedEnv;
void jniCallback(JNIEnv* env) {
savedEnv = env; // 保存了线程A的env
}
void otherThread() {
savedEnv->CallVoidMethod(...); // 在线程B中使用 → 崩溃!
}
2. Native线程使用JNI的正确方式
// 保存JavaVM(全局唯一)
static JavaVM* gJavaVM;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
gJavaVM = vm;
return JNI_VERSION_1_6;
}
// 在Native线程中获取JNIEnv
void nativeThreadFunction() {
JNIEnv* env;
bool needDetach = false;
// 尝试获取当前线程的JNIEnv
int status = gJavaVM->GetEnv((void**)&env, JNI_VERSION_1_6);
if (status == JNI_EDETACHED) {
// 当前线程未附加到JVM,需要附加
gJavaVM->AttachCurrentThread(&env, nullptr);
needDetach = true;
}
// 使用env进行JNI调用
env->CallVoidMethod(...);
// 重要:离开前必须Detach
if (needDetach) {
gJavaVM->DetachCurrentThread();
}
}
3. 线程间传递Java对象
// Local引用不能跨线程,需要转为Global引用
static jobject gCallbackObj;
void setCallback(JNIEnv* env, jobject callback) {
// 释放旧的全局引用
if (gCallbackObj != nullptr) {
env->DeleteGlobalRef(gCallbackObj);
}
// 创建新的全局引用
gCallbackObj = env->NewGlobalRef(callback);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 十、常见问题与调试
# 10.1 混淆问题
在proguard-rules.pro中排除native方法和相关类的混淆:
-keepclasseswithmembernames class * {
native <methods>;
}
-keep class com.yc.testjnilib.** { *; }
2
3
4
混淆对JNI的影响和解决方案:
为什么native方法不能被混淆:
静态注册的JNI方法名格式:Java_包名_类名_方法名
→ 如果Java类名或方法名被混淆
→ Native层的函数名不会跟着变
→ 运行时找不到对应的Native方法
→ 抛出UnsatisfiedLinkError
动态注册不受影响的原因:
动态注册通过JNI_OnLoad中的RegisterNatives绑定
→ 方法映射在运行时建立
→ 不依赖命名规则
→ 但仍需保证Java类不被混淆(RegisterNatives需要jclass参数)
完整的混淆配置建议:
# 保持所有JNI相关类
-keepclasseswithmembernames class * { native <methods>; }
# 保持JNI中使用的回调类(Native层通过FindClass查找的类)
-keep class com.example.callback.** { *; }
# 保持JNI中使用的数据类(Native层通过GetFieldID访问的类)
-keep class com.example.model.** { *; }
# 如果使用动态注册,保持注册的类
-keep class com.example.NativeLib { *; }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 10.2 字符串编码问题
JNI中不能直接使用C字符串作为参数传递给Java方法,需要使用env->NewStringUTF()转换:
// 错误:直接使用字符串
env->CallMethod(objCallBack, _methodName, "123");
// 正确:使用NewStringUTF转换
jstring str = env->NewStringUTF("123");
env->CallMethod(objCallBack, _methodName, str);
2
3
4
5
6
需要注意的是,JNI中的Modified UTF-8与标准UTF-8有差异:
- 空字符
\0编码为0xC0 0x80(两字节),而非标准的0x00 - 补充字符(U+10000以上)使用代理对编码
- 这意味着包含
\0或emoji的字符串处理需要特别注意
# 10.3 Native崩溃分析
Native崩溃的定位流程:
1. 获取崩溃日志
adb logcat -b crash
关键信息:
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
backtrace:
#00 pc 00001234 /data/app/.../lib/arm64/libmylib.so (myFunction+16)
#01 pc 00005678 /data/app/.../lib/arm64/libmylib.so (caller+32)
2. 使用addr2line定位代码行
${NDK}/toolchains/.../bin/aarch64-linux-android-addr2line \
-e libmylib.so -f 00001234
输出:
myFunction
/path/to/source.cpp:42
3. 常见崩溃信号:
SIGSEGV (11): 访问无效内存地址(空指针、野指针、越界)
SIGBUS (7): 内存对齐错误
SIGABRT (6): 调用abort()(通常是assert失败或JNI错误)
SIGFPE (8): 算术错误(除以零)
4. AddressSanitizer (ASan) 检测内存问题
在build.gradle中启用:
android {
defaultConfig {
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared"
cFlags "-fsanitize=address -fno-omit-frame-pointer"
cppFlags "-fsanitize=address -fno-omit-frame-pointer"
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 10.4 JNI开发最佳实践
- 引用管理:及时释放局部引用,循环中使用PushLocalFrame/PopLocalFrame
- 线程安全:JNIEnv不能跨线程使用,多线程场景需通过JavaVM的AttachCurrentThread获取
- 异常处理:每个可能抛出异常的JNI调用后都应检查异常
- 性能优化:缓存频繁使用的jclass、jmethodID、jfieldID
- 内存安全:所有Get操作必须有对应的Release,使用智能指针管理Native内存
- 调试能力:编译debug版so时保留符号信息,生产环境使用strip
# 十一、学习路线建议
JNI/NDK学习路线:
入门阶段
├── 基本C/C++语法
├── 理解JNI/NDK概念和架构分层
└── 跑通Java调C/C++、C/C++调Java的基本案例
进阶阶段
├── NDK编译原理(CMake工作流程)
├── so文件打包和loadLibrary流程
├── JNI三种引用和异常处理
└── 动态注册vs静态注册
深入阶段
├── JNIEnv函数表与间接跳转机制
├── 引用表的底层结构与溢出防护
├── JNI与GC的交互(线程状态转换)
├── so文件ELF格式与dlopen/dlsym原理
└── Native崩溃分析与AddressSanitizer
实践阶段
├── 集成第三方so库(FFmpeg等)
├── JNI性能优化(缓存ID、批量操作、DirectByteBuffer)
├── 多线程JNI编程(AttachCurrentThread、全局引用)
└── Native内存泄漏检测与修复
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
建议:避免一开始就深入原理,焦点先放在JNI通信流程上,通过案例学习。边实践边琢磨问题和背后的原理。
# 十二、面试高频问题
问题:JNI中Local引用和Global引用的区别?
- Local引用线程私有,方法返回时自动释放,默认最多512个
- Global引用全局共享,需手动DeleteGlobalRef,否则内存泄漏
- Local引用不能跨线程传递,Global引用可以
问题:静态注册和动态注册的区别?
- 静态注册通过命名规则Java_包名_类名_方法名,首次调用时dlsym查找
- 动态注册在JNI_OnLoad中调用RegisterNatives,加载时即完成绑定
- 动态注册更灵活,不暴露包名类名,安全性更高
问题:为什么JNIEnv不能跨线程使用?
- JNIEnv存储在线程本地存储(TLS)中,与线程一一绑定
- 包含线程私有的Local引用表
- 跨线程使用会操作错误的引用表,导致崩溃
- 应通过JavaVM->AttachCurrentThread获取当前线程的JNIEnv
问题:GetPrimitiveArrayCritical和GetIntArrayElements的区别?
- Critical版本直接返回数组内存指针,零拷贝但禁止GC
- Elements版本可能返回拷贝,GC可以正常运行
- Critical版本持有期间不能调用其他JNI函数
- 性能敏感场景用Critical,安全性优先用Elements