从Java到C++:JNI实战

1,034 阅读17分钟

从Java到C++系列目录

前言

概念

本文中:

JNI方法:指JNI提供的一系列API。

native方法:跨native层调用的方法(Java->C/C++)。

C/C++方法:除native方法外,普通的C/C++方法。

native层:C/C++代码。

代码

示例代码:JNIInterface

测试代码:JNIInterfaceTest

摘要

本文主要内容如下:

加载so

native方法声明、定义

native与static native

静态注册与动态注册

Java元素定位

局部引用与全局引用

传递Java基本类型

传递Java对象

调用Java方法

处理Java方法调用异常

传递枚举

传递字符串

加载so

在Java层调用native方法,首先要加载so。加载so,主要通过下面两个方法,二者效果是一致的:

void load(String filename)
void loadLibrary(String libname)

以加载libjava2cpp.so为例:

load的参数是so文件的全路径名。APK安装后,在设备的/data/data/{packageName}/lib下可以看见APK里的so。 所以:

System.load("/data/data/com.example.java2cpp/lib/libjava2cpp.so");

loadLibrary的参数,是so文件的部分名称。如libjava2cpp.so,就是去掉前缀"lib",去掉后缀".so":

System.loadLibrary("java2cpp");

注意事项:

  1. 加载so的代码,通常写在静态代码块里。好处是加载类时,静态代码块会优先执行。确保了在用户调用类的native方法之前,so已加载。
class JNIInterface {
    static {
        System.loadLibrary("java2cpp");
    }
}

native方法声明、定义

在C/C++中,声明和定义通常是分离的。声明一般在头文件(.h)中,定义一般在源文件(.cpp/.cc)中。

native方法的声明和定义也是如此。但是特别的是,声明是在Java源文件中的。

class JNIInterface {
    public native static String stringFromJNI();
}

native的定义则是在C/C++的源文件中。

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_java2cpp_JNIInterface_stringFromJNI(JNIEnv *env, jclass clazz) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

注意事项:

  1. 声明是在Java源文件中,需要遵守Java语法。没有方法体,并且需要用native修饰符来声明。
  2. 定义是在C/C++的源文件中。需要遵守C/C++的语法。
  3. native方法声明和定义的参数个数不一样。定义总会比声明多两个参数:JNIEnvjclass/jobject
  4. native方法是不支持重载的。如,再声明一个stringFromJNI的重载方法是不可行的。
public native static String stringFromJNI(int x);
  1. 推测:extern "C"导致了native方法无法重载。extern "C"会让编译器按照C语言的编译方式,为native方法生成符号表。而C语言是不支持重载的。

native与static native

native方法,可以声明为成员方法,也可以声明为静态方法

声明:

class JNIInterface {
    public native void jniMethod();
    public native static void staticJniMethod();
}

对应的实现:

extern "C" JNIEXPORT void JNICALL
Java_com_example_java2cpp_JNIInterface_jniMethod(JNIEnv *env, jobject thiz) {}

extern "C" JNIEXPORT void JNICALL
Java_com_example_java2cpp_JNIInterface_staticJniMethod(JNIEnv *env, jclass clazz) {}

两者的差异在于:

  1. 成员方法对应的实现,第二个参数是jobject,代表JNIInterface的一个对象
  2. 静态方法对应的实现,第二个参数是jclass,代表JNIInterface这个类

静态注册与动态注册

native方法,可以选择静态注册、动态注册两种方式。

下面将对如下两个native方法分别采用静态注册、动态注册:

public native void nativeJniMethod();

public native int nativeDynamicRegisterMethod();

静态注册

静态注册的native方法名,必须遵循一定的规则:

  1. extern "C" JNIEXPORT + 返回值 + JNICALL
  2. Java_ + 包名(_代替.) + _ + 类名 + _ + 方法名
extern "C" JNIEXPORT void JNICALL
Java_com_example_java2cpp_JNIInterface_nativeJniMethod(JNIEnv *env, jobject thiz) {}

动态注册

动态注册一般在JNI_OnLoad方法上进行。JNI_OnLoad方法会在System.loadLibrary加载so成功后,被虚拟机调用。

动态注册主要分为四步:

  1. 实现native方法:dynamic_register_method。命名和普通的c++方法一样即可。

  2. 在gMethods数组中,添加JNINativeMethod结构体(代表native方法声明与native方法实现映射关系),如:

