什么是回调?简单来说,callback就是一个回调通知。当我们需要在某个方法完成或事件触发后通知一些特定的任务时,我们就需要使用回调。最有可能看到回调的语言是javascript。基本上,在javascript中,回调无处不在。为了解决回调带来的回调地狱问题,ES6中引入了promise来解决这个问题。为了方便与native方法的交互,JNA还提供了Callback用于回调。JNA中回调的本质是一个指向本机函数的指针。通过这个指针,可以调用native函数中的方法。让我们来看看。JNA中的回调首先看JNA中Callback的定义:publicinterfaceCallback{interfaceUncaughtExceptionHandler{voiduncaughtException(Callbackc,Throwablee);}StringMETHOD_NAME="回调";ListFORBIDDEN_NAMES=Collections.unmodifiableList(Arrays.asList("hashCode","equals","toString"));}所有回调方法都需要实现这个回调接口。Callback接口很简单,定义了一个接口和两个属性。我们先来看这个界面。接口的名称是UncaughtExceptionHandler,里面有一个uncaughtException方法。该接口主要用于处理JAVA的回调代码中没有捕获到的异常。请注意,在uncaughtException方法中,不能抛出异常,任何由此方法抛出的异常都将被忽略。METHOD_NAME字段指定Callback调用的方法。如果Callback类中只定义了一个public方法,那么默认的回调方法就是这个方法。如果在Callback类中定义了多个public方法,那么METHOD_NAME="callback"的方法将被选择作为回调。最后一个属性是FORBIDDEN_NAMES。指示此列表中的名称不能用作回调方法。目前好像有3个方法名不能用,分别是:“hashCode”、“equals”、“toString”。Callback还有一个名为DLLCallback的兄弟。我们看一下DLLCallback的定义:publicinterfaceDLLCallbackextendsCallback{@java.lang.annotation.NativeintDLL_FPTRS=16;}DLLCallback主要用于WindowsAPI访问。对于回调对象,我们需要负责回调对象的释放。如果本机代码尝试访问回收的回调,它可能会导致VM崩溃。定义callbackapplicationcallback是因为JNA中的callback实际上是将指针映射到native中的函数。首先看一下struct中定义的函数指针:struct_functions{int(*open)(constchar*,int);整数(*关闭)(整数);};在这个结构体中,定义了两个函数指针,分别有两个参数和一个参数。对应的JNA回调定义如下:}publicstaticinterfaceCloseFuncextendsCallback{intinvoke(intfd);}公共OpenFunc打开;publicCloseFuncclose;}我们在Structure中定义了两个继承自Callback的接口,对应的invoke方法定义在对应的接口中。然后看具体的调用方式:Functionsfuncs=newFunctions();lib.init(函数);intfd=funcs.open.invoke("我的文件",0);funcs.close.invoke(fd);另外,Callback也可以作为函数的返回值,如下所示:typedefvoid(*sig_t)(int);sig_t信号(int信号,sig_tsigfunc);对于这种单独存在的函数指针,我们需要自定义一个Library,在对应的Callback中定义如下:}SignalFunctionsignal(intsignal,SignalFunctionfunc);}回调的获取和应用如果在Structure中定义了callback,那么在Structure初始化的时候就可以自动实例化,然后只需要从Structure中访问相应的属性即可。如果回调定义在一个普通的库中,它看起来像这样:}interfaceByteCallbackextendsCallback{字节回调(字节arg,字节arg2);}voidcallVoidCallback(VoidCallbackc);bytecallInt8Callback(ByteCallbackc,bytearg,bytearg2);上面的例子中,我们在一个Library中定义了两个回调,一个是不返回值的回调,一个是返回字节的回调。JNA提供了一个简单的工具类来帮助我们获取Callback。这个工具类是CallbackReference,对应的方法是CallbackReference.getCallback,如下所示:Pointerp=newPointer("MultiplyMappedCallback".hashCode());回调cbV1=CallbackReference.getCallback(TestLibrary.VoidCallback.class,p);回调cbB1=CallbackReference.getCallback(TestLibrary.ByteCallback.class,p);log.info("cbV1:{}",cbV1);log.info(“cbB1:{}”,cbB1);输出如下:INFOcom.flydean.CallbackUsage-cbV1:Proxyinterfacetonativefunction@0xffffffffc46eeefc(com.flydean.CallbackUsage$TestLibrary$VoidCallback)INFOcom.flydean.CallbackUsage-cbB1:ProxyinterfacetoFromnativefunction@0xffffffffc46eeefc(com.flydean.CallbackUsage$TestLibrary$ByteCallback),可见这两个Callback其实是对native方法的代理。如果详细看getCallback的实现逻辑:privatestaticCallbackgetCallback(Class>type,Pointerp,booleandirect){if(p==null){returnnull;}if(!type.isInterface())thrownewIllegalArgumentException("回调类型必须是接口");Mapmap=direct?directCallbackMap:回调映射;同步(pointerCallbackMap){Reference[]array=pointerCallbackMap.get(p);回调cb=getTypeAssignableCallback(类型,数组);如果(cb!=null){返回cb;}cb=createCallback(类型,p);pointerCallbackMap.put(p,addCallbackToArray(cb,array));//此回调没有CallbackReferencemap.remove(cb);返回cb;}}可以看出它的实现逻辑是先判断类型是否是接口,如果不是接口就会报错。然后判断是不是直接映射。实际上JNA目前的实现是接口映射,所以接下来的逻辑就是从pointerCallbackMap中获取函数指针对应的回调。然后根据传入的类型找到具体的Callback,没有找到则新建一个callback,最后将新建的存放到pointerCallbackMap中。大家要注意了,有个关键参数叫Pointer。在实际使用中,您需要传入一个指向真正的naitve函数的指针。在上面的例子中,为了简单起见,我们自定义了一个Pointer,并没有太大的实际意义。如果真的要在JNA中调用TestLibrary中创建的两个调用方法:callVoidCallback和callInt8Callback,首先需要加载相应的Library:TestLibrarylib=Native.load("testlib",TestLibrary.class);然后分别创建TestLibrary.VoidCallback和TestLibrary.ByteCallback的例子如下,先看VoidCallback:finalboolean[]voidCalled={false};TestLibrary.VoidCallbackcb1=newTestLibrary.VoidCallback(){@Overridepublicvoidcallback(){voidCalled[0]=true;}};lib.callVoidCallback(cb1);assertTrue("回调未调用",voidCalled[0]);这里我们在回调中将voidCalled的值写回true,表示回调方法已经调用。查看具有返回值的ByteCallback:finalboolean[]int8Called={false};最终字节[]cbArgs={0,0};TestLibrary.ByteCallbackcb2=newTestLibrary.ByteCallback(){@Overridepublicbytecallback(bytearg,bytearg2){int8Called[0]=true;cbArgs[0]=参数;cbArgs[1]=arg2;返回(字节)(arg+arg2);}};最后一个字节MAGIC=0x11;bytevalue=lib.callInt8Callback(cb2,MAGIC,(byte)(MAGIC*2));我们可以直接在回调方法中返回要返回的字节值。在多线程环境中使用回调默认情况下,回调方法在当前线程中执行。如果想让回调方法在另一个线程执行,可以创建一个CallbackThreadInitializer,指定daemon、detach、name、threadGroup属性:finalStringtname="VoidCallbackThreaded";ThreadGrouptestGroup=newThreadGroup("callVoidCallbackThreaded的线程组");CallbackThreadInitializerinit=newCallbackThreadInitializer(true,false,tname,testGroup);然后创建一个回调实例:T??estLibrary.VoidCallbackcb=newTestLibrary.VoidCallback(){@Overridepublicvoidcallback(){Threadthread=Thread.currentThread();守护进程[0]=thread.isDaemon();名称[0]=thread.getName();组[0]=thread.getThreadGroup();t[0]=线程;如果(thread.isAlive()){alive[0]=true;}++调用[0];if(THREAD_DETACH_BUG&&called[0]==2){Native.detach(true);}}};然后调用:Native.setCallbackThreadInitializer(cb,init);将callback与CallbackThreadInitializer关联起来,最后调用回调方法:lib.callVoidCallbackThreaded(cb,2,2000,"callVoidCallbackThreaded",0);总结JNA回调可以实现将方法传递给native方法的功能,在某些情况下还是很有用的。本文代码:https://github.com/ddean2009/learn-java-base-9-to-20.git本文已收录于http://www.flydean.com/09-jna-callbacks/最热门的解读,最深的干货,最简洁的教程,还有很多你不知道的小技巧等你来发现!欢迎关注我的公众号:《程序那些事儿》,懂技术,更懂你!