一、JNI到底是什么?

如果你做过Android开发,肯定听说过JNI(Java Native Interface)。简单来说,它就是让Java和C/C++代码互相调用的桥梁。比如你想用C++写一个高性能的图像处理算法,然后在Java里调用它,JNI就是干这个的。

1.1 基本工作原理

JNI的核心是“双向通信”:

  • Java调C/C++:通过native关键字声明方法,然后在C/C++中实现。
  • C/C++调Java:通过JNIEnv提供的接口反射调用Java方法。

1.2 一个简单示例

下面是一个Java调用C++的示例(技术栈:Android NDK + C++):

// Java端代码  
public class NativeLib {
    // 声明native方法  
    public native String sayHello();  
    
    static {
        // 加载动态库  
        System.loadLibrary("native-lib");  
    }  
}
// C++端代码(native-lib.cpp)  
#include <jni.h>  
#include <string>  

extern "C" JNIEXPORT jstring JNICALL  
Java_com_example_NativeLib_sayHello(JNIEnv* env, jobject /* this */) {  
    std::string hello = "Hello from C++!";  
    return env->NewStringUTF(hello.c_str());  
}

注释说明

  • extern "C":确保C++编译器不修改函数名(C语言风格导出)。
  • JNIEXPORT/JNICALL:JNI的宏定义,标记函数可被Java调用。
  • 函数名规则:Java_包名_类名_方法名

二、JNI调用中的性能陷阱

JNI虽然强大,但用不好会严重拖慢性能。以下是几个常见问题:

2.1 频繁的JNI调用开销

每次Java和Native代码交互都会产生额外开销。比如下面这个反面教材:

// Java端:错误示例!频繁调用native方法  
for (int i = 0; i < 10000; i++) {  
    nativeProcessData(i); // 每次调用都有JNI开销  
}

优化方案

  • 批量处理数据,减少调用次数。
  • 将循环逻辑移到C++中。

2.2 局部引用泄漏

JNI默认使用局部引用(Local Reference),如果不手动释放,可能导致内存泄漏:

// C++端:错误示例!局部引用未释放  
jclass clazz = env->FindClass("java/util/ArrayList");  
jmethodID constructor = env->GetMethodID(clazz, "<init>", "()V");  
jobject list = env->NewObject(clazz, constructor);  
// 忘记调用env->DeleteLocalRef(list);  

正确做法

  • 显式调用DeleteLocalRef释放对象。
  • 或者使用PushLocalFrame/PopLocalFrame自动管理。

三、高级技巧:如何优化JNI性能

3.1 缓存字段ID和方法ID

查找字段ID或方法ID(如GetFieldID)是耗时操作,应该缓存结果:

// C++端:缓存优化示例  
static jmethodID cacheMethodId = nullptr;  

if (cacheMethodId == nullptr) {  
    jclass clazz = env->FindClass("com/example/NativeLib");  
    cacheMethodId = env->GetMethodID(clazz, "callback", "(I)V");  
    env->DeleteLocalRef(clazz);  
}  
// 后续直接使用cacheMethodId  

3.2 使用Direct Buffer减少拷贝

如果Java和Native需要共享大数据(如图像),用ByteBuffer.allocateDirect避免内存拷贝:

// Java端:分配直接内存  
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);  
nativeProcessBuffer(buffer);  
// C++端:直接访问内存  
void* ptr = env->GetDirectBufferAddress(buffer);  
// 直接操作ptr,无需拷贝  

四、实战:一个完整的JNI性能优化案例

假设我们要优化一个图像灰度化处理的功能:

4.1 初始版本(低效)

// Java端:逐像素调用native方法  
for (int y = 0; y < height; y++) {  
    for (int x = 0; x < width; x++) {  
        int gray = nativeGetGray(pixels[y][x]); // 每次调用都有开销  
        result[y][x] = gray;  
    }  
}

4.2 优化版本(高效)

// Java端:一次性传递所有数据  
nativeProcessImage(pixels, result, width, height);  
// C++端:批量处理  
void processImage(JNIEnv* env, jobject, jintArray pixels, jintArray result, jint width, jint height) {  
    jint* pixelPtr = env->GetIntArrayElements(pixels, nullptr);  
    jint* resultPtr = env->GetIntArrayElements(result, nullptr);  
    
    for (int i = 0; i < width * height; i++) {  
        int r = (pixelPtr[i] >> 16) & 0xFF;  
        int g = (pixelPtr[i] >> 8) & 0xFF;  
        int b = pixelPtr[i] & 0xFF;  
        resultPtr[i] = (r + g + b) / 3; // 灰度化计算  
    }  
    
    env->ReleaseIntArrayElements(pixels, pixelPtr, 0);  
    env->ReleaseIntArrayElements(result, resultPtr, 0);  
}

优化效果

  • 调用次数从O(N²)降到O(1)。
  • 运行速度提升10倍以上。

五、应用场景与注意事项

5.1 适合使用JNI的场景

  • 高性能计算(如图像处理、音视频编解码)。
  • 调用已有的C/C++库(如OpenCV、FFmpeg)。

5.2 注意事项

  1. 线程安全:JNIEnv是线程相关的,不能跨线程使用。
  2. 异常处理:Native代码中要通过env->ExceptionCheck()检查Java异常。
  3. 内存管理:Native层分配的内存必须手动释放。

总结

JNI是Android高性能开发的利器,但也需要谨慎使用。核心原则是:

  • 减少跨语言调用次数。
  • 缓存重复使用的ID。
  • 合理管理内存和引用。

如果你能掌握这些技巧,NDK开发就会变得游刃有余!