{"nativeDynamicRegisterMethod", "()I", (void *) dynamic_register_method}
  1. 通过FindClass来加载对应的Java类。
  2. 通过RegisterNatives来进行动态注册。

后续需要新增native方法时,只需要重复前两个步骤即可。

#define JNIInterface_CLASS "com/example/java2cpp/JNIInterface"

static jint dynamic_register_method(JNIEnv *env, jobject thiz) {
    return 0;
}

/**
 * 代表native方法声明与native方法实现映射关系
 * 结构体JNINativeMethod的三个字段分别是:
 * name:Java定义的native方法名
 * signature:Java定义的native方法的类型签名
 * fnPtr:native方法的函数指针,指向native方法的实现
 */
static const JNINativeMethod gMethods[] = {
        {"nativeDynamicRegisterMethod", "()I", (void *) dynamic_register_method},
};

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

    jclass c = env->FindClass(JNIInterface_CLASS);
    if (c == nullptr) {
        const char *msg = "Native registration unable to find class; aborting...";
        env->FatalError(msg);
    }

    int numMethods = sizeof(gMethods) / sizeof(gMethods[0]);
    //采用RegisterNatives进行动态注册
    if (env->RegisterNatives(c, gMethods, numMethods) < 0) {
        const char *msg = "RegisterNatives failed; aborting...";
        env->FatalError(msg);
    }
    return JNI_VERSION_1_6;
}

还可以对动态注册的方法进行反注册,一般在JNI_OnUnload进行,通过UnregisterNatives来反注册。

void JNI_OnUnload(JavaVM *vm, void *reserved) {
    ...
    env->UnregisterNatives(c)
}

Java元素定位

在native方法中,无论是获取Java对象的变量、还是调用Java的方法等,首先需要通过JNI方法,定位对应的Java元素。

这里的Java元素,包括:类、成员变量、静态变量、成员方法、静态方法等。

Java代码:

class JNIInterface {
    public int num = 1;
    public static int staticNum = 2;

    private int getNum() {
        return num;
    }

    private static int getStaticNum() {
        return staticNum;
    }
}

C++代码:

struct JavaJNIInterface {
    jclass class_ref;
    jfieldID num;
    jfieldID static_num;
    jmethodID getNum;
    jmethodID getStaticNum;
};
static JavaJNIInterface javaJNIInterface;

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    ...
    jclass c = env->FindClass(JNIInterface_CLASS);
    javaJNIInterface.class_ref = reinterpret_cast<jclass>(env->NewGlobalRef(c));
    javaJNIInterface.num = env->GetFieldID(c, "num", "I");
    javaJNIInterface.static_num = env->GetStaticFieldID(c, "staticNum", "I");
    javaJNIInterface.getNum = env->GetMethodID(c, "getNum", "()I");
    javaJNIInterface.getStaticNum = env->GetStaticMethodID(c, "getStaticNum", "()I");
    env->DeleteLocalRef(c);
    ...
}

注意事项:

  1. jfieldIDjmethodID是可以反复使用的,除非虚拟机卸载了该类。所以,这里将jfieldIDjmethodID结果保存起来。
  2. jclass在保存之前,经过了NewGlobalRef转换。因为FindClass获得的jclass是局部引用,生命周期短。具体原因参考后面的局部引用与全局引用
  3. 变量/方法的是public,还是private都不影响JNI方法访问。
  4. 字段、方法的类型签名不需要刻意去记忆。可以通过Studio的提示,快速实现。

code.gif

局部引用与全局引用

JNI的引用类型具体有哪些,可以回顾一下《从Java到C++-JNI基本概念》。

JNI将引用类型分为两类:局部引用、全局引用。

当Java方法调用native方法时,Java VM会创建一个注册表。所有从Java层传递到native层的Java对象会被添加到注册表中。注册表会将不可移动的局部引用映射到Java对象,防止对象被垃圾回收。native方法调用结束并返回时,注册表会被删除,局部引用不再指向Java对象。这些Java对象如果没有其他GC Roots可达,就可正常被垃圾回收。

