目录
  1. 1. 一、JNI 在 Android 中的角色
  2. 2. 二、JNI 数据类型映射
  3. 3. 三、局部引用与全局引用:引用表溢出
    1. 3.1. 3.1 三种引用类型
    2. 3.2. 3.2 局部引用表溢出
    3. 3.3. 3.3 全局引用
  4. 4. 四、JNI_OnLoad 与动态注册
    1. 4.1. 4.1 静态注册(基于方法名约定)
    2. 4.2. 4.2 动态注册(通过 JNI_OnLoad)
  5. 5. 五、从 Native 调用 Java 方法
  6. 6. 六、Native 线程与 JavaVM
  7. 7. 七、Java 异常的 Native 处理
  8. 8. 八、完整实践:Native 加密实现
  9. 9. 九、面试常问题目
【C/C++理论实战技术】JNI基础实战

一、JNI 在 Android 中的角色

JNI(Java Native Interface)是 Java 虚拟机规范的一部分,定义了 Java 代码与 C/C++ 原生代码之间互操作的接口。在 Android 中,JNI 是连接 Java 世界(Android Framework、App 代码)和 Native 世界(C/C++ 库、硬件驱动、NDK 模块)的桥梁。

Android 系统本身大量使用 JNI:Framework 层通过 JNI 调用 HAL(硬件抽象层)实现对传感器、摄像头、音频等硬件的访问。Zygote 进程在 fork 每个应用进程时,会预先加载 JNI 库以减少应用启动时的开销。

JNI 在 Android 中的典型使用场景:

  • 性能敏感:图像处理、音视频编解码、加密算法。
  • 代码复用:移植已有的 C/C++ 库(如 ffmpeg、OpenSSL、SDL)。
  • 安全需求:将加解密、反作弊等关键逻辑放在 native 层增加逆向难度。
  • 系统调用:访问 Linux 底层 API(epoll、mmap、ptrace 等)。

二、JNI 数据类型映射

JNI 定义了一组与 Java 类型对应的 C/C++ 类型:

Java 类型 JNI 类型 C/C++ 类型 描述
boolean jboolean unsigned char (8-bit) 0 = false, nonzero = true
byte jbyte signed char (8-bit) -128 to 127
char jchar unsigned short (16-bit) UTF-16 code unit
short jshort signed short (16-bit)
int jint signed int (32-bit)
long jlong signed long long (64-bit)
float jfloat float (32-bit IEEE 754)
double jdouble double (64-bit IEEE 754)
void void N/A
Object jobject pointer 所有 Java 对象的基类型
String jstring pointer Java String 对象
Class jclass pointer Java Class 对象
Throwable jthrowable pointer Java 异常对象
boolean[] jbooleanArray pointer
int[] jintArray pointer

引用类型是一个指针(在 ART 运行时中指向堆上的对象),不能直接当作 C 指针使用。访问 Java 数组中的数据需要通过 JNI 提供的 Get/Release 函数。

// 访问 Java int[] 数组
JNIEXPORT void JNICALL Java_com_example_NativeLib_processArray(
JNIEnv *env, jobject thiz, jintArray array) {

// 获取数组长度
jsize len = (*env)->GetArrayLength(env, array);

// 获取数组元素的指针(可能是拷贝,取决于 VM 实现)
jint *elements = (*env)->GetIntArrayElements(env, array, NULL);
if (elements == NULL) {
return; // OutOfMemoryError 已经抛出
}

// 处理数组
for (int i = 0; i < len; i++) {
elements[i] *= 2;
}

// 释放数组元素(mode: 0=回写并释放, JNI_COMMIT=回写不释放, JNI_ABORT=不回写并释放)
(*env)->ReleaseIntArrayElements(env, array, elements, 0);
}

GetPrimitiveArrayCritical 提供了更直接的访问方式(可能阻止 GC),配对使用的是 ReleasePrimitiveArrayCritical。Critical 版本与 Get/Release 版本的区别是:Critical 期间 GC 被暂停(对性能敏感的代码慎用),且不能在这两个调用之间调用其他 JNI 函数或阻塞。

三、局部引用与全局引用:引用表溢出

JNI 的引用管理是面试中的高频考点,也是 native 内存泄漏的常见来源。

3.1 三种引用类型

引用类型 创建方式 生命周期 使用场景
局部引用(Local Reference) 大多数 JNI 函数自动创建 当前 native 方法返回时自动释放 临时使用的 Java 对象
全局引用(Global Reference) NewGlobalRef() 显式调用 DeleteGlobalRef() 释放 跨多个 native 调用共享的对象
弱全局引用(Weak Global Reference) NewWeakGlobalRef() 显式调用 DeleteWeakGlobalRef() 释放 可能被 GC 回收的对象引用

3.2 局部引用表溢出

