编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

    • 库的解读

    • 专栏博客

      • 系统启动Zygote
      • Binder通信原理
      • Handler消息机制
      • Activity启动原理
      • 四大组件原理分析
      • AMS与组件管理
      • View绑制与渲染
      • 事件分发机制
      • Surface渲染原理
      • 自定义View设计
      • WMS窗口管理
      • PMS与APK安装
      • 虚拟机与类加载
      • 内存管理与GC
      • 线程与并发编程
      • 性能优化与监控
      • 序列化与数据存储
      • 组件化与路由设计
      • 插件化与热修复
      • 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开发最佳实践
        • 十一、学习路线建议
        • 十二、面试高频问题
      • WebView核心设计
      • ADB常见使用操作
    • 智能硬件

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

  • Apps
  • Android提升进阶
  • 专栏博客
杨充
2026-04-14
目录

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方法
1
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动态库文件
1
2
3
4
5
6
7

JNI可以理解为代理模式,Java让JNI代其与C/C++沟通。NDK则是编译工具,类似JDK编译Java程序一样,NDK用于编译C/C++代码并打包生成.so库。

# 2.3 NDK使用场景

  1. 性能优化:对图形、视频、音频等计算密集型应用,将复杂模块封装在.so中处理
  2. 第三方库移植:使用C/C++编写的第三方库,如FFmpeg、OpenGL等
  3. 安全性:为提高数据安全性,封装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");                 // 库名称
    }
}
1
2
3
4
5
6

两者区别:

  • load加载完整路径,loadLibrary加载库名称(自动加前缀lib和后缀.so)
  • load不会自动加载依赖库,loadLibrary会自动加载依赖库

底层流程:无论哪种方式,最终都调用LoadNativeLibrary()方法:

  1. 通过dlopen打开动态库文件
  2. 通过dlsym找到JNI_OnLoad符号对应的方法地址
  3. 通过JNI_OnLoad注册对应的JNI方法

# 3.3 Native层

JNI是连接Java层和Native层的桥梁。开发者可以在native层通过JNI调用Java层代码,也可以在Java层声明native方法的调用入口。

JNI核心原理:

Java代码 ←→ JVM(C/C++编写)←→ C/C++代码
                ↕
           JNIEnv(线程执行环境)
1
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)
   适用场景:缓存可选对象,内存紧张时允许回收
1
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)       → 抛出致命错误,结束进程
1
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();
1
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());
}
1
2
3
4
5
6

# 5.2 动态注册

通过RegisterNatives方法建立映射关系,无需遵循特定命名格式:

动态注册流程:

System.loadLibrary("testjnilib")
→ 加载so文件
→ 触发Load事件
→ 查找并执行JNI_OnLoad函数
→ env->RegisterNatives(clazz, gMethods, numMethods)
→ 注册完成
1
2
3
4
5
6
7
8

动态注册优势:

  • 方法名更简洁,修改包名类名只需调整签名信息
  • 效率更高:在so加载时就建立映射,后续调用直接查找
  • 减少so库导出符号数量,优化so文件体积

推荐使用动态注册,更不容易出错,灵活性更高。


# 六、so库操作实践

# 6.1 so库生成

CMake方式(推荐):

  1. 创建Native C++ Project项目
  2. 编写CMakeLists.txt配置文件
  3. 在build.gradle中声明externalNativeBuild
  4. 编译后从apk中解压获取.so文件

传统ndk-build方式:

  1. 在Java类中声明本地方法
  2. 执行javah获得.h文件
  3. 编写.c文件实现本地方法,配置Android.mk和Application.mk
  4. 执行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层加载
1
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
)
1
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/目录
1
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();
}
1
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());
}
1
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);
1
2
3
4
5
6
7
8

# 7.3 Java调用第三方so中API

  1. 获取第三方的.so文件和.h头文件
  2. 在CMakeLists.txt中配置第三方库路径
  3. 在C++文件中导入头文件并调用第三方API
  4. 在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内部实现
1
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);
}
1
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)
      // 引用表会溢出!
  }
1
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);
}
1
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级)+ 字符串匹配
   动态注册首次调用:直接跳转(已在加载时绑定)
   后续调用:两者相同,都是直接函数指针跳转
1
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);
1
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(过程链接表/全局偏移)│ ← 延迟绑定
  └─────────────────────────────┘
1
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内存
1
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
1
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);
   }
1
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.** { *; }
1
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 { *; }
1
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);
1
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"
               }
           }
       }
   }
1
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开发最佳实践

  1. 引用管理:及时释放局部引用,循环中使用PushLocalFrame/PopLocalFrame
  2. 线程安全:JNIEnv不能跨线程使用,多线程场景需通过JavaVM的AttachCurrentThread获取
  3. 异常处理:每个可能抛出异常的JNI调用后都应检查异常
  4. 性能优化:缓存频繁使用的jclass、jmethodID、jfieldID
  5. 内存安全:所有Get操作必须有对应的Release,使用智能指针管理Native内存
  6. 调试能力:编译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内存泄漏检测与修复
1
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
上次更新: 2026/06/10, 11:13:41
插件化与热修复
WebView核心设计

← 插件化与热修复 WebView核心设计→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式