局部引用

  1. 在一个native方法调用期间都是有效的,在native方法完成调用返回时,会被自动释放。不能跨线程使用。
  2. Java对象作为参数,传递到native方法时,都是局部引用。
  3. 通过JNI方法,获取到的Java对象,都是局部引用。如:FindClassNewObjectGetObjectField等JNI方法。
  4. 局部引用在下面两种情况要考虑通过env->DeleteLocalRef主动释放:
    1. native方法访问大型的Java对象时(比如一个大数组),会创建对Java对象的本地引用。native方法使用完大对象后,还进行较耗时的操作。这期间,由于本地引用的存在,会导致大对象无法及时被垃圾回收。
    2. native方法创建了大量的局部引用,可能导致本地引用表溢出,甚至系统内存不足。比如在循环遍历中,调用JNI方法,获取Java对象,不断创建新的本地引用。下列代码,将会导致JNI ERROR (app bug): local reference table overflow (max=8388608)
static void create_local_ref_too_much(JNIEnv *env, jclass clazz) {
    for (int i = 0; i < 10000000; i++) {
        jclass c = env->FindClass(JNIInterface_CLASS);
    }
}

运行单测testCreateLocalRefTooMuch,在logcat里将会看到如下结果:

create_local_ref_too_much.png

全局引用

  1. 明确释放之前,都是有效的。可以跨方法、跨线程使用。
  2. 全局引用是用局部引用来创建:
jclass local_ref_class = env->FindClass(JNIInterface_CLASS);
jclass global_ref_class = reinterpret_cast<jclass>(env->NewGlobalRef(local_ref_class));
  1. 全局引用创建后,不像局部引用,可以被自动释放,只能手动释放:
env->DeleteGlobalRef(global_ref_class);
  1. 不及时释放不需要的全局引用,可能会导致全局引用表溢出。下列代码,将会导致JNI ERROR (app bug): global reference table overflow (max=51200)
static void create_global_ref_too_much(JNIEnv *env, jclass clazz) {
    for (int i = 0; i < 10000000; i++) {
        jclass c = env->FindClass(JNIInterface_CLASS);
        jclass global_ref = reinterpret_cast<jclass>(env->NewGlobalRef(c));
        env->DeleteLocalRef(c);
    }
}

运行单测testCreateGlobalRefTooMuch,在logcat里将会看到如下结果:

create_global_ref_too_much.png

传递Java基本类型

Java的基本类型,都能在JNI里找到对应的类型。对应表详见《从Java到C++-JNI基本概念》。如:

Java TypeNative TypeDescription
intjintsigned 32 bits

int对应jint。而jint在头文件jni.h的定义为:

typedef int32_t  jint;     /* signed 32 bits */

int32_t其实只是C++中int的别名。

typedef __int32_t     int32_t;

typedef int __int32_t;

Java的基本类型与C++的基本类型的对应关系如下:

Java TypeC++ TypeDescription
booleanboolunsigned 8 bits
bytesigned charsigned 8 bits
charunsigned shortunsigned 16 bits
shortshortsigned 16 bits
intintsigned 32 bits
longlong longsigned 64 bits
floatfloat32 bits
doubledouble64 bits

注意事项:

  1. 该表格仅在Android平台上有效。因为C++与Java不同,它是平台相关的。Java的基本数据类型是固定长度的。但C++不提供这种保证。C++提供了一种灵活的标准,它只确保最小长度(从C语言借鉴而来),如下所示:
    • short至少16位;

    • int至少与short一样长;

    • long至少32位,且至少与int一样长;

    • long long至少64位,且至少与long一样长

  2. Java的long对应C++的long long,不是long
  3. Java的char是16位的,采用的是Unicode编码,两个字节表示一个字符。对应C++的unsigned short。而C++的char是8位的。
  4. Java的byte对应C++的signed char。C++中的char与C++中的intshort(默认是有符号类型)不同,char默认是有符号和无符号,由C++实现决定。经测试,在Android平台,char是等同于signed char的。

测试代码如下:

Java代码:

public native static void nativeTransmitPrimitiveType(int i, long l, float f,
                                                byte _byte, double d, boolean b, short s, char c);

@Test
public void testTransmitPrimitiveType() {
    JNIInterface.nativeTransmitPrimitiveType(Integer.MAX_VALUE, Long.MAX_VALUE, Float.MAX_VALUE,
        Byte.MAX_VALUE, Double.MAX_VALUE, true, Short.MAX_VALUE, '中');
}

C++代码:

