JNI实现java与c/c++相互通讯
JNI获取Java类的方法和字段,都需要一个很重要的参数,就是Java类的方法和字段的签名。所以最好能够记住它们
签名 | java类型 |
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
V | void |
Lfully-qualified-class; | fully-qualified-class |
[type | type[] |
(arg-types)ret-type | method type |
"Lfully-qualified-class;"->L类全名; 例如Java String类对应的签名是Ljava/lang/String;
"[type"->java数组的签名,例如int[]的签名[I,java Stringg[]的签名是[Ljava/lang/String;
"(arg-types)ret-type"->(函数参数)返回值,()只是所有参数,ret-type是返回类型签名例如
void test(String msg)对应的签名是(Ljava/lang/String;)V
long f(int n,String s,int[] arr)对应的签名(Ijava/lang/String;[I)J
void f()对应用的签名()V
jni的native接口中,第一个参数为JNI接口指针(JNIEnv),第二个参数根据native方法是静态还是非静态而不同。非静态native方法的第二个参数是对该对象的引用,静态方法的第二参数是对其java类的引用。其与参数对应Java方法参数。
从上述描述中,如果是非静态的,我们可以拿到对象的引用。通过对象的引用,我们可以访问该对象的字段和方法。如果是静态的,我们可以访问该对象的静态方法和静态字段。那么jni中具体是如何访问的呢?我们将在下面的章节通过实例来介绍
java代码
public void show(String s){ Log.i("MainActivity","show:"+s); }
public native void showString(String s);
jni代码
//访问Java中的show方法 JNIEXPORT void JNICALL Java_com_zzy_ndkdemo_MainActivity_showString(JNIEnv *env, jobject instance, jstring s) { //获取instance的类名称 jclass cls = (*env)->GetObjectClass(env,instance); if(cls==NULL) { LOGD("Class %s not found"); } //获取方法ID,第二个参数为类名称,第三个参数为方法名称,第三个参数为方法签名,详细参见签名对照表 jmethodID id =(*env)->GetMethodID(env,cls,"show","(Ljava/lang/String;)V"); if(id !=NULL) { //访问方法,第二个为类实例,第三个参数为方法ID,第四和第四以后为方法参数, // 根据返回类型不同,调用不同的CallXXXMethod方法,xxx返回类型 (*env)->CallVoidMethod(env,instance,id,s); } }
java代码
public static void showStatic(String s){ Log.i("MainActivity","show static:"+s); }
public native void showStaticString(String s);
jni代码
//访问Java中的showStatic静态方法 JNIEXPORT void JNICALL Java_com_zzy_ndkdemo_MainActivity_showStaticString(JNIEnv *env, jobject instance, jstring s) { //获取instance的类名称 jclass cls = (*env)->GetObjectClass(env,instance); if(cls==NULL) { LOGD("Class %s not found"); } //获取静态方法ID,第二个参数为类名称,第三个参数为方法名称,第三个参数为方法签名,详细参见签名对照表 jmethodID id =(*env)->GetStaticMethodID(env,cls,"showStatic","(Ljava/lang/String;)V"); if(id !=NULL) { //访问方法,第二个为类名称,第三个参数为方法ID,第四和第四以后为方法参数, // 根据返回类型不同,调用不同的CallStaticXXXMethod方法,xxx返回类型 (*env)->CallStaticVoidMethod(env,cls,id,s); } }
1、从中我们可以看出静态方法比非静态方法的访问多加了一个static,访问函数由类实例,变成类引用。
2、如果静态方法不存在instance类中,我们可以通过FindClass进行访问,其中第二个参数为类的全路径
jclass cls = (*env)->FindClass(env, "com/zzy/ndkdemo/JniDemo"); if (cls == NULL) { LOGD("Class %s not found"); }
java代码
User user = new User(); user.name = "zhang san"; user.age = 30; User.token = "2018-2011—3223"; String name = showUserName(user); Log.i("MainActivity","show name:"+name);
public native String showUserName(User user);
jni代码
//访问User中的字段name JNIEXPORT jstring JNICALL Java_com_zzy_ndkdemo_MainActivity_showUserName(JNIEnv *env, jobject instance, jobject user) { //获取User的类名称 jclass cls=(*env)->GetObjectClass(env,user); if (cls == NULL) { LOGD("Class %s not found"); return NULL; } //获取字段ID,第二个参数类名称,第三个参数字段名,第三个参数字段签名,详细参见签名对照表 jfieldID id = (*env)->GetFieldID(env,cls,"name","Ljava/lang/String;"); if(id==NULL) { LOGD("Field token not found"); return NULL; } //获取字段内容,调用GetXXXField,xxx为字段类型。除基本类型外,其他的都使用GetObjectField jstring name=(jstring) (*env)->GetObjectField(env,user,id); return name; }
java代码
User user = new User(); user.name = "zhang san"; user.age = 30; User.token = "2018-2011—3223"; String token =showUserStaticToken(user); Log.i("MainActivity","show static token:"+token);
public native String showUserStaticToken(User user);
jni代码
//访问User中的静态字段Token JNIEXPORT jstring JNICALL Java_com_zzy_ndkdemo_MainActivity_showUserStaticToken(JNIEnv *env, jobject instance, jobject user) { //获取User的类名称 jclass cls=(*env)->GetObjectClass(env,user); if (cls == NULL) { LOGD("Class %s not found"); return NULL; } //获取静态字段ID,第二个参数类名称,第三个参数字段名,第三个参数字段签名,详细参见签名对照表 jfieldID id = (*env)->GetStaticFieldID(env,cls,"token","Ljava/lang/String;"); if(id==NULL) { LOGD("Field token not found"); return NULL; } //获取字段内容,调用GetStaticXXXField,xxx为字段类型。除基本类型外,其他的都使用GetStaticObjectField jstring token=(*env)->GetStaticObjectField(env,cls,id); return token; }
1、从中我们可以看出静态字段比非静态字段的访问多加了一个static,访问函数由类实例,变成类引用。
2、如果静态字段中不存在instance类中,我们可以通过FindClass进行访问,其中第二个参数为类的全路径
jclass cls = (*env)->FindClass(env, "com/zzy/ndkdemo/User"); if (cls == NULL) { LOGD("Class %s not found"); }
java代码
User user = updateUser(user); Log.i("MainActivity","updateUser name:"+user.name+" age:"+user.age+" token:"+User.token);
public native User updateUser(User user);
jni代码
//更新user内容 JNIEXPORT jobject JNICALL Java_com_zzy_ndkdemo_MainActivity_updateUser(JNIEnv *env, jobject instance,jobject user) { //获取User的类名称 jclass cls=(*env)->GetObjectClass(env,user); if (cls == NULL) { LOGD("Class %s not found"); return NULL; } //获取每一个字段ID,第二个参数类名称,第三个参数字段名,第三个参数字段签名,详细参见签名对照表 jfieldID idName = (*env)->GetFieldID(env,cls,"name","Ljava/lang/String;"); jfieldID idAge = (*env)->GetFieldID(env,cls,"age","I"); jfieldID idToken = (*env)->GetStaticFieldID(env,cls,"token","Ljava/lang/String;"); //将cha字符串转成jstring jstring name = (*env)->NewStringUTF(env, "李四"); jstring token = (*env)->NewStringUTF(env, "new token"); //更新字段内容,调用setxxxField,xxx为字段类型,如果是静态字段还需要加上static //第二个参数为需要修改的类实例或则类名,第三个参数为字段Id,第四个参数为需要修改的内容 (*env)->SetObjectField(env,user,idName,name); (*env)->SetIntField(env,user,idAge,20); (*env)->SetStaticObjectField(env,cls,idToken,token); return user; }
java代码
User user = createUser(); Log.i("MainActivity","createUser name:"+user.name+" age:"+user.age+" token:"+User.token);
public native User createUser();
jni代码
//创建user实例 JNIEXPORT jobject JNICALL Java_com_zzy_ndkdemo_MainActivity_createUser(JNIEnv *env, jobject instance) { //在指定路径查找到USer类名称 jclass cls = (*env)->FindClass(env, "com/zzy/ndkdemo/User"); if (cls == NULL) { LOGD("Class %s not found"); } //获取User类的构造方法ID,第二个参数为类名称,第三个参数固定为"" //第四个参数为构造函数签名,详细参见签名对照表 jmethodID id= (*env)->GetMethodID(env, cls, "" , "()V"); //实例化User类,第二个参数类名称,第三个参数构造方法ID jobject user = (*env)->NewObject(env, cls, id); if (user == NULL) { LOGD("Create User failed"); } //获取每一个字段ID,第二个参数类名称,第三个参数字段名,第三个参数字段签名,详细参见签名对照表 jfieldID idName = (*env)->GetFieldID(env,cls,"name","Ljava/lang/String;"); jfieldID idAge = (*env)->GetFieldID(env,cls,"age","I"); jfieldID idToken = (*env)->GetStaticFieldID(env,cls,"token","Ljava/lang/String;"); //将cha字符串转成jstring jstring name = (*env)->NewStringUTF(env, "王五"); jstring token = (*env)->NewStringUTF(env, "second token"); //赋予字段内容,调用setxxxField,xxx为字段类型,如果是静态字段还需要加上static //第二个参数为需要修改的类实例或则类名,第三个参数为字段Id,第四个参数为需要修改的内容 (*env)->SetObjectField(env,user,idName,name); (*env)->SetIntField(env,user,idAge,10); (*env)->SetStaticObjectField(env,cls,idToken,token); return user; }
JNI中也有异常,不过它和C++、Java的异常不太一样。当调用JNIEnv的某些函数出错后,会产生一个异常,但这个异常不会中断本地函数的执行,直到从JNI层返回到Java层后,虚拟机才会抛出这个异常。虽然在JNI层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能做一些资源清理工作了(例如释放全局引用,或者ReleaseStringChars)。如果这时调用除上面所说函数之外的其他JNIEnv函数,则会导致程序死掉。JNIEnv提供了三个函数进行帮助:
1、ExceptionOccured函数,用来判断是否发生异常。
2、ExceptionClear函数,用来清理当前JNI层中发生的异常。
3、ThrowNew函数,用来向Java层抛出异常。
int jniCheckException(JNIEnv *env) { jthrowable ex = (*env)->ExceptionOccurred(env); if (ex) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); (*env)->DeleteLocalRef(env, ex); return 1; } return 0; }
在JNI中访问方法和字段,都需要走好几个步骤,如果其中一个步骤失败了,就会导致整个访问过程的失败、甚至死机。所以我们在每个函数的调用过程中,尽量对其做异常检测。以下为常用函数的封装。
jobject jniGlobalRef(JNIEnv *env, jobject cls) { jobject gcls = (*env)->NewGlobalRef(env, cls); if (gcls == NULL) LOGE("Global ref failed (out of memory?)"); return gcls; } jclass jniFindClass(JNIEnv *env, const char *name) { jclass cls = (*env)->FindClass(env, name); if (cls == NULL) LOGE("Class %s not found", name); else jniCheckException(env); return cls; } jmethodID jniGetMethodID(JNIEnv *env, jclass cls, const char *name, const char *signature) { jmethodID method = (*env)->GetMethodID(env, cls, name, signature); if (method == NULL) { LOGE("Method %s %s not found", name, signature); jniCheckException(env); } return method; } jfieldID jniGetFieldID(JNIEnv *env, jclass cls, const char *name, const char *type) { jfieldID field = (*env)->GetFieldID(env, cls, name, type); if (field == NULL) LOGE("Field %s type %s not found", name, type); return field; } jobject jniNewObject(JNIEnv *env, jclass cls, jmethodID constructor, const char *name) { jobject object = (*env)->NewObject(env, cls, constructor); if (object == NULL) LOGE("Create object %s failed", name); else jniCheckException(env); return object; }
JNI 程序开发者要遵循 native 语言本身的内存管理机制,避免造成内存泄漏。以 C 语言为例,当用 malloc() 在进程堆中动态分配内存时,JNI 程序在使用完后,应当调用 free() 将内存释放。总之,所有在 native 语言编程中应当注意的内存泄漏规则,在 JNI 编程中依然适应。Native 语言本身引入的内存泄漏会造成 native memory 的内存,严重情况下会造成 native memory 的 out of memory。如上述中使用到的NewStringUTF和NewObject在使用完成后,都需要进行释放。Demo:https://github.com/zhao007z4/NDKDemo