你应该了解的JNI知识(二)——Java与JNI互相调用

你应该了解的JNI知识(一)——静态注册与动态注册中,了解了JNI是如何使用的,以及两种注册方式的使用以及区别。本篇博客将介绍Java和JNI的互相调用,因此主要包括两部分:

  1. JNI层调用Java层
  2. Java层调用JNI、Native层

JNI层调用Java层

JNI层调用Java层有点类似Java的反射机制,需要首先找到类、再找到某个方法或字段,再进行调用。
这里涉及JNIEnv的几个方法:

//根据全限定名找到类
jclass FindClass(const char* name)  

//根据方法名和方法参数的签名得到方法id
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)

//从方法名可以看到,该方法是获取静态方法的
jmethodID GetStaticMethodID(jclass clazz, const char* name, const char* sig)

//Java层的方法返回不同类型,需要调用不同的方法
_jtype Call##_jname##MethodA(jobject obj, jmethodID methodID,           \
        jvalue* args)                                                 

//根据字段名得到字段id
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)

...

上面的方法主要包括得到jclass类、jmethodId、jfieldId,是不是和Java的反射好像?

有点需要注意的是,JNI层的方法严格区分了返回类型,返回类型是boolean的,会有CallBoolenMethodId,返回类型是Int的,会有CallIntMethodId;同理关于FieldId和数组的方法都是这样的,不能调用错。

这边以一个demo为例:Java层提供了三个方法:JNI层首先调用两个方法得到两个数,然后相加,再调用Java层更新界面。

MainActivity的代码

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btnJniInvokeJava.setOnClickListener {
            jniInvokeJava()
        }

    }

    external fun jniInvokeJava()

    fun getNum1(): Int {
        return 10
    }

    fun getNum2(): Double {
        return 20.0
    }

    fun showResult(value: Double) {
        tvResultShow.text = value.toString()
    }


    companion object {
        init {
            System.loadLibrary("jniAndJava")
        }
    }

}

JNI层的代码

这里采用静态注册的方式,方法的的实现如下:

//调用MainActivity中的两个方法,得到两个数,相加,再显示到TextView上
JNIEXPORT void JNICALL
Java_com_enniu_jnidemo_MainActivity_jniInvokeJava(JNIEnv *env, jobject thiz) {
    //找到MainActivity类
    jclass mainActivityClazz = env->FindClass("com/enniu/jnidemo/MainActivity");

    //找到getNum1()方法
    jmethodID getNum1MethodId = env->GetMethodID(mainActivityClazz, "getNum1", "()I");
    //找到getNum2()方法
    jmethodID getNum2MethodId = env->GetMethodID(mainActivityClazz, "getNum2", "()D");

    //因为getNum1()返回值是Int,所以调用CallIntMethod
    jint num1=env->CallIntMethod(thiz,getNum1MethodId);
    //因为getNum2()返回值是Double,所以调用CallDoubleMethod
    jdouble  num2=env->CallDoubleMethod(thiz,getNum2MethodId);

    //两数相加
    jdouble result=num1+num2;

    //设置给TextView
    jmethodID showResultMethodId=env->GetMethodID(mainActivityClazz,"showResult","(D)V");
    //showResult()方法的返回值是void,所以调用CallVoidMethod()方法
    env->CallVoidMethod(thiz,showResultMethodId,result);

}
}

从上面可以看到,整体调用流程和反射是很像的。Call***Method()的第一个参数是jobject,表示在某个对象上调用该方法,因此如果需要调用对象的方法,JNI又无法获取的话,需要从Java层传入。

jclass GetObjectClass(jobject obj)

上面这个方法提供了从jobject–>jclass的快捷方式,就不需要走FindClass()的步骤了,这里是不是发现
getObjectClass====Object.getClass()
FindClass()====Class.forName()

是不是很相似?因此很多时候我们要学会类比,这样记忆和理解起来会比较快。

Java层调用C/C++代码

这里可以标题取得有所歧义,因为JNI不就是Java调用C/C++吗?这里的情形可以举个例子:比如说需要在C++层创建多份同一个对象,Java层会根据不同情况调用不同对象,那么该怎么做呢?

Java层要能调用不同对象,得保存各个对象的信息,但那是C层的对象,怎么保存了?答案是指针(对象地址),然后根据不同对象传入不同指针即可。如果C++层需要保存对象,可以使用vector或map来进行保存。

举个例子:C++层有Person类,Java层去创建Person类、设置和获取name字段。

MainActivity代码

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btnJavaInvokeCpp.setOnClickListener {
            //创建一个Person类并设置name
            val person1 = createPerson()
            setPersonName(person1, "Hello")

            //再创建一个Person类并设置name
            val person2 = createPerson()
            setPersonName(person2, "World")
            
            //获取Person类的name字段
            tvResultShow.text = "${getPersonName(person1)},${getPersonName(person2)}"
        }


    }

    //三个native方法声明
    external fun createPerson(): Long

    external fun setPersonName(ptr: Long, name: String)

    external fun getPersonName(ptr: Long): String

JNI层的代码

创建了一个Person类,有一个字段name。jni中对应的三个代码如下:

JNIEXPORT jlong JNICALL
Java_com_enniu_jnidemo_MainActivity_createPerson(JNIEnv *env, jobject thiz, jstring name) {
    Person *p = new Person();
    //返回指针,供Java层保存
    return reinterpret_cast<uintptr_t>(p);
}

JNIEXPORT void JNICALL
Java_com_enniu_jnidemo_MainActivity_setPersonName(JNIEnv *env, jobject thiz, jlong ptr,
                                                  jstring name) {
    //强转到Person指针
    Person *person = reinterpret_cast<Person *>(ptr);
    person->setName(env->GetStringUTFChars(name, NULL));
}

JNIEXPORT jstring JNICALL
Java_com_enniu_jnidemo_MainActivity_getPersonName(JNIEnv *env, jobject thiz, jlong ptr) {
    //强转到Person指针
    Person *person = reinterpret_cast<Person *>(ptr);
    return env->NewStringUTF(person->getName());
}

可以看到在createPerson()方法中创建一个Person类,然后返回其指针,set和get方法首先都是通过Java层传入的指针强转到Person对象,再进行操作的。

总结

至此,介绍完了Java与JNI代码的互相调用。
JNI调用Java代码是一种类似反射的原理,先找到jclass、再找到jmethodId,然后调用,这样一步步地来;Java调用C/C++代码创建对象是需要保存对象指针,然后各种操作是要将指针传入到jni层,然后强转到具体对象再进行操作的。

关于代码,可以移步Github

关注我的技术公众号,不定期会有技术文章推送,不敢说优质,但至少是我自己的学习心得。微信扫一扫下方二维码即可关注:
二维码

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页