每个 native 线程有一个局部引用表,默认容量通常是 512(ART 运行时)。如果在一个 native 方法中创建了大量局部引用而不释放,会抛出 JNI ERROR (app bug): local reference table overflow

// BAD: 局部引用在循环中累积,可能溢出
JNIEXPORT void JNICALL Java_com_example_NativeLib_processList(
JNIEnv *env, jobject thiz, jobjectArray strings) {
jsize len = (*env)->GetArrayLength(env, strings);
for (int i = 0; i < len; i++) {
jstring str = (jstring)(*env)->GetObjectArrayElement(env, strings, i);
// str 是一个局部引用,每次循环创建一个新的
// 如果 len > 512,就会溢出!
const char *utf = (*env)->GetStringUTFChars(env, str, NULL);
// 处理 utf...
(*env)->ReleaseStringUTFChars(env, str, utf);
// BUG FIX: 需要手动释放局部引用
// (*env)->DeleteLocalRef(env, str);
}
}

正确的处理方式:

// GOOD: 及时删除局部引用,或使用 Push/PopLocalFrame
JNIEXPORT void JNICALL Java_com_example_NativeLib_processList(
JNIEnv *env, jobject thiz, jobjectArray strings) {
jsize len = (*env)->GetArrayLength(env, strings);
for (int i = 0; i < len; i++) {
jstring str = (jstring)(*env)->GetObjectArrayElement(env, strings, i);
const char *utf = (*env)->GetStringUTFChars(env, str, NULL);
process(utf);
(*env)->ReleaseStringUTFChars(env, str, utf);
(*env)->DeleteLocalRef(env, str); // 关键:手动删除局部引用
}
}

// 或者使用 PushLocalFrame / PopLocalFrame
JNIEXPORT void JNICALL Java_com_example_NativeLib_processList2(
JNIEnv *env, jobject thiz, jobjectArray strings) {
jsize len = (*env)->GetArrayLength(env, strings);
for (int i = 0; i < len; i++) {
// Push 一个局部帧——帧内的所有局部引用会在这个帧中创建
if ((*env)->PushLocalFrame(env, 16) < 0) {
return; // OutOfMemoryError
}
jstring str = (jstring)(*env)->GetObjectArrayElement(env, strings, i);
const char *utf = (*env)->GetStringUTFChars(env, str, NULL);
process(utf);
(*env)->ReleaseStringUTFChars(env, str, utf);
// Pop 时自动释放帧内所有局部引用
(*env)->PopLocalFrame(env, NULL);
}
}

3.3 全局引用

// 缓存一个 Java 对象供多个 native 调用使用
static jobject g_cached_object = NULL; // 全局变量

JNIEXPORT void JNICALL Java_com_example_NativeLib_init(JNIEnv *env, jobject thiz, jobject obj) {
// 创建全局引用(必须显式删除)
g_cached_object = (*env)->NewGlobalRef(env, obj);
}

JNIEXPORT void JNICALL Java_com_example_NativeLib_cleanup(JNIEnv *env, jobject thiz) {
if (g_cached_object != NULL) {
(*env)->DeleteGlobalRef(env, g_cached_object);
g_cached_object = NULL;
}
}

四、JNI_OnLoad 与动态注册

有两种注册 native 方法的方式:

4.1 静态注册(基于方法名约定)

// 方法名格式:Java_<package>_<Class>_<method>
// 必须包含完整的包名(用下划线替换点)
JNIEXPORT jstring JNICALL Java_com_example_NativeLib_getMessage(JNIEnv *env, jobject thiz) {
return (*env)->NewStringUTF(env, "Hello from JNI");
}

缺点:方法名太长,容易写错,且每次调用 native 方法时 VM 需要搜索符号表。

4.2 动态注册(通过 JNI_OnLoad)

// native 方法实现
static jstring native_getMessage(JNIEnv *env, jobject thiz) {
return (*env)->NewStringUTF(env, "Hello from JNI");
}

static jint native_add(JNIEnv *env, jobject thiz, jint a, jint b) {
return a + b;
}

// 方法注册表
static const JNINativeMethod gMethods[] = {
{"getMessage", "()Ljava/lang/String;", (void *)native_getMessage},
{"add", "(II)I", (void *)native_add},
};

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}

jclass clazz = (*env)->FindClass(env, "com/example/NativeLib");
if (clazz == NULL) {
return JNI_ERR;
}

if ((*env)->RegisterNatives(env, clazz, gMethods,
sizeof(gMethods) / sizeof(gMethods[0])) < 0) {
return JNI_ERR;
}

return JNI_VERSION_1_6; // 返回使用的 JNI 版本
}

动态注册的优势:

  1. 方法名更简洁,不需要长命名。
  2. 首次加载时批量注册,后续调用查找快。
  3. 可以在 JNI_OnLoad 中做初始化工作(如缓存 MethodID)。