static void transmit_primitive_type(JNIEnv *env, jclass clazz, jint i, jlong l, jfloat f,
                                    jbyte byte, jdouble d, jboolean b, jshort s, jchar c) {
    int c_i = numeric_limits<int>::max();
    assert(i == c_i);
    //long等同于long int
    assert(i == numeric_limits<long>::max());

    long long c_l = numeric_limits<long long>::max();
    assert(l == c_l);
    assert(l != numeric_limits<unsigned long long>::max());
    assert(l == numeric_limits<long long int>::max());

    float c_f = numeric_limits<float>::max();
    assert(f == c_f);

    signed char c_byte = numeric_limits<signed char>::max();
    assert(byte == c_byte);
    assert(byte == numeric_limits<char>::max());
    assert(255 == numeric_limits<unsigned char>::max());

    double c_d = numeric_limits<double>::max();
    assert(d == c_d);

    bool c_b = numeric_limits<bool>::max();
    assert(b == c_b);

    short c_s = numeric_limits<short>::max();
    assert(s == c_s);

    unsigned short c_c = 0x4e2d;//'中'的Unicode编码
    assert(2 == sizeof(unsigned short));
    assert(c == c_c);
}

传递Java对象

Java的引用类型与JNI引用类型的对应表详见《从Java到C++-JNI基本概念》。

在native方法中,可以实现:

  • 获取Java对象的各种变量的值
  • 修改Java对象的各种变量
  • 创建Java对象

Java对象定义如下:

public class Position {
    public float longitude;
    public float latitude;
}

public class Image {
    public long id;
    public int width;
    public int height;
    public Position pos = new Position();
    public byte[] data = new byte[10];
}

C++代码:

  1. 定位Java类Image、Position的相关元素:类、变量
struct JavaPosition {
    jclass class_ref;
    jfieldID longitude;
    jfieldID latitude;
};

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    ...
    jclass c = env->FindClass("com/example/java2cpp/bean/Image");
    javaImage.class_ref = reinterpret_cast<jclass>(env->NewGlobalRef(c));
    javaImage.id = env->GetFieldID(c, "id", "J");
    javaImage.width = env->GetFieldID(c, "width", "I");
    javaImage.height = env->GetFieldID(c, "height", "I");
    javaImage.position = env->GetFieldID(c, "pos", "Lcom/example/java2cpp/bean/Position;");
    javaImage.data = env->GetFieldID(c, "data", "[B");
    env->DeleteLocalRef(c);

    c = env->FindClass("com/example/java2cpp/bean/Position");
    javaPosition.class_ref = reinterpret_cast<jclass>(env->NewGlobalRef(c));
    javaPosition.latitude = env->GetFieldID(c, "latitude", "F");
    javaPosition.longitude = env->GetFieldID(c, "longitude", "F");
    env->DeleteLocalRef(c);
    ...
}
  1. 在native方法中,通过JNI的一系列方法,获取Java对象的变量的值
    //获取对象的基本类型变量:通过Get<PrimitiveType>Field系列方法
    jlong id = env->GetLongField(image, javaImage.id);
    jint width = env->GetIntField(image, javaImage.width);
    jint height = env->GetIntField(image, javaImage.height);

    assert(id == 0);
    assert(width == 0);
    assert(height == 0);

    //获取对象的普通引用类型变量:通过GetObjectField
    jobject position = env->GetObjectField(image, javaImage.position);
    if (position != nullptr) {
        jfloat longitude = env->GetFloatField(position, javaPosition.longitude);
        jfloat latitude = env->GetFloatField(position, javaPosition.latitude);
        assert(longitude == 0.0f);
        assert(latitude == 0.0f);
    }

    //获取对象的数组变量:也是通过GetObjectField,需要强转为对应的JNI引用类型,如:
    //byte[]<->jbyteArray
    //int[]<->jintArray
    jbyteArray data = reinterpret_cast<jbyteArray>( env->GetObjectField(image, javaImage.data));
    //获取Java数组内容
    if (data != nullptr) {
        jsize size = env->GetArrayLength(data);
        jboolean isCopy;
        //基本类型数组:使用Get<PrimitiveType>ArrayElements系列方法。
        //引用类型数组:使用GetObjectArrayElement
        jbyte *byte_array_native = env->GetByteArrayElements(data, &isCopy);
        for (int i = 0; i < size; ++i) {
            jbyte b = byte_array_native[i];
            assert(b == 0);
        }
        //第三个参数mode通常为0即可
        env->ReleaseByteArrayElements(data, byte_array_native, 0);
    }
  1. 修改Java对象的变量的值
    //修改对象的基本类型变量:通过Set<PrimitiveType>Field系列方法
    env->SetLongField(image, javaImage.id, 999);
    env->SetIntField(image, javaImage.width, 1920);
    env->SetIntField(image, javaImage.height, 1080);

    //修改对象的普通引用类型变量:通过SetObjectField
    //创建一个Java对象:通过AllocObject
    jobject newPosition = env->AllocObject(javaPosition.class_ref);
    env->SetFloatField(newPosition, javaPosition.longitude, 9.9f);
    env->SetFloatField(newPosition, javaPosition.latitude, 99.9f);
    env->SetObjectField(image, javaImage.position, newPosition);

    //修改对象的数组类型变量:也是通过SetObjectField
    //创建Java基本类型的数组:通过New<PrimitiveType>Array系列方法
    //创建Java引用类型的数组:通过NewObjectArray
    jbyteArray newData = env->NewByteArray(10);
    int len = 1;
    for (int i = 0; i < 10; ++i) {
        jbyte b = i;
        //基本类型数组通过Set<PrimitiveType>ArrayRegion系列方法设置数组的元素
        //Set<PrimitiveType>ArrayRegion其实可以直接设置整个C++数组给newData;
        //如果是引用类型数组,则要通过SetObjectArrayElement一个个设置数组的元素
        env->SetByteArrayRegion(newData, i, len, &b);
    }
    env->SetObjectField(image, javaImage.data, newData);


    //直接修改Java数组的内容:大体流程与获取Java数组内容一致
    jbyteArray extraData = reinterpret_cast<jbyteArray>(env->GetObjectField(image,
                                                                            javaImage.extra_data));
    if (data != nullptr) {
        jsize size = env->GetArrayLength(extraData);
        jboolean isCopy;
        jbyte *byte_array_native = env->GetByteArrayElements(extraData, &isCopy);
        for (int i = 0; i < size; i++) {
            //更改数组的内容
            byte_array_native[i] = i;
        }
        //将缓冲数组的内容拷贝回原数组,释放缓冲数组。
        env->ReleaseByteArrayElements(extraData, byte_array_native, 0);
    }

