第一章:Native方法声明与实现

各位同学,今天我们来聊聊JNI最基础、也最核心的部分——Native方法的声明与实现。说实话,我刚接触NDK那会儿,被JNI的函数命名规则折磨得不轻。但搞懂了背后的设计思路,你会发现它其实很规整。

1.1 Java中的native关键字

在Java里,native关键字告诉虚拟机:这个方法我不在Java层实现,而是交给C/C++去处理。写法很简单:

public class NativeBridge {
    // 声明一个native方法
    public native String stringFromJNI();
    
    // 带参数的方法
    public native int add(int a, int b);
    
    // 静态native方法
    public static native String getBuildType();
}

注意,native方法只有声明,没有方法体。它就像一份合同,Java这边签了字,具体干活的是C/C++那边。

关键点:native方法可以是实例方法,也可以是静态方法。返回值可以是基本类型、String、数组,甚至是自定义的Java对象。

1.2 C/C++中JNI函数实现

Java声明完了,C/C++这边怎么接?JNI有一套严格的命名规则:

// 格式:Java_包名_类名_方法名
// 包名中的点(.)要换成下划线(_)

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_NativeBridge_stringFromJNI(
    JNIEnv* env,
    jobject thiz) {
    // 实现代码
    return env->NewStringUTF("Hello from C++!");
}

extern "C" JNIEXPORT jint JNICALL
Java_com_example_NativeBridge_add(
    JNIEnv* env,
    jobject thiz,
    jint a,
    jint b) {
    return a + b;
}

这里有几个细节我得提醒你:

  • extern "C" 防止C++名字修饰,保证JNI能找到函数
  • JNIEXPORTJNICALL 是平台相关的宏,保证函数导出和调用约定正确
  • 函数名必须和Java层完全匹配,大小写敏感

我曾经因为包名写错一个字母,导致运行时一直报UnsatisfiedLinkError。排查了半天才发现是包名大小写问题。嗯,JNI函数名是区分大小写的,千万别马虎。

1.3 JNIEnv与jobject的使用

每个JNI函数的前两个参数是固定的:JNIEnv*jobject(或 jclass)。

JNIEnv 是JNI环境的接口指针。说白了,它就是C/C++访问Java世界的桥梁。你想调用Java的方法、访问Java的字段、创建Java对象,都得通过它。

jobject 代表调用这个native方法的Java对象实例。如果是静态方法,第二个参数是 jclass,代表Java类本身。

// 实例方法:thiz 指向调用该方法的Java对象
extern "C" JNIEXPORT void JNICALL
Java_com_example_NativeBridge_doSomething(
    JNIEnv* env,
    jobject thiz) {
    // 通过thiz可以访问Java对象的字段和方法
    jclass clazz = env->GetObjectClass(thiz);
    // ...
}

// 静态方法:clazz 指向Java类
extern "C" JNIEXPORT void JNICALL
Java_com_example_NativeBridge_staticMethod(
    JNIEnv* env,
    jclass clazz) {
    // 通过clazz可以访问静态字段和静态方法
}

我个人习惯在实现JNI函数时,先检查env和thiz/clazz是否为NULL。虽然大多数情况下不会为空,但养成这个习惯能帮你避免一些诡异的崩溃。

1.4 字符串处理

Java的String和C/C++的字符串不是一回事。Java的String是UTF-16编码的Unicode字符串,而C/C++用的是以'\0'结尾的char数组。JNI提供了转换函数:

// Java字符串 -> C字符串
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_NativeBridge_processString(
    JNIEnv* env,
    jobject thiz,
    jstring input) {
    
    // 获取C风格字符串
    const char* nativeStr = env->GetStringUTFChars(input, nullptr);
    if (nativeStr == nullptr) {
        return nullptr; // 内存分配失败
    }
    
    // 使用字符串
    std::string result = "Processed: ";
    result += nativeStr;
    
    // 释放资源
    env->ReleaseStringUTFChars(input, nativeStr);
    
    // 返回新的Java字符串
    return env->NewStringUTF(result.c_str());
}

