一、 Java和JNI类型对照
1.1 基本类型对照表
| Java类型 | Native类型 | C/C++类型 | 大小 |
|---|---|---|---|
| Boolean | jboolean | unsigned char | 无符号8位 |
| Byte | jbyte | char | 有符号8位 |
| Char | jchar | unsigned short | 无符号16位 |
| Short | jshort | short | 有符号16位 |
| Integer | jint | int | 有符号32位 |
| Long | jlong | long long | 有符号64位 |
| Float | jfloat | float | 32位浮点值 |
| Double | jdouble | double | 64位双精度浮点值 |
1.2 引用类型对照表
| Java类型 | Native类型 |
|---|---|
| java.lang.Class | jclass |
| java.lang.Throwable | jthrowable |
| java.lang.String | jstring |
| java.lang.Object | jobject |
| java.util.Objects | jobjects |
| java.lang.Object[] | jobjectArray |
| Boolean[] | jbooleanArray |
| Byte[] | jbyteArray |
| Char[] | jcharArray |
| Short[] | jshortArray |
| int[] | jintArray |
| long[] | jlongArray |
| float[] | jfloatArray |
| double[] | jdoubleArray |
| 通用数组 | jarray |
说明任何Java数组在JNI里面都可以使用jarray来表示,比如Java的int[]数组,用JNI可以表示为jintArray,也可以表示为jarray |
二、JNI函数详解
2.1 JNI函数的解析
首先我们需要在Java代码里面声明Native方法原型,比如:
public native void helloJNI(String msg);
其次我们需要在C/C++代码里面声明JNI方法原型,比如:
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_helloJNI(JNIEnv* env, jobject thiz,jstring msg) {
//do something
}
JNI函数的原型
[extern “C”]JNIEXPORT 函数返回值 JNICALL 完整的函数声明(JNIENV *env, jobject thiz, …)
extern "C"根据需要动态添加,如果是C++代码,则必须要添加extern “C”声明,如果是C代码,则不用添加。
JNIEXPORT 这个关键字说明这个函数是一个可导出函数,学过C/C++的朋友都知道,C/C++ 库里面的函数有些可以直接被外部调用,有些不可以,原因就是每一个C/C++库都有一个导出函数列表,只有在这个列表里面的函数才可以被外部直接调用,类似Java的public函数和private函数的区别。
JNICALL 说明这个函数是一个JNI函数,用来和普通的C/C++函数进行区别,实际发现不加这个关键字,Java也是可以调用这个JNI函数的。
Void 说明这个函数的返回值是void,如果需要返回值,则把这个关键字替换成要返回的类型即可。
Java_com_kgdwbb_jnistudy_MainActivity_helloJNI(JNIEnv*env, jobject thiz,jstring msg)这是完整的JNI函数声明,JNI函数名的原型如下:
Java_ + JNI方法所在的完整的类名,把类名里面的”.”替换成”_” + 真实的JNI方法名,这个方法名要和Java代码里面声明的JNI方法名一样+ JNI函数必须的默认参数(JNIEnv* env, jobjectthiz)
env参数是一个指向JNIEnv函数表的指针,thiz参数代表的就是声明这个JNI方法的Java类的引用msg参数就是和Java声明的JNI函数的msg参数对于的JNI函数参数
2.2 静态JNI方法和实例JNI方法区别
Java代码:
public native void showHello();
public native static void showHello2();
JNI代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_showHello(JNIEnv* env, jobject thiz) {
//do something
}
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_showHello2(JNIEnv* env, jclass thiz) {
//do something
}
普通的JNI方法对应的JNI函数的第二个参数是jobject类型,而静态的JNI方法对应的JNI函数的第二个参数是jclass类型
三、JNI常用函数
3.1 JNI字符串相关的函数
C/C++字符串转JNI字符串
NewString函数用来生成Unicode JNI字符串
NewStringUTF函数用来生成UTF-8 JNI字符串
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testJString(JNIEnv* env, jobject thiz,jstring jstr) {
char *str="helloboy";
jstring jstr2=env->NewStringUTF(str);
const jchar *jchar2=env->GetStringChars(jstr,NULL);
size_t len=env->GetStringLength(jstr);
jstring jstr3=env->NewString(jchar2,len);
}
JNI字符串转C/C++字符串
GetStringChars函数用来从jstring获取Unicode C/C++字符串
GetStringUTFChars函数用来从jstring获取UTF-8 C/C++字符串
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testJString(JNIEnv* env, jobject thiz,jstring jstr) {
const char *str=env->GetStringUTFChars(jstr,NULL);
const jchar *jchar2=env->GetStringChars(jstr,NULL);
}
释放JNI字符串
ReleaseStringChars函数用来释放Unicode C/C++字符串
ReleaseStringUTFChars函数用来释放UTF-8 C/C++字符串
当你的本地代码使用完通过 GetStringUTFChars 获取的 UTF-8 字符串后,调用 ReleaseStringUTFChars 表明本地代码不再需要 GetStringUTFChars 返回的 UTF-8 字符串了,就能够释放掉 UTF-8 字符串占用的内存。不调用 ReleaseStringUTFChars 进行内存释放会导致内存泄漏,最终导致内存耗尽。
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testJString(JNIEnv* env, jobject thiz,jstring jstr) {
const char *str=env->GetStringUTFChars(jstr,NULL);
env->ReleaseStringUTFChars(jstr,str);
const jchar *jchar2=env->GetStringChars(jstr,NULL);
env->ReleaseStringChars(jstr,jchar2);
}
JNI字符串截取
GetStringRegion函数用来截取Unicode JNI字符串
GetStringUTFRegion函数用来截取UTF-8 JNI字符串
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testJString(JNIEnv* env, jobject thiz,jstring jstr) {
const char *str=env->GetStringUTFChars(jstr,NULL);
char *subStr=new char;
env->GetStringUTFRegion(jstr,0,3,subStr);
env->ReleaseStringUTFChars(jstr,str);
const jchar *jchar2=env->GetStringChars(jstr,NULL);
jchar *subJstr=new jchar;
env->GetStringRegion(jstr,0,3,subJstr);
env->ReleaseStringChars(jstr,jchar2);
}
获取JNI字符串的长度
GetStringLength用来获取Unicode JNI字符串的长度
GetStringUTFLength函数用来获取UTF-8 JNI字符串的长度
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testJString(JNIEnv* env, jobject thiz,jstring jstr) {
jsize len=env->GetStringLength(jstr);
jsize len2=env->GetStringUTFLength(jstr);
}
3.2 JNI数组相关的函数
JNI数组相关的类
| Java类型 | Native类型 |
|---|---|
| java.lang.Object[] | jobjectArray |
| Boolean[] | jbooleanArray |
| Byte[] | jbyteArray |
| Char[] | jcharArray |
| Short[] | jshortArray |
| int[] | jintArray |
| long[] | jlongArray |
| float[] | jfloatArray |
| double[] | jdoubleArray |
| 通用数组 | jarray |
说明任何Java数组在JNI里面都可以使用jarray来表示,比如Java的int[]数组,用JNI可以表示为jintArray,也可以表示为jarray |
获取JNI基本类型数组元素
Get< Type>ArrayElements函数用来获取基本类型JNI数组的元素,这里面的< Type>需要被替换成实际的类型,比如GetIntArrayElements,GetLongArrayElements等
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testJIntArray(JNIEnv* env, jobject thiz,jintArray array) {
jint *intArray=env->GetIntArrayElements(array,NULL);
int len=env->GetArrayLength(array);
for(int i=0;i<len;i++){
jint item=intArray[i];
}
}
获取JNI基本类型数组的子数组
Get< Type>ArrayRegion函数用来获取JNI数组的子数组,这里面的< Type>需要被替换成实际的类型,比如GetIntArrayRegion,GetLongArrayRegion等
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testJIntArray(JNIEnv* env, jobject thiz,jintArray array) {
jint *subArray=new jint;
env->GetIntArrayRegion(array,0,3,subArray);
}
设置JNI基本类型数组的子数组
Set< Type>ArrayRegion函数用来设置JNI基本类型数组的子数组,这里面的< Type>需要被替换成实际的类型,比如SetIntArrayRegion,SetLongArrayRegion等
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testJIntArray(JNIEnv* env, jobject thiz,jintArray array) {
jint *subArray=new jint;
env->GetIntArrayRegion(array,0,3,subArray);
env->SetIntArrayRegion(array,0,3,subArray);
}
JNI对象数组
GetObjectArrayElement函数用来获取JNI对象数组元素
SetObjectArrayElement函数用来设置JNI对象数组元素
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testJObjectArray(JNIEnv* env, jobject thiz,jobjectArray array) {
int len=env->GetArrayLength(array);
for(int i=0;i<len;i++)
{
jobject item=env->GetObjectArrayElement(array,i);
}
}
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testJObjectArray(JNIEnv* env, jobject thiz,jobjectArray array) {
jobject obj;
env->SetObjectArrayElement(array,1,obj);
}
获取JNI数组的长度
GetArrayLength用来获取数组的长度
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testJObjectArray(JNIEnv* env, jobject thiz,jobjectArray array) {
int len=env->GetArrayLength(array);
}
3.3 JNI访问Java类的方法和字段
Java类型签名映射表
JNI获取Java类的方法ID和字段ID,都需要一个很重要的参数,就是Java类的方法和字段的签名。
| Java类型 | 签名 |
|---|---|
| Boolean | Z |
| Byte | B |
| Char | C |
| Short | S |
| Integer | I |
| Long | J |
| Float | F |
| Double | D |
| Void | V |
| 任何Java类 | L任何Java类的全名; 比如Java String类对应的签名是Ljava/lang/String; |
| < type>[] 数组 | [<类型签名>,type代表类型 比如Java int[]的签名是[I,Java long[]的签名就是[J,Java String[]的签名是 [Ljava/lang/String; |
| 方法类型 | (参数类型签名)返回值类型签名 比如Java方法 void hello(String msg,String msg2)对应的签名就是 (Ljava/lang/String; Ljava/lang/String;)V Java方法 String getNewName(String name)对应的签名是 (Ljava/lang/String;) Ljava/lang/String; Java方法 long add(int a,int b)对应的签名是 (II)J |
JNI获取Java类
GetObjectClass、FindClass用来获取调用对象的类
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_callJavaHelloWorld2(JNIEnv* env, jobject thiz) {
jclass clazz=env->GetObjectClass(thiz);
if(clazz==NULL) return;
jclass clazz2=env->FindClass("com/matt/jnidemo/JNIApi");
if(clazz2==NULL) return;
}
JNI访问Java类非静态字段
GetFieldID用来获取字段的ID。需要传入字段类型的签名描述。
Get< Type>Field用来获取Java类字段的值,比如用GetIntField函数获取Java int型字段的值,用GetLongField函数获取Java long字段的值,用GetObjectField函数获取Java引用类型字段的值
Set< Type>Field用来设置Java类字段的值,比如用SetIntField函数设置Java int型字段的值,用SetLongField函数设置Java long字段的值,用SetObjectField函数设置Java引用类型字段的值
Java代码:
public class Person{
public String name;
public int age;
}
public native void getJavaObjectField(Person person);
private void test(){
Person person=new Person();
person.name="wubb";
person.age=20;
getJavaObjectField(person);
}
JNI代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_getJavaObjectField(JNIEnv* env, jobject thiz,jobject person) {
//获取jclass
jclass clazz=env->GetObjectClass(person);
//获取jfieldID
jfieldID name_fieldID=env->GetFieldID(clazz,"name","Ljava/lang/String;");
//获取java成员变量string值
jstring name=(jstring) env->GetObjectField(person,name_fieldID);
//获取jfieldID
jfieldID age_fieldID=env->GetFieldID(clazz,"age","I");
//获取java成员变量int值
jint age=env->GetIntField(person,age_fieldID);
//修改age的值改为18
env->SetIntField(person, age_fieldID, 18);
}
JNI访问Java类静态字段
GetStaticFieldID函数用来获取Java静态字段的字段ID。
GetStatic< Type>Field用来获取Java类静态字段的值。
SetStatic< Type>Field用来设置Java类静态字段的值。
Java代码:
public class Person {
public String name;
public int age;
public static String name_static;
public static int age_static;
}
public native void getJavaObjectStaticField(Person person);
private void test(){
Person.name_static="wubb";
Person.age_static=20;
Person person=new Person();
getJavaObjectStaticField(person);
}
JNI代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_getJavaObjectStaticField(JNIEnv* env, jobject thiz,jobject person) {
jclass clazz=env->GetObjectClass(person);
jfieldID name_fieldID=env->GetStaticFieldID(clazz,"name_static","Ljava/lang/String;");
jstring name=(jstring) env->GetStaticObjectField(clazz,name_fieldID);
jfieldID age_fieldID=env->GetStaticFieldID(clazz,"age_static","I");
jint age=env->GetStaticIntField(clazz,age_fieldID);
env->SetStaticIntField(clazz, age_fieldID, 18);
}
JNI访问Java类的非静态方法
GetMethodID函数用来获取Java类实例方法的方法ID。
Call< Type>Method函数用来调用Java类实例特定返回值的方法,比如CallVoidMethod,调用java没有返回值的方法,CallLongMethod用来调用Java返回值为Long的方法,等等。
Java代码:
public native void callJavaHelloWorld2();
public void helloWorld2(String msg){
Log.i("hello","hello world "+msg);
}
JNI代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_callJavaHelloWorld2(JNIEnv* env, jobject thiz) {
jclass clazz=env->GetObjectClass(thiz);
if(clazz==NULL) return;
jmethodID helloWorld2_methodID=env->GetMethodID(clazz,"helloWorld2","(java/lang/String;)V");
if(helloWorld2_methodID==NULL) return;
const char *msg="hello world";
jstring jmsg=env->NewStringUTF(msg);
env->CallVoidMethod(thiz,helloWorld2_methodID,jmsg);
}
JNI访问Java类的静态方法
GetStaticMethodID函数用来获取Java类静态方法的方法ID
CallStatic< Type>Method函数用来调用Java类特定返回值的静态方法。
Java代码:
public native void callStaticJavaHelloWorld2();
public static void helloWorldStatic2(String msg){
Log.i("hello","hello world static "+msg);
}
JNI代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_callStaticJavaHelloWorld2(JNIEnv* env, jobject thiz) {
jclass clazz=env->GetObjectClass(thiz);
if(clazz==NULL) return;
jmethodID helloWorldStatic2_methodID=env->GetStaticMethodID(clazz,"helloWorldStatic2","(java/lang/String;)V");
if(helloWorldStatic2_methodID==NULL) return;
const char *msg="hello world";
jstring jmsg=env->NewStringUTF(msg);
env->CallStaticVoidMethod(clazz,helloWorldStatic2_methodID,msg);
}
3.4 JNI NIO缓冲区相关的函数
使用NIO缓冲区可以在Java和JNI代码中共享大数据,性能比传递数组要快很多,当Java和JNI需要传递大数据时,推荐使用NIO缓冲区的方式来传递。
NewDirectByteBuffer函数用来创建NIO缓冲区
GetDirectBufferAddress函数用来获取NIO缓冲区的内容
GetDirectBufferCapacity函数用来获取NIO缓冲区的大小
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testDirectBuffer(JNIEnv* env, jobject thiz) {
const char *data="hello world";
int len=strlen(data);
jobject obj=env->NewDirectByteBuffer((void*)data,len);
long capicity=env->GetDirectBufferCapacity(obj);
char *data2=(char*)env->GetDirectBufferAddress(obj);
}
3.5 JNI线程同步相关的函数
JNI可以使用Java对象进行线程同步
MonitorEnter函数用来锁定Java对象
MonitorExit函数用来释放Java对象锁
Java代码:
public native void jniLock(new Object());
JNI代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_jniLock(JNIEnv* env, jobject thiz,jobject obj) {
env->MonitorEnter(obj);
//do something
env->MonitorExit(obj);
}
3.6 JNI异常相关的函数
JNI处理Java异常
当JNI函数调用的Java方法出现异常的时候,并不会影响JNI方法的执行,但是我们并不推荐JNI函数忽略Java方法出现的异常继续执行,这样可能会带来更多的问题。我们推荐的方法是,当JNI函数调用的Java方法出现异常的时候,JNI函数应该合理的停止执行代码。
ExceptionOccurred函数用来判断JNI函数调用的Java方法是否出现异常
ExceptionClear函数用来清除JNI函数调用的Java方法出现的异常
Java代码:
public void helloWorld(){
throw new NullPointerException("null pointer occurred");
//Log.i("hello","hello world");
}
JNI代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_callJavaHelloWorld(JNIEnv* env, jobject thiz) {
jclass clazz=env->GetObjectClass(thiz);
if(clazz==NULL) return;
jmethodID helloWorld_methodID=env->GetMethodID(clazz,"helloWorld","()V");
if(helloWorld_methodID==NULL) return;
env->CallVoidMethod(thiz,helloWorld_methodID);
if(env->ExceptionOccurred()!=NULL){
env->ExceptionClear();
__android_log_print(ANDROID_LOG_VERBOSE,"hello","%s","program end with java exception");
return;
}
__android_log_print(ANDROID_LOG_VERBOSE,"hello","%s","program end normallly");
}
JNI抛出Java类型的异常
JNI通过ThrowNew函数抛出Java类型的异常
Java代码:
try
{
testNativeException();
}
catch (NullPointerException e){
e.printStackTrace();
}
JNI代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testNativeException(JNIEnv* env, jobject thiz) {
jclass clazz=env->FindClass("java/lang/NullPointerException");
if(clazz==NULL) return;
env->ThrowNew(clazz,"null pointer exception occurred");
}
3.7 JNI对象的全局引用和局部引用
JNI对象的局部引用
通过NewLocalRef或JNI 函数内部创建的 jobject 对象及其子类( jclass 、 jstring 、 jarray 等) 对象都是局部引用。会阻止GC回收所引用的对象,不在本地函数中跨函数使用,不能跨线前使用。函数返回后局部引用所引用的对象会被JVM自动释放,或调用DeleteLocalRef释放。
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testDeleteLocalRef(JNIEnv* env, jobject thiz) {
jclass clazz=env->GetObjectClass(thiz);
if(clazz==NULL) return;
jmethodID helloWorld_methodID=env->GetMethodID(clazz,"helloWorld","()V");
if(helloWorld_methodID==NULL) return;
env->CallVoidMethod(thiz,helloWorld_methodID);
env->DeleteLocalRef(clazz);
}
JNI对象的全局引用
JNI对象的全局引用分为两种,一种是强全局引用,这种引用会阻止Java的垃圾回收器回收JNI代码引用的Java对象,另一种是弱全局引用,这种全局引用则不会阻止垃圾回收器回收JNI代码引用的Java对象。
我们知道Java代码的内存是由垃圾回收器来管理,而JNI代码则不受Java的垃圾回收器来管理,所以JNI代码提供了一组函数,来管理通过JNI代码生成的JNI对象,比如jobject,jclass,jstring,jarray等,对于这些对象,绝对不能简单的声明一个全局变量,在JNI接口函数里面给这个全局变量赋值这么简单,一定要使用JNI代码提供的管理JNI对象的函数,否则代码可能会出现预想不到的问题。
强全局引用
NewGlobalRef用来创建强全局引用的JNI对象
DeleteGlobalRef用来删除强全局引用的JNI对象
jobject gThiz;
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testStrongGlobalRef(JNIEnv* env, jobject thiz) {
//gThiz=thiz;//不能这样给全局JNI对象赋值,要采用下面这种方式
gThiz=env->NewGlobalRef(thiz);//生成全局的JNI对象引用,这样生成的全局的JNI对象才可以在其它函数中使用
env->DeleteGlobalRef(gThiz);//假如我们不需要gThiz这个全局的JNI对象引用,我们可以把它删除掉
}
弱全局引用
NewWeakGlobalRef用来创建弱全局引用的JNI对象
DeleteWeakGlobalRef用来删除弱全局引用的JNI对象
IsSameObject用来判断两个JNI对象是否相同
jobject gThiz;
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_testWeakGlobalRef(JNIEnv*env, jobject thiz) {
//gThiz=thiz;//不能这样给全局JNI对象赋值,要采用下面这种方式
gThiz=env->NewWeakGlobalRef(thiz);//生成全局的JNI对象引用,这样生成的全局的JNI对象才可以在其它函数中使用
if(env->IsSameObject(gThiz,NULL)){
//弱全局引用已经被Java的垃圾回收器回收
}
env->DeleteWeakGlobalRef(gThiz);//假如我们不需要gThiz这个全局的JNI对象引用,我们可以把它删除掉
}
四、调用Java构造方法和父类实例方法
4.1 调用构造函数
调用构造方法和调用对象的实例方法方式是相似的,传入"<init>"作为方法名查找类的构造方法ID,然后调用JNI函数NewObject调用对象的构造函数初始化对象。
Java代码:
public class Cat {
protected String name;
public Cat(String name) {
this.name = name;
}
}
JNI代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_newObject(JNIEnv*env, jobject thiz) {
// 1、获取Cat类的class引用
jclass cls_cat = env->FindClass("com/matt/jnidemo/Cat");
if (cls_cat == NULL) {
return;
}
// 2、获取Cat的构造方法ID(构造方法的名统一为:<init>
jmethodID mid_cat_init = env->GetMethodID(cls_cat, "<init>", "(Ljava/lang/String;)V");
if (mid_cat_init == NULL) {
return; // 没有找到只有一个参数为String的构造方法
}
// 3、创建一个String对象,作为构造方法的参数
jstring c_str_name = env->NewStringUTF("汤姆猫");
if (c_str_name == NULL) {
return; // 创建字符串失败(内存不够)
}
// 4、创建Cat对象的实例(调用对象的构造方法并初始化对象)
jobject obj_cat = env->NewObject(cls_cat, mid_cat_init,c_str_name);
if (obj_cat == NULL) {
return;
}
}
4.2 调用父类实例方法
如果一个方法被定义在父类中,在子类中被覆盖,也可以调用父类中的这个实例方法。JNI 提供了一系列函数CallNonvirtual< Type>Method来支持调用各种返回值类型的实例方法。
Java代码:
// Animal.java
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
System.out.println("Animal Construct call...");
}
public String getName() {
System.out.println("Animal.getName Call...");
return this.name;
}
public void run() {
System.out.println("Animal.run...");
}
}
// Cat.java
public class Cat extends Animal {
public Cat(String name) {
super(name);
System.out.println("Cat Construct call....");
}
@Override
public String getName() {
return "My name is " + this.name;
}
@Override
public void run() {
System.out.println(name + " Cat.run...");
}
}
JNI代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_kgdwbb_jnistudy_MainActivity_callSuperInstanceMethod
(JNIEnv *env, jclass cls)
{
// 1、获取Cat类的class引用
jclass cls_cat = env->FindClass("com/matt/jnidemo/Cat");
if (cls_cat == NULL) {
return;
}
// 2、获取Cat的构造方法ID(构造方法的名统一为:<init>)
jmethodID mid_cat_init = env->GetMethodID(cls_cat, "<init>", "(Ljava/lang/String;)V");
if (mid_cat_init == NULL) {
return; // 没有找到只有一个参数为String的构造方法
}
// 3、创建一个String对象,作为构造方法的参数
jstring c_str_name = env->NewStringUTF("汤姆猫");
if (c_str_name == NULL) {
return; // 创建字符串失败(内存不够)
}
// 4、创建Cat对象的实例(调用对象的构造方法并初始化对象)
jobject obj_cat = env->NewObject(cls_cat, mid_cat_init,c_str_name);
if (obj_cat == NULL) {
return;
}
//-------------- 5、调用Cat父类Animal的run和getName方法 --------------
jclass cls_animal = env->FindClass("com/matt/jnidemo/Animal");
if (cls_animal == NULL) {
return;
}
// 例1: 调用父类的run方法
jmethodID mid_run = env->GetMethodID(cls_animal, "run", "()V"); // 获取父类Animal中run方法的id
if (mid_run == NULL) {
return;
}
// 注意:obj_cat是Cat的实例,cls_animal是Animal的Class引用,mid_run是Animal类中的方法ID
env->CallNonvirtualVoidMethod(obj_cat, cls_animal, mid_run);
// 例2:调用父类的getName方法
// 获取父类Animal中getName方法的id
jmethodID mid_getName = env->GetMethodID(cls_animal, "getName", "()Ljava/lang/String;");
if (mid_getName == NULL) {
return;
}
jstring c_str_name = env->CallNonvirtualObjectMethod(obj_cat, cls_animal, mid_getName);
char *name = env->GetStringUTFChars(c_str_name, NULL);
printf("In C: Animal Name is %s\n", name);
// 释放从java层获取到的字符串所分配的内存
env->ReleaseStringUTFChars(c_str_name, name);
// 删除局部引用(jobject或jobject的子类才属于引用变量),允许VM释放被局部变量所引用的资源
env->DeleteLocalRef(cls_cat);
env->DeleteLocalRef(cls_animal);
env->DeleteLocalRef(c_str_name);
env->DeleteLocalRef(obj_cat);
}
五、缓存字段和方法 ID
获取字段和方法 ID 需要基于字段和方法 ID 的名字和描述符进行符号查找。符号查找消耗相对较多,所以可以将获取的字段和方法ID缓存起来,以便后续重复使用。有两种方法来缓存字段和方法 ID,具体取决于是在使用字和方法 ID 时执行缓存还是在静态初始化块中定义字段或者方法来执行缓存。
5.1 在使用时执行缓存
字段或者方法 ID 可以在本地代码访问字段值或者执行方法回调的时候被缓存。在下面的 Java_InstanceFieldAccess_accessField 函数实现中,使用静态变量对方法 ID 进行缓存,以便在每次调用 InstanceFieldAccess.accessField 方法时,不需要重新计算了。
JNIEXPORT void JNICALL
Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj) {
static jfieldID fid_s = NULL; /* cached field ID for s */
jclass cls;
jstring jstr;
const char *str;
cls = (*env)->GetObjectClass(env, obj);
if (fid_s == NULL) {
fid_s = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");
if (fid_s == NULL) {
return; /* exception already thrown */
}
}
printf("In C:\n");
jstr = (*env)->GetObjectField(env, obj, fid_s);
str = (*env)->GetStringUTFChars(env, jstr, NULL);
if (str == NULL) {
return; /* out of memory */
}
printf(" c.s = \"%s\"\n", str);
(*env)->ReleaseStringUTFChars(env, jstr, str);
jstr = (*env)->NewStringUTF(env, "123");
if (jstr == NULL) {
return; /* out of memory */
}
(*env)->SetObjectField(env, obj, fid_s, jstr);
}
静态变量 fid_s 保存了为 InstanceFiledAccess.s 预先计算的方法 ID。该静态变量初始化为 NULL,当 InstanceFieldAccess.accessField 方法第一次被调用时,它计算该字段 ID 然后将其缓存到该静态变量中以方便后续使用。
注意:
在 JNI 层执行 Java 代码常用到 FindClass() 、 GetMethodID() 、 GetFieldID() ;
但只有第一个函数返回的 jclass 属于 JNI (局部)引用对象,而 jmethodID 和 jfieldID 并不是,它们是指向内部 Runtime 数据结构的指针;
实际上这些 ID 是用于缓存的静态对象:第一次查找会做一次字符串比较,但后面再次调用就能直接读取而变得很快;JVM 会保证这些 ID 是合法的,直到 Class 被 unload;所以, jmethodID 和 jfieldID 是不需要手动释放的,当然也不能作为 JNI 全局引用。
cls是一个局部引用,与方法和字段ID不一样,不能使用静态变量进行缓存,局部引用在函数结束后会被自动释放掉,这时cls成为了一个野针对(指向的内存空间已被释放,但变量的值仍然是被释放后的内存地址,不为NULL),当下次再调用Java_InstanceFieldAccess_accessField 这个函数的时候,如果像 fid_s 一样使用之前的指针地址,会试图访问一个无效的局部引用,从而导致非法的内存访问造成程序崩溃。所以在函数内用static缓存局部引用这种方式是错误的。
5.2 类静态初始化缓存
在调用一个类的方法或属性之前,Java虚拟机会先检查该类是否已经加载到内存当中,如果没有则会先加载,然后紧接着会调用该类的静态初始化代码块,所以在静态初始化该类的过程当中计算并缓存该类当中的字段ID和方法ID也是个不错的选择。
Java代码
class InstanceMethodCall {
private static native void initIDs();
private native void nativeMethod();
private void callback() {
System.out.println("In Java");
}
public static void main(String args[]) {
InstanceMethodCall c = new InstanceMethodCall();
c.nativeMethod();
}
static {
System.loadLibrary("InstanceMethodCall");
initIDs();
}
}
initIDs()的实现仅仅是简单的为 InstanceMethodCall.callback 计算和缓存方法 ID。
JNI代码:
jmethodID MID_AccessCache_callback;
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_initIDs
(JNIEnv *env, jclass cls)
{
printf("initIDs called!!!\n");
MID_AccessCache_callback = (*env)->GetMethodID(env,cls,"callback","()V");
}
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_nativeMethod
(JNIEnv *env, jobject obj)
{
printf("In C Java_com_study_jnilearn_AccessCache_nativeMethod called!!!\n");
(*env)->CallVoidMethod(env, obj, MID_AccessCache_callback);
}
JVM加载AccessCache.class到内存当中之后,会调用该类的静态初始化代码块,即static代码块,先调用System.loadLibrary加载动态库到JVM中,紧接着调用native方法initIDs,会调用用到本地函数Java_com_study_jnilearn_AccessCache_initIDs,在该函数中获取需要缓存的ID,然后存入全局变量当中。下次需要用到这些ID的时候,直接使用全局变量当中的即可