JNI 方法签名规则:

  • (参数类型签名)返回值类型签名
  • I = int, J = long, F = float, D = double, Z = boolean, V = void
  • Ljava/lang/String; = String 对象
  • [I = int[]

五、从 Native 调用 Java 方法

JNI 允许 native 代码调用 Java 方法。关键 API:GetMethodID/GetStaticMethodID + CallVoidMethod/CallStaticMethod 等。

// Native 代码调用 Java 对象的 void onResult(String result) 方法
static void callJavaCallback(JNIEnv *env, jobject callback_obj, const char *result) {
// 1. 获取 Java 类
jclass clazz = (*env)->GetObjectClass(env, callback_obj);

// 2. 获取方法 ID(GetMethodID 开销较大,应在初始化时缓存)
jmethodID methodId = (*env)->GetMethodID(env, clazz, "onResult", "(Ljava/lang/String;)V");

// 3. 创建 Java String
jstring jResult = (*env)->NewStringUTF(env, result);

// 4. 调用方法
(*env)->CallVoidMethod(env, callback_obj, methodId, jResult);

// 5. 释放局部引用
(*env)->DeleteLocalRef(env, jResult);
(*env)->DeleteLocalRef(env, clazz);
}

性能优化要点FindClassGetMethodIDGetFieldID 的调用开销较大(需要 JNI 内部字符串比较和查找),应该在 JNI_OnLoad 中一次性获取并缓存为全局引用:

static jclass g_callbackClass = NULL;
static jmethodID g_onResultMethod = NULL;

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
(*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6);
jclass localClass = (*env)->FindClass(env, "com/example/Callback");
g_callbackClass = (*env)->NewGlobalRef(env, localClass); // 全局缓存
g_onResultMethod = (*env)->GetMethodID(env, g_callbackClass,
"onResult", "(Ljava/lang/String;)V");
(*env)->DeleteLocalRef(env, localClass);
return JNI_VERSION_1_6;
}

六、Native 线程与 JavaVM

从 native 创建的线程(如通过 pthread 或 std::thread),JNIEnv 是线程局部存储的。新线程不能直接使用从其他线程传入的 JNIEnv 指针。必须调用 JavaVM::AttachCurrentThread() 获取当前线程的 JNIEnv:

static JavaVM *g_jvm = NULL;  // 在 JNI_OnLoad 中缓存

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
g_jvm = vm;
return JNI_VERSION_1_6;
}

// 在 native 线程中
void *thread_func(void *arg) {
JNIEnv *env;
// 将当前线程附加到 JVM
jint result = (*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL);
if (result != JNI_OK) {
return NULL; // 附加失败
}

// 现在可以安全使用 env 调用 JNI 函数
// ... 执行 JNI 操作 ...

// 线程结束时必须 detach(否则线程局部引用无法释放)
(*g_jvm)->DetachCurrentThread(g_jvm);
return NULL;
}

注意事项:

  • AttachCurrentThread 必须成对出现 DetachCurrentThread,否则线程退出后局部引用表无法被清理。
  • 如果线程已经被 attached,再次调用 AttachCurrentThread 是安全的(no-op)。

七、Java 异常的 Native 处理

JNI 函数出错时,大多数不会通过返回值表示,而是在 Java 层设置异常。native 代码必须显式检查和清理这些异常:

JNIEXPORT void JNICALL Java_com_example_NativeLib_safeCall(JNIEnv *env, jobject thiz, jobject obj) {
jclass clazz = (*env)->GetObjectClass(env, obj);
jmethodID methodId = (*env)->GetMethodID(env, clazz, "nonExistentMethod", "()V");

// 检查是否有异常发生(方法不存在会抛出 NoSuchMethodError)
if ((*env)->ExceptionCheck(env)) {
// 打印异常栈(调试用)
(*env)->ExceptionDescribe(env);
// 清除异常——否则后续 JNI 调用都会失败
(*env)->ExceptionClear(env);
// 执行 fallback 逻辑或抛出自己的异常
jclass npeClass = (*env)->FindClass(env, "java/lang/NullPointerException");
(*env)->ThrowNew(env, npeClass, "Callback method not found");
return;
}

(*env)->CallVoidMethod(env, obj, methodId);
}

关键规则:大多数 JNI 函数在异常挂起时如果继续调用会失败(甚至崩溃)。通过 ExceptionCheck()ExceptionOccurred() 检测异常,然后 ExceptionClear() 清除或 ThrowNew() 抛出新异常。

八、完整实践:Native 加密实现

// 一个完整的 JNI 模块:使用 OpenSSL 进行 AES-CBC 加密
// CMakeLists.txt
// find_library(log-lib log)
// target_link_libraries(native-lib ${log-lib} ${CMAKE_DL_LIBS})

#include <jni.h>
#include <string.h>
#include <openssl/aes.h>

