JNI基础语法

一、 Java和JNI类型对照

1.1 基本类型对照表

Java类型Native类型C/C++类型大小
Booleanjbooleanunsigned char无符号8位
Bytejbytechar有符号8位
Charjcharunsigned short无符号16位
Shortjshortshort有符号16位
Integerjintint有符号32位
Longjlonglong long有符号64位
Floatjfloatfloat32位浮点值
Doublejdoubledouble64位双精度浮点值

1.2 引用类型对照表

Java类型Native类型
java.lang.Classjclass
java.lang.Throwablejthrowable
java.lang.Stringjstring
java.lang.Objectjobject
java.util.Objectsjobjects
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>需要被替换成实际的类型,比如GetIntArrayElementsGetLongArrayElements

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>需要被替换成实际的类型,比如GetIntArrayRegionGetLongArrayRegion

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>需要被替换成实际的类型,比如SetIntArrayRegionSetLongArrayRegion

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类型签名
BooleanZ
ByteB
CharC
ShortS
IntegerI
LongJ
FloatF
DoubleD
VoidV
任何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类

GetObjectClassFindClass用来获取调用对象的类

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 (局部)引用对象,而 jmethodIDjfieldID 并不是,它们是指向内部 Runtime 数据结构的指针;

实际上这些 ID 是用于缓存的静态对象:第一次查找会做一次字符串比较,但后面再次调用就能直接读取而变得很快;JVM 会保证这些 ID 是合法的,直到 Class 被 unload;所以, jmethodIDjfieldID 是不需要手动释放的,当然也不能作为 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的时候,直接使用全局变量当中的即可


版权声明:本文为weixin_42046829原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。