注意事项:

  1. 获取Java对象的各种变量的值
    • 获取对象的基本类型变量:通过Get<PrimitiveType>Field系列方法
    • 获取对象的普通引用类型变量:通过GetObjectField方法
    • 获取对象的数组类型变量:也是通过GetObjectField方法,还需要强转为对应的JNI引用类型。比如:byte[]<->jbyteArrayint[]<->jintArray
    • 获取静态变量:和获取上述的成员变量类似,只是方法名的Get后面多了一个Static。方法的第一个参数也由jobject,变成了jclass
      • 基本类型:通过GetStatic<PrimitiveType>Field系列方法
      • 普通引用类型、数组类型:通过GetStaticObjectField方法
    • 获取Java数组内容:
      • 获取数组大小:通过GetArrayLength方法
      • 获取数组内容:
        • 基本类型数组:通过Get<PrimitiveType>ArrayElements系列方法
        • 引用类型数组:通过GetObjectArrayElement方法
      • 获取Java数组后,需要释放缓冲数组:通过ReleaseByteArrayElements方法
  2. 修改Java对象的各种变量
    • 修改对象的基本类型变量:通过Set<PrimitiveType>Field系列方法
    • 修改对象的普通引用类型变量:通过SetObjectField方法
    • 修改对象的数组类型变量:也是通过SetObjectField方法
    • 修改Java数组的元素:
      • 基本类型数组:通过Set<PrimitiveType>ArrayRegion系列方法设置数组的元素。Set<PrimitiveType>ArrayRegion其实可以直接设置整个C++数组给Java数组
      • 引用类型数组:通过SetObjectArrayElement方法一个个设置数组的元素
  3. 创建Java对象
    • 创建普通Java对象:通过AllocObject方法
    • 创建数组:
      • 基本类型的数组:通过New<PrimitiveType>Array系列方法
      • 引用类型的数组:通过NewObjectArray

测试代码:testFillImage

调用Java方法

在native方法中,可以通过JNI方法调用各种Java方法。无论Java方法是public,还是private,都不会影响JNI的调用。

