在进行JNI开发之前,我们必须要了解下面一些东西,首先JNI开发的流程,再然后我们需要了解一下Native和Java中类型映射关系,以及JVM中已经为我们提供的一些常用JNI函数,最后再了解一下C/C++开发的一些注意事项。

JNI开发流程

  • 编写包含Native方法的Java代码

  • 编译Java代码(javac)

  • 创建C/C++头文件(javah)

  • 编写C/C++代码

  • 创建库文件

    # gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -std=c++11 -shared -o opt_jni.so My.cpp
    

原始类型和Native类型映射

Java Type Native Type Description
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A

引用类型映射

typedef struct _jobject *jobject;
typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;

类型签名

Type Signature Java Type
Z boolean
B byte
C char
S short
I int
J long
F float
D double
L fully-qualified-class ; fully-qualified-class
V void
[ type type[]
( arg-types ) ret-type method type

例如Java方法:

long f (int n, String s, int[] arr); 

在Native层面对应的类型签名为:

(ILjava/lang/String;[I)J

()内为方法的参数类型签名,()外为方法方法返回类型。类型签名在函数重载时就会变得很有用。

常用JNI函数

类操作

//获取Java类
jclass FindClass(JNIEnv *env, char *className) ;
//确定clazz1的对象是否可以安全地转换为clazz2
jboolean IsAssignableFrom(JNIEnv *env, jclass clazz1,jclass clazz2);

数组操作

//返回数组长度
jsize GetArrayLength(JNIEnv *env, jarray array);
// 创建对象数组
jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);
// 构建基础数组对象,返回jbooleanArray、jbyteArray、jintArray...
ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);

//设置Object数组元素
void SetObjectArrayElement(JNIEnv *env, jobjectArray array,jsize index, jobject value);

// 通知VM本机代码不再需要访问elems
void Release<PrimitiveType>ArrayElements(JNIEnv *env,ArrayType array, NativeType *elems, jint mode);

//返回数组元素的指针
NativeType *Get<PrimitiveType>ArrayElements(JNIEnv *env,ArrayType array, jboolean *isCopy);
//释放数组使用的内存
ReleaseIntArrayElements(JNIEnv *env,jintArray array,int *elems,mode);

//数组拷贝,从buf从复制到array
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array,jsize start, jsize len, const NativeType *buf);

对象操作函数

//在不调用对象的任何构造函数的情况下分配新的Java对象
jobject AllocObject(JNIEnv *env, jclass clazz);

//将所有要传递给构造函数的参数紧跟在methodID参数之后。NewObject()接受这些参数并将它们传递给希望调用的Java方法
jobject NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, ...);
//功能同NewObject,但参数放在数组中
jobject NewObjectA(JNIEnv *env, jclass clazz,jmethodID methodID, const jvalue *args);
//功能同NewObject,但参数放在va_list中
jobject NewObjectV(JNIEnv *env, jclass clazz,jmethodID methodID, va_list args);

// 返回字段ID
jfieldID GetFieldID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
// 返回字段的内容
NativeType Get<type>Field(JNIEnv *env, jobject obj,jfieldID fieldID);
//设置字段内容,如SetObjectField、SetIntField
void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID,NativeType value);

//返回类或接口实例(非静态)方法的ID
jmethodID GetMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
// 调用指定返回类型的方法,如CallVoidMethod、CallObjectMethod、CallLongMethod
NativeType Call<type>Method(JNIEnv *env, jobject obj,jmethodID methodID, ...);

字符串操作

//返回字符串长度
jsize GetStringLength(JNIEnv *env, jstring string);

//返回指向字符串的Unicode字符数组的指针
const jchar * GetStringChars(JNIEnv *env, jstring string,jboolean *isCopy);
//通知JVM不再使用该字符指针
void ReleaseStringChars(JNIEnv *env, jstring string,const jchar *chars);

//返回UTF8编码表示的字符数组指针
const char * GetStringUTFChars(JNIEnv *env, jstring string,jboolean *isCopy);
// 通知JVM不再使用该字符数组指针
void ReleaseStringUTFChars(JNIEnv *env, jstring string,const char *utf);

其它

//
jint ThrowNew(JNIEnv *env, jclass clazz,const char *message);

更多可以参考:JNI Functions

开发说明

语法

在进行native开发时,我们需要调用native库中提供的方法,对于c和c++调用语法是存在区别:

# C 语法
cls = (*env)->FindClass(env, "Sample2");
# C++ 语法
cls = env->FindClass("Sample2");

归其原因主要是c是面向过程,c++是面向对象语言。

C++对象初始化

A a = A(); //code1
A* a=new A();//code2

上面两行代码的区别在于,code1在栈上开辟内存,code2是在堆上开辟内存,所以对应code2,我们在对象不再使用后,用做内存的手动释放,即delete a;

*&的区别

C/C++开发,可能最让人头疼的就是指针和引用了,所以在JNI开发时,我们也回避不了这个问题,所以我们还是要复习一下这两个概念:指针是一个变量指向的是另外一个变量地址,而引用相当于给变量起了一个别名。

例如:

int i; // 声明简单的变量
int *ip; // 指针变量的声明
int& r = i;// 声明引用变量
ip = &i // 在指针变量中存储 var 的地址
cout << ip << endl;//打印的是指向的地址
cout << *ip << endl;//打印的是指向地址中的内容

从上面我们发现不管*还是&,都有两种概念,第一他们本身可以作为指针/引用的声明,第二,对于*ip我们可以看作是取值操作,而对于&var我们可以看作这是一个取址操作,他获取的是变量的地址。

说到这,再多补充一点吧,C/C++函数定义时,有些参数定义为指针类型参数、有些定义成引用类型的参数,例如:

void foo1(int* param)
void foo2(int& param)

其实不管是指针传参还是引用传参,相同的是他们都可以改变实参的内容,不同的是:

  • 指针可以指向NULL,而引用不能引用NULL
  • 指针可以随时改变指向的对象,而引用一旦被定义就不能改变引用关系

当然还有一个直观的区别,在编码时我们就可以发现:

int i = 10;
fool(&i);
foo2(i);

貌似定义为引用时,代码看起来更加优雅。

线程安全

推荐:

env->MonitorEnter(obj);
/**
同步
*/
env->MonitorExit(obj);

其它方法:

直接在Java的native方法定义上加synchronized限定。