目录
  1. 1. 一、JNI 在 Android 中的角色
    1. 1.1. 1.1 JNIEnv 与 JavaVM 的内部结构
  2. 2. 二、JNI 数据类型映射
    1. 2.1. 2.1 GetStringUTFChars 与内存管理
  3. 3. 三、局部引用与全局引用:引用表溢出
    1. 3.1. 3.1 三种引用类型
    2. 3.2. 3.2 局部引用表溢出
    3. 3.3. 3.3 全局引用
    4. 3.4. 3.4 全局引用的容量限制
  4. 4. 四、JNI_OnLoad 与动态注册
    1. 4.1. 4.1 静态注册(基于方法名约定)
    2. 4.2. 4.2 动态注册(通过 JNI_OnLoad)
    3. 4.3. 4.3 JNIEXPORT 与 JNICALL 的含义
    4. 4.4. 4.4 JNI_OnUnload
  5. 5. 五、从 Native 调用 Java 方法
    1. 5.1. 5.1 jmethodID 的缓存策略
    2. 5.2. 5.2 调用静态方法和访问静态字段
  6. 6. 六、Native 线程与 JavaVM
    1. 6.1. 6.1 Attach 时的线程属性
    2. 6.2. 6.2 不要在 native 线程中 FindClass
  7. 7. 七、Java 异常的 Native 处理
    1. 7.1. 7.1 异常的详细处理
    2. 7.2. 7.2 native 代码中的崩溃与异常的区别
  8. 8. 八、完整实践:Native 加密实现
  9. 9. 九、JNI 性能优化总结
  10. 10. 十、面试常问题目
【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 等)。

1.1 JNIEnv 与 JavaVM 的内部结构

理解 JNIEnv 的本质对于写出正确的 JNI 代码至关重要。JNIEnv 是一个函数指针表(在 C 中是一个 const struct JNINativeInterface*),包含了所有 JNI 函数的入口。每个线程有自己的 JNIEnv,这意味着:

  • 不能跨线程传递 JNIEnv 指针。
  • 在 native 创建的线程中必须通过 AttachCurrentThread 获取当前线程的 JNIEnv。
// JNIEnv 在 C 中的本质(简化版)
// 源码: libnativehelper/include_jni/jni.h
struct JNINativeInterface {
jclass (*FindClass)(JNIEnv*, const char*);
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
// ... 约 230 个函数指针
};

// C 中:env->FindClass(env, "java/lang/String") → 函数指针解引用
// C++ 中:env->FindClass("java/lang/String") → 内联包装,自动传递 env

JavaVM 是一个进程级的单例,代表整个 Java 虚拟机实例。在 Android 进程中,一个进程只有一个 JavaVM。可以通过 JNI_OnLoad 缓存 JavaVM 指针,供 native 线程使用。

二、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 函数或阻塞。

2.1 GetStringUTFChars 与内存管理

字符串处理是 JNI 中最常见的操作之一,也是最容易产生内存泄漏的地方:

// 正确处理 Java String → C string 的转换
JNIEXPORT void JNICALL Java_com_example_NativeLib_processString(
JNIEnv *env, jobject thiz, jstring input) {

// GetStringUTFChars 分配内存,必须配对 ReleaseStringUTFChars
const char *utf = (*env)->GetStringUTFChars(env, input, NULL);
if (utf == NULL) {
return; // OutOfMemoryError
}

// 处理 utf 字符串...
size_t len = strlen(utf);

// 必须释放!第三个参数是 isCopy 输出(NULL 表示不关心是否拷贝)
(*env)->ReleaseStringUTFChars(env, input, utf);

// 如果只需要获取长度,使用 GetStringUTFLength 更高效(不会拷贝字符串)
jsize utf_len = (*env)->GetStringUTFLength(env, input);

// 从 C string 构造 Java String
jstring result = (*env)->NewStringUTF(env, "Hello from native");

// 大字符串推荐 GetStringCritical(暂停 GC,减少拷贝)
const jchar *critical = (*env)->GetStringCritical(env, input, NULL);
if (critical != NULL) {
// 快速处理——不能调用其他 JNI 函数,不能阻塞!
(*env)->ReleaseStringCritical(env, input, critical);
}
}

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

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;
}
}

