“这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战”
调用Java方法
调用静态方法
调用思路
在Java中,我们调用一个方法的思路一般是先要找到对应的类,然后找到类中对应的方法,之后进行方法的调用。
在JNI中,其实也是一致的:
- 我们先需要同JVM环境的函数找到对应的
jclass
,对应Java中的类。 - 拿到了对应的类之后,我们可以通过JVM环境中的函数拿到需要调用的方法的
jmethodID
类型的变量。 - 获取到
jmethodID
之后,我们就可以调用JVM环境中的对应函数调用对应方法来进行函数的调用。 - 调用完成后,释放相关资源即可。
具体过程
假设我们在MainActivity定义了如下两个方法:
public static void logMessage(String msg){
Log.d("NDK", msg);
}
public native void callStaticMethod();
复制代码
然后我们为native方法生成了如下的C++代码:
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_callStaticMethod(JNIEnv *env, jobject instance) {
}
复制代码
我们首先通过env的FindClass
方法找到对应的类:
jclass cls_main = env->FindClass("com/n0texpecterr0r/ndkdemo/MainActivity");
复制代码
可以看到,FindClass需要的参数是这个类的全路径。获取到的jclass
我们最好判下空
接下来我们需要拿到我们要调用的方法对应的jmethodID
。
jmethodID mth_static_method = env->GetMethodID(cls_main,"logMessage","(Ljava/lang/String;)V");
复制代码
由于logMessage
方法是static的,因此我们调用了env的GetStaticMethodID
方法,需要传入的参数分别是 jclass 方法名 以及方法对应的签名。
仍然判下空,之后我们可以通过env->CallXXXXXMethod方法来调用对应方法。
由于我们的logMessage方法是static,返回值为void的方法,因此我们使用CallStaticVoidMethod
方法即可
jstring str = env->NewStringUTF("这是从JNI调用的Log");
env->CallStaticVoidMethod(cls_main,mth_static_method,str);
复制代码
调用完成后,我们可以通过env的DeleteLocalRef
方法来释放刚刚声明的变量。
env->DeleteLocalRef(cls_main);
env->DeleteLocalRef(str);
复制代码
这样,在MainActivity中调用callStaticMethod方法后,便会打印这样一条log
D/NDK: 这是从JNI调用的Log
完整代码如下:
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_callStaticMethod(JNIEnv *env, jobject instance) {
// 找到对应的类
jclass cls_main = env->FindClass("com/n0texpecterr0r/ndkdemo/MainActivity");
if(cls_main == NULL) return;
// 获取methodId
jmethodID mth_static_method = env->GetStaticMethodID(cls_main,"logMessage","(Ljava/lang/String;)V");
if(mth_static_method == NULL) return;
// 构建String变量
jstring str = env->NewStringUTF("这是从JNI调用的Log");
// 调用static方法
env->CallStaticVoidMethod(cls_main,mth_static_method,str);
// 释放内存
env->DeleteLocalRef(cls_main);
env->DeleteLocalRef(str);
}
复制代码
有了之前的jclass之后,我们还可以通过获取jfieldID
,之后调用env的SetStaticObjectField
方法来修改static变量。与前面类似。
调用实例方法
假设我们有了下面这样一个类
public class Adder {
private int arg1;
private int arg2;
public Adder(int arg1, int arg2) {
this.arg1 = arg1;
this.arg2 = arg2;
}
public int doAdd(){
return arg1+arg2;
}
}
复制代码
调用思路
调用实例方法相比调用静态方法会复杂一些
- 找到对应的
jclass
- 调用其构造方法
- 创建其对象
- 调用其方法
具体过程
JNIEXPORT jint JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_addNative(JNIEnv *env, jobject instance, jint arg1, jint arg2) {
// 找到对应类
jclass cls_adder = env->FindClass("com/n0texpecterr0r/ndkdemo/Adder");
// 获取构造方法
jmethodID mth_constructor = env->GetMethodID(cls_adder,"<init>","(II)V");
// 调用构造方法构建jobject
jobject adder = env->NewObject(cls_adder, mth_constructor,arg1,arg2);
// 获取add方法
jmethodID mth_add = env->GetMethodID(cls_adder,"doAdd","()I");
// 调用add方法获取返回值
jint result = env->CallIntMethod(adder,mth_add);
// 回收资源
env->DeleteLocalRef(cls_adder);
env->DeleteLocalRef(adder);
// 返回结果
return result;
}
复制代码
可以看到,在获取构造函数的id时,指定的方法名为<init>
。
对象引用的处理
为什么要处理对象的引用
当我们在Java中使用new创建了一个对象后,可以随意使用这个对象,不需要关注它什么时候被回收,因为这个对象的回收托管给了GC。但是当使用JNI传递给Native层的对象时该如何处理呢?
将对象传递给Native语言后,Native层会持有Java对象,如果我们不妥善处理会导致内存泄漏。 因此在Native层使用Java对象时,需要释放这个引用。
引用的处理
下面可以看看对于数组引用的处理
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_useCppQSort(JNIEnv *env, jobject instance, jintArray jarray) {
jint* arrayElemts = env->GetIntArrayElements(jarray, NULL);
jsize arraySize = env->GetArrayLength(jarray);
... // 排序算法
env->ReleaseIntArrayElements(jarray, arrayElemts, JNI_COMMIT);
}
复制代码
将array
对象传递给C++, C++中的变量将持有array
这个引用,因为数组和对象在java中都是引用,都会在堆内存中开辟一块空间 , 但我们使用完对象之后需要将引用释放掉,不然会导致内存泄漏 。
释放数组元素时最后一个参数可以传入两个值:
- JNI_ABORT, Java数组不进行更新,但是释放C/C++数组
- JNI_COMMIT,Java数组进行更新,不释放C/C++数组(函数执行完,数组还是会释放)
其实,只要是Java对象,在Native层中都需要释放(包括在Native层创建的对象引用)。
引用分级
在Java中引用有强弱之分,在C/C++中也不例外,C/C++中也有一套全局引用
,局部引用
,弱全局引用
等等 。
局部引用
局部引用指的是C/C++使用到或自行创建的Java对象,需要告知虚拟机在合适的时候回收对象。(通过DeleteLocalRef手动释放对象)
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_localRef(JNIEnv *env, jobject instance) {
// 找到类
jclass dateClass = env->FindClass("java/util/Date");
// 得到构造方法ID
jmethodID dateConstructorId = env->GetMethodID(dateClass, "<init>", "()V");
// 创建Date对象
jobject dateObject = env->NewObject(dateClass, dateConstructorId);
// 创建一个局部引用
jobject dateLocalRef = env->NewLocalRef(dateObject);
...
// 不再使用对象,通知GC回收对象
env->DeleteLocalRef(dateLocalRef);
// 因为dateObject也是局部对象,可以直接回收dateObject对象
// env->DeleteLocalRef(dateObject);
}
复制代码
全局引用
全局引用的特点是共享(可以跨多个线程),手动控制内存使用
jstring globalStr;
/*创建全局引用*/
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_createGlobalRef(JNIEnv *env, jobject jobj) {
jstring jStr = env->NewStringUTF("N0tExpectErr0r");
// 创建一个全局引用
globalStr = env->NewGlobalRef(jStr);
}
/*使用全局引用*/
JNIEXPORT jstring JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_useGlobalRef(JNIEnv *env, jobject jobj) {
return globalStr;
}
/*释放全局引用*/
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_deleteGlobalRef(JNIEnv *env, jobject jobj) {
// 释放全局引用
env->DeleteGlobalRef(globalStr);
}
复制代码
而弱全局引用的特点是节省内存,在内存不足时可以释放所引用的对象,可以用来引用一个不常用的对象,如果为NULL,临时创建。
创建:NewWeakGlobalRef
销毁:DeleteWeakGlobalRef
还未完结,上班摸鱼有点慌,明天再说。。。
公众号:程序员喵大人(专注于Android各类学习笔记、面试题以及IT类资讯的分享。)