这里有个坑:GetStringUTFChars 返回的是UTF-8编码的字符串,但Java内部是UTF-16。转换过程有性能开销。如果你处理大量字符串,可以考虑用 GetStringCriticalReleaseStringCritical 来减少拷贝。

我曾经在项目中忘记调用ReleaseStringUTFChars,导致内存泄漏。JNI的字符串函数分配的内存需要手动释放,不像C++的智能指针那么省心。建议用RAII封装一下:

class JStringGuard {
public:
    JStringGuard(JNIEnv* env, jstring str)
        : m_env(env), m_str(str) {
        m_chars = env->GetStringUTFChars(str, nullptr);
    }
    ~JStringGuard() {
        if (m_chars) {
            m_env->ReleaseStringUTFChars(m_str, m_chars);
        }
    }
    const char* get() const { return m_chars; }
private:
    JNIEnv* m_env;
    jstring m_str;
    const char* m_chars;
};

1.5 数组处理

数组的处理逻辑和字符串类似。JNI提供了专门的操作函数:

// 处理int数组
extern "C" JNIEXPORT jintArray JNICALL
Java_com_example_NativeBridge_doubleArray(
    JNIEnv* env,
    jobject thiz,
    jintArray input) {
    
    // 获取数组长度
    jsize len = env->GetArrayLength(input);
    
    // 获取数组元素指针
    jint* elements = env->GetIntArrayElements(input, nullptr);
    if (elements == nullptr) {
        return nullptr;
    }
    
    // 创建输出数组
    jintArray result = env->NewIntArray(len);
    jint* resultElements = env->GetIntArrayElements(result, nullptr);
    
    // 处理每个元素
    for (jsize i = 0; i < len; i++) {
        resultElements[i] = elements[i] * 2;
    }
    
    // 释放资源
    env->ReleaseIntArrayElements(input, elements, JNI_ABORT);
    env->ReleaseIntArrayElements(result, resultElements, 0);
    
    return result;
}

注意 ReleaseIntArrayElements 的第三个参数:

  • 0:将修改同步回Java数组并释放缓冲区
  • JNI_ABORT:不同步修改,直接释放缓冲区
  • JNI_COMMIT:同步修改但不释放缓冲区

对于对象数组(比如String数组),需要用 GetObjectArrayElementSetObjectArrayElement 逐个访问:

extern "C" JNIEXPORT void JNICALL
Java_com_example_NativeBridge_printStrings(
    JNIEnv* env,
    jobject thiz,
    jobjectArray stringArray) {
    
    jsize len = env->GetArrayLength(stringArray);
    for (jsize i = 0; i < len; i++) {
        jstring str = (jstring)env->GetObjectArrayElement(stringArray, i);
        const char* nativeStr = env->GetStringUTFChars(str, nullptr);
        // 处理字符串...
        env->ReleaseStringUTFChars(str, nativeStr);
        env->DeleteLocalRef(str); // 释放局部引用
    }
}

我建议在处理完每个对象数组元素后,及时调用DeleteLocalRef释放局部引用。JNI的局部引用表默认容量有限(通常是512个),处理大数组时很容易溢出。

知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:

Native方法声明与实现 - 知识体系 Java层 native关键字声明方法 JNI桥接层 C/C++层 JNI函数实现 核心组件 JNIEnv · jobject · jclass 字符串处理 GetStringUTFChars / NewStringUTF 数组处理 GetIntArrayElements / GetObjectArrayElement

这张图把整个流程串起来了:Java层用native声明,JNI桥接层负责匹配,C/C++层实现具体逻辑,同时通过JNIEnv操作字符串和数组。你写代码时,脑子里要有这张图,就不会迷失方向。


公众号:蓝海资料掘金营,微信deep3321