3.4 全局引用的容量限制

全局引用表也有容量限制(ART 中默认约为 51200 个全局引用)。如果创建了过多全局引用且不释放,会导致 global reference table overflow。这是比局部引用表溢出更难排查的问题——因为全局引用是跨方法的,泄漏会随着时间累积。

// 监控全局引用使用量
// Android 没有公开 API,但可以通过 JNI CheckJNI 开启检测
// adb shell setprop dalvik.vm.checkjni true
// 会在全局引用数量异常时输出警告

四、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[]
  • L 开头的类名必须以 ; 结尾,如 Ljava/util/List;
  • 构造函数的返回值类型为 V,方法名为 <init>

4.3 JNIEXPORT 与 JNICALL 的含义

// JNIEXPORT: 导出符号(在 Android/Linux 上通常定义为
// __attribute__((visibility("default")))
// JNICALL: 调用约定(在 Android/Linux 上通常为空或 __stdcall on Windows)
// 这两个宏确保 native 函数被正确导出,供 VM 通过 dlsym 查找

4.4 JNI_OnUnload

// 当 ClassLoader 被 GC 时(在 Android 中很少发生),
// VM 调用 JNI_OnUnload 给予释放资源的机会
void JNI_OnUnload(JavaVM *vm, void *reserved) {
JNIEnv *env;
(*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6);

// 释放全局引用
if (g_cached_class != NULL) {
(*env)->DeleteGlobalRef(env, g_cached_class);
}
// 注意:MethodID 和 FieldID 不需要释放
}

五、从 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;
}

5.1 jmethodID 的缓存策略

MethodID 和 FieldID 是进程级有效的,不需要全局引用。但 jclass 必须用全局引用缓存。ID 本身在 ART 中是 ArtMethod*ArtField* 的指针,只要类没有被卸载就不会失效。在 Android 中,应用类几乎从不卸载,所以 ID 的缓存非常安全。

5.2 调用静态方法和访问静态字段

// 调用静态方法
jclass clazz = (*env)->FindClass(env, "java/lang/System");
jmethodID gcMethod = (*env)->GetStaticMethodID(env, clazz, "gc", "()V");
(*env)->CallStaticVoidMethod(env, clazz, gcMethod);

// 访问静态字段
jclass buildClass = (*env)->FindClass(env, "android/os/Build");
jfieldID modelField = (*env)->GetStaticFieldID(env, buildClass, "MODEL",
"Ljava/lang/String;");
jstring model = (jstring)(*env)->GetStaticObjectField(env, buildClass, modelField);

六、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)。
  • 一旦 detach,该线程就不能再使用 JNI 函数,除非重新 attach。
  • 大量线程频繁 attach/detach 会影响性能——考虑使用线程池。

6.1 Attach 时的线程属性

// AttachCurrentThreadAsDaemon:将线程附加为守护线程
// 守护线程不会阻止 JVM 退出
JavaVMAttachArgs attach_args;
attach_args.version = JNI_VERSION_1_6;
attach_args.name = "MyNativeWorker";
attach_args.group = NULL; // 使用默认线程组
(*g_jvm)->AttachCurrentThread(g_jvm, &env, &attach_args);

// 或者使用便捷函数
// 注意:Android 的 AttachCurrentThread 默认将线程附加为 daemon

6.2 不要在 native 线程中 FindClass

在 native 创建的线程中调用 FindClass 会使用系统 ClassLoader(Bootstrap ClassLoader),导致找不到应用自定义的类。解决方案是在 JNI_OnLoad 中缓存 ClassLoader 并使用它来加载类:

static jobject g_classLoader = NULL;
static jmethodID g_findClassMethod = NULL;

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
(*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6);

// 获取当前线程的 ClassLoader(在 main 线程中调用 JNI_OnLoad)
jclass clazz = (*env)->FindClass(env, "com/example/NativeLib");
jclass classLoaderClass = (*env)->FindClass(env, "java/lang/ClassLoader");
jmethodID getClassLoaderMethod = (*env)->GetMethodID(env,
(*env)->GetObjectClass(env, clazz), "getClassLoader",
"()Ljava/lang/ClassLoader;");
jobject classLoader = (*env)->CallObjectMethod(env, clazz, getClassLoaderMethod);