Java代码:

public class Image {
    private static final String TAG = "Image";
    ...
    public Position getPos() {
        return pos;
    }

    private byte[] getData() {
        return data;
    }
}

class JNIInterface {
    public native static Position nativeGetImagePos(Image image);

    public native static byte[] nativeGetImageData(Image image);

    public native static int nativeAdd(int i, int j);
}

C++代码:

  1. 元素定位:略
  2. 在native中调用对应的Java方法
static jobject get_image_pos(JNIEnv *env, jclass clazz, jobject image) {
    jobject pos = env->CallObjectMethod(image, javaImage.getPos);
    return pos;
}

static jbyteArray get_image_data(JNIEnv *env, jclass clazz, jobject image) {
    jbyteArray data = reinterpret_cast<jbyteArray>(env->CallObjectMethod(image, javaImage.getData));
    return data;
}

static jint add(JNIEnv *env, jclass clazz, jint i, jint j) {
    jint result = env->CallStaticIntMethod(javaCalculator.class_ref, javaCalculator.add, i, j);
    return result;
}

注意事项:

  1. Java成员方法通过Call<return type>Method系列的方法调用,如CallObjectMethodCallIntMethodCallVoidMethod等。
    • 参数1固定是Java对象,参数2固定是方法Id。
    • 后续参数是Java方法参数,没有则不传。
  2. Java静态方法通过CallStatic<return type>Method系列的方法调用,如CallStaticObjectMethodCallStaticIntMethodCallStaticVoidMethod等。
    • 参数1固定是Java类,参数2固定是方法Id。
    • 后续参数是Java方法参数,没有则不传。

测试代码:testCallJavaMethodInNative

处理Java调用异常

C++调用Java方法时,如果发生了异常,需要通过JNI的相关方法进行处理。

Java代码:

class JNIInterface {
    private static void javaThrowException() {
        throw new NullPointerException();
    }
    
    public native static int nativeHandleJavaException();
}

C++代码:

  1. 元素定位:略

  2. 处理调用Java方法时发生的异常

jint handle_java_exception(JNIEnv *env, jclass clazz) {
    //产生了一个Java异常
    env->CallStaticVoidMethod(javaJNIInterface.class_ref, javaJNIInterface.javaThrowExceptMethod);
    //在Clear之前,不能调用除ExceptionXxx系列之外的JNI方法
    //env->FindClass("com/example/java2cpp/JNIInterface");
    if (env->ExceptionCheck()) {
        env->ExceptionClear();
        return -1;
    } else {
        return 0;
    }
}

注意事项:

  1. 先通过ExceptionCheck检查方法调用是否产生异常,然后通过ExceptionClear处理。ExceptionClear的作用相当于try catch
  2. 在Clear之前,不能调用除ExceptionXxx系列之外的JNI方法。
  3. 还可以通过ExceptionDescribe获取异常描述,通过ExceptionOccurred获取异常对象。暂不提供代码示例。

测试代码:testHandleJavaExceptiontestUnHandleJavaException

传递枚举

Java枚举实际上是个对象。而C++的枚举,可以自动转换为int值,或者由int强转为枚举。

传递Java枚举到C++层,或者转换C++枚举到Java层,需要做一些额外的处理。

Java代码:

public enum ImageFormat {
    RGB_888, NV21, NV12
}

class JNIInterface {
    public native static ImageFormat nativeTransmitEnum(int iFormat, ImageFormat format);
}

C++代码:

  1. C++层对应枚举:
enum image_format {
    RGB_888, NV21, NV12
};
  1. 元素定位:
    jclass c = env->FindClass("com/example/java2cpp/bean/ImageFormat");
    javaImageFormat.class_ref = reinterpret_cast<jclass>(env->NewGlobalRef(c));
    javaImageFormat.name = env->GetMethodID(c, "name", "()Ljava/lang/String;");
    javaImageFormat.ordinal = env->GetMethodID(c, "ordinal", "()I");
    javaImageFormat.values = env->GetStaticMethodID(c, "values",
                                                "()[Lcom/example/java2cpp/bean/ImageFormat;");
    env->DeleteLocalRef(c);
  1. 转换Java枚举成C++枚举、转换C++枚举成Java枚举:
