JNI 与 NDK 入门(二)

770 阅读5分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

调用Java方法

调用静态方法

调用思路

在Java中,我们调用一个方法的思路一般是先要找到对应的类,然后找到类中对应的方法,之后进行方法的调用。

在JNI中,其实也是一致的:

  1. 我们先需要同JVM环境的函数找到对应的jclass,对应Java中的类。
  2. 拿到了对应的类之后,我们可以通过JVM环境中的函数拿到需要调用的方法的jmethodID类型的变量。
  3. 获取到jmethodID之后,我们就可以调用JVM环境中的对应函数调用对应方法来进行函数的调用。
  4. 调用完成后,释放相关资源即可。
具体过程

假设我们在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;
    }
}
调用思路

调用实例方法相比调用静态方法会复杂一些

  1. 找到对应的jclass
  2. 调用其构造方法
  3. 创建其对象
  4. 调用其方法
具体过程
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类资讯的分享。)