深入浅出 Android JNI (三):数据类型转换与内存管理

2 阅读5分钟

1. 基本类型:无缝直通

Java 的基本类型(intbooleanfloat 等)在 JNI 中有直接的对应类型(jintjbooleanjfloat 等)。它们在内存中的表示相同,因此传递时直接拷贝值本身,无需特殊转换。

1.1 类型映射表

Java 类型JNI 类型C/C++ 类型大小/范围
booleanjbooleanunsigned char8位,JNI_TRUE(1)/JNI_FALSE(0)
bytejbytesigned char8位,-128~127
charjcharunsigned short16位,Unicode 字符 (UTF-16)
shortjshortshort16位,-32768~32767
intjintint32位,约±21亿
longjlonglong long64位
floatjfloatfloat32位 IEEE 754
doublejdoubledouble64位 IEEE 754
voidvoidvoid-

1.2 使用示例

Java 侧:

public native int add(int a, int b);

C++ 侧:

extern "C" JNIEXPORT jint JNICALL
Java_com_example_MyClass_add(JNIEnv *env, jobject thiz, jint a, jint b) {
    // 直接像使用普通 int 一样操作 jint
    return a + b;
}

2. 引用类型:需要"翻译官"

Java 的引用类型(String、数组、任意对象)在 JNI 中对应 jstringjarray/jxxxArrayjobject。它们不是直接可用的 C/C++ 类型,必须通过 JNIEnv* 提供的函数操作。

2.1 字符串 (jstring):最常用的引用类型

2.1.1 Java String → C/C++ char* (UTF-8)

// 1. 获取 UTF-8 编码的 C 字符串
const char *c_str = env->GetStringUTFChars(javaString, nullptr);
if (c_str == nullptr) return; // 必须检查!

// 2. 使用字符串
printf("C String: %s\n", c_str);

// 3. 必须释放!避免内存泄漏
env->ReleaseStringUTFChars(javaString, c_str);

2.1.2 C/C++ char* → Java String

const char *utf8_str = "Hello from C++!";
jstring javaString = env->NewStringUTF(utf8_str);
return javaString;

2.2 数组:处理批量数据

2.2.1 基本类型数组 (如 jintArray)

模式 1:获取数组指针(适合大数组操作)

jint *c_array = env->GetIntArrayElements(javaArray, nullptr);
jsize length = env->GetArrayLength(javaArray);

// 操作数组元素
for (int i = 0; i < length; i++) {
    c_array[i] *= 2; 
}

// 释放并提交更改
env->ReleaseIntArrayElements(javaArray, c_array, 0);

模式 2:复制数组区域(适合小数组)

jsize length = env->GetArrayLength(javaArray);
jint buffer[length];

// 复制数据到缓冲区
env->GetIntArrayRegion(javaArray, 0, length, buffer);

// 操作数据...

// 将修改写回 Java 数组
env->SetIntArrayRegion(javaArray, 0, length, buffer);

2.3 对象操作:访问字段与方法

// 获取类引用
jclass clazz = env->GetObjectClass(jobject);

// 获取字段 ID(签名是关键!)
jfieldID fieldId = env->GetFieldID(clazz, "count", "I");

// 读写字段值
jint count = env->GetIntField(jobject, fieldId);
env->SetIntField(jobject, fieldId, count + 1);

// 获取方法 ID
jmethodID methodId = env->GetMethodID(clazz, "update", "(I)V");

// 调用方法
env->CallVoidMethod(jobject, methodId, 42);

3. 类型签名:JNI 的"密码本"

在获取字段/方法 ID 时,类型签名是正确识别目标的关键:

