【Android】 如何进行JNI回调?

朱梦渝
原创 161       2018-04-08  

回调作为一种常见的方法间通讯方式,有着不可取代的作用。当处理一些无法立即给出结果的逻辑时,往往会选择开放一个回调接口,让上层用户注册,然后等结果处理结束后调用上层传来的回调对象进行结果返回。这样的设计模式,在JNI中也会有很高的实用价值,因此这篇文章主要为大家介绍两种JNI层通知JAVA层的回调方式。

同步回调方式】 

同步回调方式,顾名思义,就是在JNI C/C++普通函数内直接调用JAVA层回调对象的方式。需要JAVA层实现设置一个CallBack对象到JNI层。但不管是同步还是异步方式,我们都需要先在JAVA层定义一个回调接口TestCallBack.java,实现如下:


接口中我们声明了两个方法,OnCallOnCallArgu,写这两个函数主要用于示范不同类型函数如何在JNI层定义使用。JAVA层的回调接口写好后,我们将这个回调对象当作一个参数设置到需要的地址,比如一个init函数:

 public native void init(TestCallback callBack);   

这个示例函数的含义是JAVA层调用init方法,在init方法中,调用JAVA层的callBack对象通知到JAVA层。

我们在JNI层具体实现这个方法:(注意本文Native层使用的是C++,使用C的话,JNI的语法会稍微不同,主要集中在env的调用和方法的函数中,这并不是本文介绍的重点在此不再赘述) 



细心的人可能注意到了,在调用JNIGetMethodID方法时,最后一个参数看起来有些奇怪。这里要介绍一下,这个参数传递的内容叫做“java方法签名”,无需死记硬背,可通过“javap -s 包名.类名”查询获取,但注意你要在build\intermediates\classes(如果你是Android开发者并且使用的是AndroidStudio开发工具,这个路径是准确的;其他的IDE工具请自行查找这个文件夹在哪里)文件夹下执行这个命令才能查询到。这里对于void类型的无参方法,它的签名是“()V”,对于有参数的int OnCallArgu(int arg),它的方法签名就是这样“(I)I”。

这样一个简单的JNI同步回调就处理完了。

异步回调方式】 

上一节中为大家介绍了同步的回调方式,即直接在调用的JNI方法中使用参数中传递过来的JAVA回调对象执行回调操作。但是很多时候特别是涉及到网络请求、大量数据计算等耗时操作时,Native层本身自己就做了异步事件,如果我们要把Native层的异步事件转成JAVA层的回调模式的话,就不能像上节所述那样直接在调用这些Native方法的时候直接传入callback等待回调结果了,这些callback要移步到Native的回调事件中进行调用。这时,就有两个问题必须解决:

1、 Native层的回调函数里没有JAVACallBack 参数

2、 Native代码中自己实现的回调方法,不带JNIEnv对象,无法调用JAVA

第一个问题,在Native自己的回调方法里是不会有jobject callback这个参数的。我们得专门设计一个setCallBackJNI方法,保存一个全局的jobject对象,对象的内容实际上就是JAVA CallBack类,让这个jobject可以在Native层各处可以调用,这样也就可以解决在Native层的回调中取不到JAVA CallBack对象的问题。

第二个问题,纯Native层的代码里,并不带有JNIEnv对象。这里有必要简单地介绍一下这个对象。在JNI层的代码里,JNIEnv类型是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递。因此我们可以看到所有的JNI方法都默认地传递了JNIEnv *env作为参数,没有这个env,是无法调用JNI方法的。除此之外,还有一个JVM对象,也需要简单了解下,JavaVM是虚拟机在JNI中的表示,一个JVM中只有一个JavaVM对象,这个对象是线程共享的。换句话说,JVM更像是一个进程级别的对象。一个进程全局只有一个,和AndroidApplication概念非常相似。这个对象因为是线程共享的,因此它提供了可以获取到JNIEnv对象的方法,这就非常有用了。如果我拿到了当前的JVM对象,我在通过这个对象再获取到JNIEnv,那么第二个问题也就迎刃而解了!下面就来看看怎么做吧:

1、在JAVA层,声明JNI方法:

public native void setCallBack(TestCallback callBack);  


2、在JNI层,实现该方法:



上面代码中的注释请了解一下,这很重要,没有经过NewGlobalRef修饰过的jobject对象会在函数执行完后直接释放掉。


3、在创建了全局的CallBack对象后,我们还需要可以获得JavaVM对象的方法,具体实现如下:

JNI的入口函数是JNI_OnLoad的方法,我们可以在这个方法里获取到当前JVM对象,因为JVM对象是线程共享的全局变量,因此直接在JNI层代码中,定义一个全局的JavaVM *javaVM变量,用于保存JVM的指针即可。

AttachCurrentThread:前面我们提到该指针只在创建它的线程有效,不能跨线程传递。如果想要在代码的任意地方获取到JNIEnv指针,可以通过调用接口(invocation interface)中的AttachCurrentThread方法来获取。与其配套的是DetachCurrentThread方法,DetachCurrentThreadAttachCurrentThread必须配套使用!因此我加了一个hasAttated方法,来避免重复获取对应线程的JNIEnv和错误的Detach

经过上述步骤,异步回调就实现好了。这样JNI的同步和异步回调方式就介绍完了。谢谢阅读。



恒生技术之眼原创文章,未经授权禁止转载。详情见转载须知

联系我们

恒 生 技 术 之 眼