static jobject transmit_enum(JNIEnv *env, jclass clazz, jint i_format, jobject format) {
    //如果传的是java枚举的下标
    //c++的枚举,可以自动转为int
    assert(i_format == image_format::NV21);

    //int无法自动转换为c++枚举,需要强转
    image_format nv21 = static_cast<image_format>(i_format);
    assert(nv21 == image_format::NV21);

    //调用Java枚举的ordinal方法,获取对应枚举的下标
    image_format c_format = static_cast<image_format> (env->CallIntMethod(format,
                                                                          javaImageFormat.ordinal));
    assert(c_format == image_format::NV21);


    image_format c_rgb = image_format::RGB_888;
    //调用Java枚举的静态方法values,获取枚举集合
    jobjectArray values = reinterpret_cast<jobjectArray>(env->CallStaticObjectMethod(
            javaImageFormat.class_ref, javaImageFormat.values));
    //获取对应的Java枚举
    jobject java_format = env->GetObjectArrayElement(values, c_rgb);
    return java_format;
}

注意事项:

  1. 转换过程中,需要用到Java枚举的ordinalvalues方法。
  2. C++的枚举,可以自动转换为int值。但反之则不行,需要强转。
  3. 如代码中所示,直接传递Java枚举进入native,还得通过Java枚举的ordinal方法来获取枚举的下标。所以建议直接传递枚举的下标给native层。

测试代码:testTransmitEnum

传递字符串

在Java、C++之间传递字符串,需要注意字符串编码的格式。

传递UTF-8编码

static jstring transmit_string(JNIEnv *env, jclass clazz, jstring s) {
    jboolean isCopy;
    const char *chars = env->GetStringUTFChars(s, &isCopy);
    std::string c_s = "中国";
    assert(strcmp(chars, c_s.c_str()) == 0);

    char c_chars[7];
    c_chars[0] = 0xE4;//E4B8AD即"中"的UTF-8编码
    c_chars[1] = 0xB8;
    c_chars[2] = 0xAD;
    c_chars[3] = 0xE5;//E59BBD即"国"的UTF-8编码
    c_chars[4] = 0x9B;
    c_chars[5] = 0xBD;
    c_chars[6] = '\0';//'\0',C语言里用来表示字符串的结尾

    assert(strcmp(chars, c_chars) == 0);

    //使用完,要及时释放
    env->ReleaseStringUTFChars(s, chars);

    return env->NewStringUTF(c_chars);
}

注意事项:

  1. native方法获取到Java字符串,使用GetStringUTFChars
  2. 使用完后,必须调用ReleaseStringUTFChars释放获取到的字符数组指针。因为GetStringUTFChars调用了C++的关键字new,创建对应的字符数组。
  3. native方法返回字符串给Java层,需要先调用NewStringUTF创建Java字符串。

测试代码:testTransmitString

传递其他编码

比如从C++层获取的字符数组是GB2312编码的,想要回传到Java层。

C++代码:

  1. 元素定位
    jclass c = env->FindClass("java/lang/String");
    javaString.class_ref = reinterpret_cast<jclass>(env->NewGlobalRef(c));
    //java.lang.String的一个构造方法
    javaString.constructor = env->GetMethodID(c, "<init>", "([BLjava/lang/String;)V");
    env->DeleteLocalRef(c);

2.使用GB2312编码的字符数组,构造Java String

static jstring native_get_GB2312String(JNIEnv *env, jclass clazz) {
    char c_str[5];
    c_str[0] = 0xD6;//D6D0即"中"
    c_str[1] = 0xD0;
    c_str[2] = 0xB9;//B9FA即"国"
    c_str[3] = 0xFA;
    c_str[4] = 0x00;//即'\0',C语言里用来表示字符串的结尾

    jbyteArray bytes = env->NewByteArray((jsize) strlen(c_str));
    env->SetByteArrayRegion(bytes, 0, (jsize) strlen(c_str), (jbyte *) c_str);
    jstring encoding = env->NewStringUTF("gb2312");

    return reinterpret_cast<jstring>(env->NewObject(javaString.class_ref, javaString.constructor,
                                                    bytes, encoding));
}

注意事项:

  1. 这里使用了String的一个构造方法,构造方法的类型签名是<init>
  2. 这里不是通过AllocObject方法创建String对象。而是通过JNI的NewObject来调用String的特定构造方法。

测试代码:testGetGB2312

参考资料

Oracle的Java Native Interface Specification