类型签名表示示例
intIint count → I
StringLjava/lang/String;String name → Ljava/lang/String;
int[][Iint[] data → [I
方法(参数)返回值void update(int) → (I)V

获取签名的实用方法:

  1. 终端命令:javap -s -p MyClass.class
  2. Android Studio 插件:JNI Helper
  3. 运行时生成:env->GetMethodID(clazz, "method", "签名")

4. 内存管理:JNI 的雷区

4.1 引用类型管理

引用类型创建方式生命周期释放方式使用场景
局部引用大部分 JNI 函数返回当前 native 方法执行期间DeleteLocalRef() 或自动释放临时使用
全局引用NewGlobalRef()显式释放前DeleteGlobalRef()长期缓存
弱全局引用NewWeakGlobalRef()不阻止 GC 回收DeleteWeakGlobalRef()缓存可能回收的对象

4.2 局部引用管理最佳实践

危险代码(会导致局部引用表溢出):

for (int i = 0; i < 1000; i++) {
    jstring str = env->NewStringUTF("Hello");
    // 没有释放局部引用!
}

修复方案 1:显式删除

for (int i = 0; i < 1000; i++) {
    jstring str = env->NewStringUTF("Hello");
    // 使用字符串...
    env->DeleteLocalRef(str); // 及时释放
}

修复方案 2:使用局部帧(推荐)

for (int i = 0; i < 1000; i++) {
    env->PushLocalFrame(10); // 创建新的局部引用帧
    
    jstring str = env->NewStringUTF("Hello");
    // 创建其他局部引用...
    
    env->PopLocalFrame(nullptr); // 自动释放当前帧所有局部引用
}

4.3 全局引用使用示例

// 全局缓存类引用
static jclass myGlobalClass = nullptr;

JNIEXPORT void JNICALL
Java_com_example_MyClass_init(JNIEnv *env, jobject thiz) {
    if (myGlobalClass == nullptr) {
        jclass localClass = env->FindClass("com/example/MyClass");
        myGlobalClass = (jclass)env->NewGlobalRef(localClass);
        env->DeleteLocalRef(localClass);
    }
}

// 使用全局引用
env->GetStaticMethodID(myGlobalClass, "staticMethod", "()V");

// 在适当位置释放
env->DeleteGlobalRef(myGlobalClass);

5. 资源释放清单

资源类型释放方式
GetStringUTFChars 返回的指针ReleaseStringUTFChars
数组元素指针Release<Type>ArrayElements
局部引用 (大对象/循环内)DeleteLocalRef
全局引用DeleteGlobalRef
弱全局引用DeleteWeakGlobalRef
malloc/new 分配的内存free/delete

黄金法则:  谁分配,谁释放!  对 JNI 函数返回的资源,必须成对调用对应的释放函数。

6. 实战:类型转换综合示例

Java 对象:

public class User {
    public String name;
    public int age;
    public int[] scores;
    
    public native void processInNative();
}

JNI 实现:

extern "C" JNIEXPORT void JNICALL
Java_com_example_User_processInNative(JNIEnv *env, jobject user) {
    // 获取类引用
    jclass userClass = env->GetObjectClass(user);
    
    // 获取字段 ID
    jfieldID nameField = env->GetFieldID(userClass, "name", "Ljava/lang/String;");
    jfieldID ageField = env->GetFieldID(userClass, "age", "I");
    jfieldID scoresField = env->GetFieldID(userClass, "scores", "[I");
    
    // 获取字段值
    jstring jname = (jstring)env->GetObjectField(user, nameField);
    jint age = env->GetIntField(user, ageField);
    jintArray jscores = (jintArray)env->GetObjectField(user, scoresField);
    
    // 处理字符串
    const char *cname = env->GetStringUTFChars(jname, nullptr);
    printf("User: %s, Age: %d\n", cname, age);
    env->ReleaseStringUTFChars(jname, cname);
    
    // 处理数组
    jint *scores = env->GetIntArrayElements(jscores, nullptr);
    jsize length = env->GetArrayLength(jscores);
    
    int sum = 0;
    for (int i = 0; i < length; i++) {
        sum += scores[i];
    }
    printf("Average score: %.2f\n", (float)sum / length);
    
    // 修改数组内容
    for (int i = 0; i < length; i++) {
        scores[i] += 5; // 给每人加5分
    }
    
    // 释放数组资源并提交更改
    env->ReleaseIntArrayElements(jscores, scores, 0);
    
    // 释放局部引用
    env->DeleteLocalRef(jname);
    env->DeleteLocalRef(jscores);
}

7. 本章总结:安全贸易指南

  1. 基本类型:直接传递,高效安全

  2. 字符串转换

    • GetStringUTFChars/ReleaseStringUTFChars 配对使用
    • 优先使用 UTF-8 编码
  3. 数组操作

    • 大数组:GetArrayElements/ReleaseArrayElements
    • 小数组:GetArrayRegion/SetArrayRegion
  4. 对象操作

    • 先获取类引用,再获取字段/方法 ID
    • 类型签名必须精确匹配
  5. 内存管理核心

    • 局部引用:警惕溢出,及时删除或使用局部帧
    • 全局引用:长期缓存的首选
    • 弱全局引用:不阻止 GC 的缓存方案
  6. 资源释放

    • 每个 GetXXX 调用必须有对应的 ReleaseXXX
    • 本地分配的内存必须本地释放

避坑指南:

  • 🔸 签名错误是 NoSuchMethodError 的罪魁祸首
  • 🔸 忘记释放资源会导致内存泄漏
  • 🔸 局部引用溢出是常见崩溃原因
  • 🔸 修改数组后忘记使用 0 模式提交更改