在你应该了解的JNI知识(一)——静态注册与动态注册中,了解了JNI是如何使用的,以及两种注册方式的使用以及区别。本篇博客将介绍Java和JNI的互相调用,因此主要包括两部分:
JNI层调用Java层有点类似Java的反射机制,需要首先找到类、再找到某个方法或字段,再进行调用。 这里涉及JNIEnv的几个方法:
//根据全限定名找到类jclass FindClass(const char* name) //根据方法名和方法参数的签名得到方法idjmethodID 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) //根据字段名得到字段idjfieldID GetFieldID(jclass clazz, const char* name, const char* sig)...
上面的方法主要包括得到jclass类、jmethodId、jfieldId,是不是和Java的反射好像?
有点需要注意的是,JNI层的方法严格区分了返回类型,返回类型是boolean的,会有CallBoolenMethodId,返回类型是Int的,会有CallIntMethodId;同理关于FieldId和数组的方法都是这样的,不能调用错。
这边以一个demo为例:Java层提供了三个方法:JNI层首先调用两个方法得到两个数,然后相加,再调用Java层更新界面。
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") } }}
这里采用静态注册的方式,方法的的实现如下:
//调用MainActivity中的两个方法,得到两个数,相加,再显示到TextView上JNIEXPORT void JNICALLJava_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()
是不是很相似?因此很多时候我们要学会类比,这样记忆和理解起来会比较快。
这里可以标题取得有所歧义,因为JNI不就是Java调用C/C++吗?这里的情形可以举个例子:比如说需要在C++层创建多份同一个对象,Java层会根据不同情况调用不同对象,那么该怎么做呢?
Java层要能调用不同对象,得保存各个对象的信息,但那是C层的对象,怎么保存了?答案是指针(对象地址),然后根据不同对象传入不同指针即可。如果C++层需要保存对象,可以使用vector或map来进行保存。
举个例子:C++层有Person类,Java层去创建Person类、设置和获取name字段。
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
创建了一个Person类,有一个字段name。jni中对应的三个代码如下:
JNIEXPORT jlong JNICALLJava_com_enniu_jnidemo_MainActivity_createPerson(JNIEnv *env, jobject thiz, jstring name) { Person *p = new Person(); //返回指针,供Java层保存 return reinterpret_cast<uintptr_t>(p);}JNIEXPORT void JNICALLJava_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 JNICALLJava_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层,然后强转到具体对象再进行操作的。
关于代码,可以移步:https://github.com/wangli135/ClimbDemo/tree/master/jnidemo