g_classLoader = (*env)->NewGlobalRef(env, classLoader);
g_findClassMethod = (*env)->GetMethodID(env, classLoaderClass, "loadClass",
"(Ljava/lang/String;)Ljava/lang/Class;");

// ... RegisterNatives ...

return JNI_VERSION_1_6;
}

// 在 native 线程中加载类
jclass findClassInNativeThread(JNIEnv *env, const char *name) {
jstring className = (*env)->NewStringUTF(env, name);
jclass clazz = (jclass)(*env)->CallObjectMethod(env, g_classLoader,
g_findClassMethod, className);
(*env)->DeleteLocalRef(env, className);
return clazz;
}

七、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() 抛出新异常。

7.1 异常的详细处理

// ExceptionOccurred vs ExceptionCheck
// ExceptionOccurred 返回异常对象引用(需 DeleteLocalRef)
// ExceptionCheck 返回 jboolean,不创建新引用(推荐)
if ((*env)->ExceptionCheck(env)) {
// 获取异常对象以获取详细信息
jthrowable exception = (*env)->ExceptionOccurred(env);
if (exception != NULL) {
// 获取异常类名
jclass exceptionClass = (*env)->GetObjectClass(env, exception);
jmethodID getMessageMethod = (*env)->GetMethodID(env,
exceptionClass, "getMessage", "()Ljava/lang/String;");
jstring message = (jstring)(*env)->CallObjectMethod(env,
exception, getMessageMethod);

// 注意:ExceptionOccurred 后,异常仍然处于 pending 状态
// 必须清除后才能继续调用其他 JNI 函数
(*env)->ExceptionClear(env);

// 现在可以安全地将异常信息传递给 Java 层
jclass runtimeEx = (*env)->FindClass(env,
"java/lang/RuntimeException");
// 包装为 RuntimeException 并重新抛出
const char *msg = (*env)->GetStringUTFChars(env, message, NULL);
(*env)->ThrowNew(env, runtimeEx, msg);
(*env)->ReleaseStringUTFChars(env, message, msg);

(*env)->DeleteLocalRef(env, message);
(*env)->DeleteLocalRef(env, exceptionClass);
(*env)->DeleteLocalRef(env, exception);
}
}

7.2 native 代码中的崩溃与异常的区别

Native 崩溃(SIGSEGV、SIGABRT 等)不等于 Java 异常。native 层的空指针解引用直接导致进程收到 SIGSEGV 信号,触发 tombstone 生成和进程退出——它不会转换为 Java 层的 NullPointerException。只有通过 JNI 调用的 Java 方法失败(如找不到方法、类型不匹配)才会产生 Java 异常。

八、完整实践: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;
}

九、JNI 性能优化总结

优化点 说明
缓存 jclass/jmethodID/jfieldID 在 JNI_OnLoad 中一次获取,避免重复查找
使用 RegisterNatives 比静态注册更快
避免频繁的 JNI 调用边界跨越 将多次小操作合并为一次大数据传递
在 native 侧批量操作数据 如数组处理在 native 循环中完成,不要每个元素都回传 Java
使用 GetPrimitiveArrayCritical 对性能敏感的数组访问(但要短小,不能阻塞)
管理引用 及时 DeleteLocalRef,避免引用表溢出
使用 PushLocalFrame 对大量临时引用比手动 DeleteLocalRef 更方便

十、面试常问题目

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 在方法返回后可能失效。

Q5: 在 native 线程中 FindClass 为什么可能失败?如何解决?

在 native 创建的 pthread 中调用 FindClass 使用的是系统 ClassLoader(Bootstrap ClassLoader),该 ClassLoader 只能找到 Framework 类(如 java.lang.String),找不到应用自定义的类。解决方案:(1) 在 JNI_OnLoad(运行在主线程中)时缓存应用 ClassLoader 的 GlobalRef;(2) 在 native 线程中通过缓存的 ClassLoader.loadClass() 加载类;(3) 或者在 JNI_OnLoad 时就缓存好所有需要的 jclass 全局引用,native 线程中直接使用。


参考源码路径:

  • 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
打赏
  • 微信
  • 支付宝

评论