static jbyteArray native_encrypt(JNIEnv *env, jobject thiz,
jbyteArray data, jbyteArray key) {
// 获取输入数据
jsize dataLen = (*env)->GetArrayLength(env, data);
jsize keyLen = (*env)->GetArrayLength(env, key);

if (keyLen != AES_BLOCK_SIZE) {
jclass exClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException");
(*env)->ThrowNew(env, exClass, "Key must be 16 bytes (AES-128)");
return NULL;
}

jbyte *dataBytes = (*env)->GetByteArrayElements(env, data, NULL);
jbyte *keyBytes = (*env)->GetByteArrayElements(env, key, NULL);

// PKCS7 填充
jsize paddedLen = ((dataLen / AES_BLOCK_SIZE) + 1) * AES_BLOCK_SIZE;
unsigned char *paddedData = malloc(paddedLen);
memcpy(paddedData, (unsigned char *)dataBytes, dataLen);
int padValue = paddedLen - dataLen;
memset(paddedData + dataLen, padValue, padValue);

// AES-CBC 加密
AES_KEY aesKey;
AES_set_encrypt_key((const unsigned char *)keyBytes, 128, &aesKey);

unsigned char iv[AES_BLOCK_SIZE] = {0}; // 实际应用中应随机生成
unsigned char *ciphertext = malloc(paddedLen);
AES_cbc_encrypt(paddedData, ciphertext, paddedLen, &aesKey, iv, AES_ENCRYPT);

// 构造返回结果
jbyteArray result = (*env)->NewByteArray(env, paddedLen);
(*env)->SetByteArrayRegion(env, result, 0, paddedLen, (jbyte *)ciphertext);

// 清理
free(ciphertext);
free(paddedData);
(*env)->ReleaseByteArrayElements(env, key, keyBytes, JNI_ABORT);
(*env)->ReleaseByteArrayElements(env, data, dataBytes, JNI_ABORT);

return result;
}

static const JNINativeMethod gMethods[] = {
{"encrypt", "([B[B)[B", (void *)native_encrypt},
};

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
(*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6);
jclass clazz = (*env)->FindClass(env, "com/example/crypto/NativeCrypto");
(*env)->RegisterNatives(env, clazz, gMethods,
sizeof(gMethods) / sizeof(gMethods[0]));
return JNI_VERSION_1_6;
}

九、面试常问题目

Q1: 局部引用和全局引用的区别?什么情况下会导致 local reference table overflow?

局部引用在 native 方法返回时自动释放,全局引用需要显式 DeleteGlobalRef() 才能释放。在循环中创建大量局部引用(如遍历大数组时每次都 GetObjectArrayElement)而不手动 DeleteLocalRef,或在循环内不对引用进行释放,会导致局部引用表(默认 512 容量)溢出。解决方案是使用 PushLocalFrame/PopLocalFrame 或在循环内 DeleteLocalRef。

Q2: JNI_OnLoad 的作用是什么?什么时候需要实现它?

JNI_OnLoad 在 System.loadLibrary() 加载 .so 文件后由 VM 自动调用。主要用于:(1) 使用 RegisterNatives() 进行动态注册,替代冗长的 Java_xxx 命名;(2) 初始化全局引用(缓存 class、methodID、fieldID);(3) 缓存 JavaVM 指针供 native 线程使用;(4) 返回所需的 JNI 版本号。

Q3: 为什么 native 线程不能直接使用传入的 JNIEnv 指针?

JNIEnv 是线程局部存储的(thread-local),与创建它的线程绑定。如果在 Thread A 获取了一个 JNIEnv 指针,然后在 Thread B(通过 pthread_create 创建的 native 线程)中使用它,会出现两种情况:(1) 使用错误的线程局部数据导致崩溃或未定义行为;(2) 该 JNIEnv 对应的线程局部引用表是为 Thread A 管理的,Thread B 没有权限访问。正确方法是新线程调用 JavaVM::AttachCurrentThread() 获取自己的 JNIEnv。

Q4: JNI 调用 Java 方法时,methodID 可以跨线程使用吗?需要全局引用吗?

MethodID 和 FieldID 是进程范围内的,可以跨线程安全使用,不需要转换为全局引用。这是因为它们在 VM 内部是以指针或索引形式存在的,不受 GC 影响。但 class 引用(jclass)必须转换为全局引用才能跨线程使用或缓存——局部 jclass 在方法返回后可能失效。


参考源码路径:

  • JNI 规范:https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
  • Android JNI Tips:https://developer.android.com/training/articles/perf-jni
  • AOSP 示例:frameworks/base/core/jni/android_util_EventLog.cpp
  • AOSP JNI 入口:libnativehelper/include_jni/jni.h
  • ART 运行时 JNI 实现:art/runtime/jni/jni_internal.cc
打赏
  • 微信
  • 支付宝

评论