从Java到C++:JNI实战

从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 Type Native Type Description
int jint signed 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 Type C++ Type Description
boolean bool unsigned 8 bits
byte signed char signed 8 bits
char unsigned short unsigned 16 bits
short short signed 16 bits
int int signed 32 bits
long long long signed 64 bits
float float 32 bits
double double 64 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

猜你喜欢

转载自blog.csdn.net/qq_34356130/article/details/123600149