因为android的APP由java开发,所以FMX在开发android时也遵循了JAVA的协议,并且是最多见的JNI协议,在JNI中咱们知道使用JVM的env接口来对接java内部的各类类,实例,好比调用某个实例的方法。各类语言对JNI的封装程度不一样,并且封装的质量每每体如今各自语言对JAVA的控制自由度上。比方说,若是只是导入了JNI的头文件,那即便最简单的调用ToString方法,也会变得很是麻烦。java
基于对delphi的喜欢,我就斗胆说一句,目前全部语言对JNI的封装程度,惟有Delphi最高端(在Delphi面前,其余都是渣渣),由于你可使用Delphi的类来直接调用JAVA实例或类的方法,并且使用过程当中甚至感受不到JNI的存在。android
在FMX框架中,对于android的JNI支持,最关键的代码文件就是Androidapi.JNIBridge.pas和Androidapi.JNIMarshal.pas。api
在Androidapi.JNIBridge.pas文件中,最关键的类是 TJavaImport 和 TJavaLocal框架
大体原理是,经过java class的翻译文件(将java的class翻译为pascal的接口定义文件,EMB有现成的java2op工具)中的java类同名接口,再使用RTTI方法获得该java类的方法表,又结合TRawVirtualClass实现该java类同名接口的虚拟类(咱们知道delphi里若是只是接口interface是没法直接使用的,注意此处说的接口仅仅是Delphi的基础类interface,和COM技术无关,要使用delphi的interface方法,必需要将interface接口继承到某个类再把方法实现了才能使用接口,而TRawVirtualClass就是用来在运行时动态建立并继承接口的虚拟类,该虚拟类等同于继承接并实现方法)。函数
以蓝牙接口翻译文件Androidapi.JNI.Bluetooth.pas来讲明:工具
[JavaSignature('android/bluetooth/BluetoothClass')] Jbluetooth_BluetoothClass = interface(JObject) ['{5B43837A-0671-4D08-9885-EA58330D393E}'] function describeContents: Integer; cdecl; function equals(o: JObject): Boolean; cdecl; function getDeviceClass: Integer; cdecl; function getMajorDeviceClass: Integer; cdecl; function hasService(service: Integer): Boolean; cdecl; function hashCode: Integer; cdecl; function toString: JString; cdecl; procedure writeToParcel(out_: JParcel; flags: Integer); cdecl; end; TJbluetooth_BluetoothClass = class(TJavaGenericImport<Jbluetooth_BluetoothClassClass, Jbluetooth_BluetoothClass>) end;
上面代码即便用 java2op翻译过来的Androidapi.JNI.Bluetooth.pas文件片断(EMB自带的), TJbluetooth_BluetoothClass内部继承自TJavaGenericImport(同时会建立TJavaImport),名称规则是TJXXXX,这是一个类,delphi直接create就可使用(在delphi的jni里不建议直接使用,一般会报错),或不用create便可使用其class类方法(即一般调用wrap方法,该方法就是一个class function,经过TJXXXX.Wrap调用)。ui
关于TJXXXX的Create和Wrap的区别: 1)Create对应的就是java里new一个java类的实例,同时内部调用java类的init方法。 2)Wrap的做用是把一个java类实例封装到delphi里对应名称的类(其实原理上是接口)。 所以,若是咱们想操控一个java层已经建立好的实例,就用wrap,想建立一个实例,就用Create或init,固然建立实例的前提是保证该java类构造函数简单,由于Create并无办法应对带参数的构造函数。
继续来说前面提到的蓝牙接口,从代码中看到,实际中咱们须要的方法都在Jbluetooth_BluetoothClass接口里,但咱们知道Jbluetooth_BluetoothClass只是一个delphi接口,而在delphi里接口方法必需要实现了才能使用,但咱们看到该接口只被“伪继承”到TJbluetooth_BluetoothClass,由于在TJbluetooth_BluetoothClass没有看到对接口方法进行实现。且另一方面,咱们想要调用的java类的方法,真正实现方法的代码确定都在java层的同名类里,不可能在delphi层实现的。因此咱们能够想象,这里delphi对Jbluetooth_BluetoothClass接口的实现必然隐藏在TJbluetooth_BluetoothClass的父类TJavaGenericImport中, 经过代码分析,咱们看到,delphi接口和java类方法之间存在一条桥梁(这也是该单元文件命名为JNIBridge的缘由),且这个桥梁是经过Delphi的RTTI技术和JAVA的JNI接口共同协做完成的。 spa
那delphi怎么实现这个桥梁,让其作到调用一个delphi接口的方法就可以直接调用java类的方法呢?翻译
在这里,咱们先说明一下在JNI中调用java类实例的方法,就是须要经过JNIEvn的CallXXXMethod来间接调用。 delphi却能够经过调用Jbluetooth_BluetoothClass接口方法就能等同JNI的一系列操做,其原理是这样的:代理
在封装的开始,就是TJbluetooth_BluetoothClass继承自TJavaGenericImport,TJavaGenericImport内部再建立TJavaImport,TJavaGenericImport是一个泛型类,其做用是传递TJavaImport所须要的Jbluetooth_BluetoothClass接口信息;也就是将Jbluetooth_BluetoothClass接口信息--“方法表”收集了并保存到TJavaVTable里,这样TJavaImport就能够根据TJavaVTable建立一个Jbluetooth_BluetoothClass接口的虚拟类,该虚拟类其实就是TJavaImport(继承自TRawVirtualClass,若是很别扭可将TJavaImport理解为虚拟类的代理,而虚拟类是RTTI一个强大的功能,另外还有虚拟接口,有兴趣能够研究RTTI),若是须要得到Jbluetooth_BluetoothClass接口,直接使用TJavaImport.QueryInterface便可。
讲这么绕口,其实只要理解,咱们虽然没有在TJbluetooth_BluetoothClass里看到Jbluetooth_BluetoothClass接口方法的实现,但实际内部已经由TJavaImport自动实现了便可,而且实现的接口方法内部逻辑是自动调用了JNI的CallXXXMethod操做,至于如何作到自动调用JNI,在后面会讲到。
综上所述,TJavaImport实现了从delphi代码直接调用java类方法的功能,也就是实现了代码逻辑从delphi->java执行,那有没有办法让代码从java->delphi执行呢,答案固然有,就是下面要说的TJavaLocal。
这里先说明下,其实并非本地化java类,而是本地化java接口,也就是说,在java里调用java接口(不一样于delphi接口),便可直接触发其本地化后的delphi类方法,通俗讲就是在java里调用delphi类方法。但实际中,因为咱们是作delphi开发的,不多需求要在java里开发而后调用某个delphi类实例的方法,因此FMX实现TJavaLocal最大做用就是解决“当一个java类的方法使用了一个java接口做为参数时,咱们不须要额外编写java代码就能在Delphi里随意调用”的问题(有点绕,接下来说为何)。
在java中,也有interface,且不少java类方法的参数或者事件就是使用interface,而假如须要使用该java类方法,咱们必须在java里经过一个java class实现该interface(固然使用动态代理方法是例外),再将新的class建立实例后做为参数传递。
若是按照上面的规则,当咱们在delphi里使用某一个java类的方法时,恰好须要传递一个java interface参数,那就须要编写一个java文件,把该java interface继承实现到某个java interfaceclass,且定义该类为static(让JVM启动时就实例化该类),而且在实现的接口方法中保存各类结果interfaceResult,同时再添加一些获取结果的方法如GetInterfaceResult,接着再制做成jar包添加到delphi工程,同时使用java2op翻译该java interfaceclass为Jinterfaceclass,最后在delphi里使用TJinterfaceclass.Wrap来获得已经由JVM实例化的Jinterfaceclass实例(实际上是静态类),调用java类的方法时,传递的参数就是该Jinterfaceclass实例(静态类),一旦调用成功,在java层内部就会接收到传递过来的类型为接口的参数,并在内部调用过程当中触发该接口的方法,在接口的方法中咱们刚才提到要保存一些结果,这些都是在java层实现好。而在delphi层就经过Jinterfaceclass的GetInterfaceResult方法获得java interfaceclass的接口方法所保存的结果,大致流程如上,很是繁琐麻烦。
所以,很高兴在delphi里咱们有了TJavaLocal,原理上就是使用java的动态代理,将java interface代理到已经在fmx java源码里实现好的代理类ProxyInterface,该类的源码在下面路径中:
java\fmx\src\com\embarcadero\rtl\ProxyInterface.java
而咱们在翻译某一个java interface到delphi interface后,若是要为该java interface建立代理,直接使用以下定义:
TJavaSomeInterfaceImplement=class(TJavaLocal, JJavaSomeInterface) procedure JavaInterfaceMethod();cdecl; end;
如上,JJavaSomeInterface即经过java2op直接翻译某一个java接口后的同名delphi接口,实现其方法JavaInterfaceMethod后,使用时TJavaSomeInterfaceImplement.create后便可当作参数传递给java层,在java层内部接收到的倒是java的同名接口,而且当java层内部触发了该接口的JavaInterfaceMethod方法时,又会触发delphi层接口的同名JavaInterfaceMethod方法,最终执行咱们使用pascal开发的JavaInterfaceMethod方法代码,至关于接口方法从java层触发调用,回到pascal层执行。
FMX可以作到如此自动化,原理上是由于在ProxyInterface的invoke方法中调用了一个强大的JNI接口:dispatchToNative,该接口源码就在Androidapi.JNIBridge.pas里。不得不佩服FMX的团队,经过该接口直接将代码从java层返回到pascal层执行,将java接口的方法挂接到同名的TRttiMethod,并经过TRttiMethod.Invoke,让咱们回到了熟悉的pascal世界,固然这一切都离不开java的动态代理和delphi的rtti。
须要注意的是,java的动态代理只支持对接口的代理类实现,若是是java抽象类,则没法直接使用,具体可参看FMX的作法,将抽象类继承实现并转嫁到新的接口上,就可使用代理类了。例如蓝牙的BluetoothGattCallback就是一个抽象类,FMX先把该抽象类继承为RTLBluetoothGattCallback,并将其关联到RTLBluetoothGattListener,再其方法中调用RTLBluetoothGattListener的方法,而RTLBluetoothGattListener就是一个java接口。这样咱们就能够在delphi里经过代理类TJavaLocal直接实现JRTLBluetoothGattListener的方法了,整体上有点美中不足,由于对于第三方jar库有使用到抽象类,就得额外再编写java代码再制做jar包。
在Androidapi.JNIMarshal.pas中,最关键的是ExecJNI函数。
前面TJavaImport的探索中提到,由TJavaImport自动实现的接口方法内部自动调用了JNI的CallXXXMethod操做,其中比较核心的过程就是将接口方法表搜集到TJavaVTable中,TJavaVTable的JNIMethodInvokeData成员保存了调用JNI须要的各类数据(如方法签名,参数,方法ID,返回类ID等),以便后续可以调用CallXXXMethod操做。
可是查看源码咱们发现,TJavaVTable将虚拟类的方法地址都绑定到一个叫DispatchToImport的函数。也就是说经过TJavaImport继承自虚拟类TRawVirtualClass,该虚拟类因为特殊的实现,前面探索讲到该虚拟类等同于继承自接口(内部有保存接口的guid,知足QueryInterface的调用),同时因为TRawVirtualClass的特色,使其等同于实现了接口的方法(内部建立了类的方法表)。但其方法表中的全部方法的参数虽然记录到TJavaVTable的JNIMethodInvokeData里,而其方法地址却又都指向同一个方法:DispatchToImport,经过定义咱们知道DispatchToImport是一个可变参数的方法,那DispatchToImport到底起到什么样的做用呢? 为何一个DispatchToImport就可以自动实现全部Java类同名接口的全部方法呢?
很遗憾,DispatchToImport是librtlhelper.a库里实现的,librtlhelper.a没找到开源代码(可能在之前的XE某个版本中有开放过?),FMX的秘密在此只能猜想了,因此如下是猜想结果(有兴趣的能够去反编译确认,我相信和猜想的结果大体相同):
当咱们调用某一个Java类同名的Delphi接口方法时,其实是调用该接口对应虚拟类的同名类实例方法,而类实例的方法又到了DispatchToImport函数中,DispatchToImport函数中从新封装参数后调用ExecJNI函数,ExecJNI内部解封参数获得JNIMethodInvokeData,最终调用了JNI的CallXXXMethod接口。
因此,即便librtlhelper.a库没有源码,咱们若想调试也只须要在ExecJNI函数中下断点便可。
delphi的一些秘密封装在运行时库中,如librtlhelper.a 和 librtl.a,主要实现了移动端RTTI的相关调用。固然若是颇有兴趣必定要深刻研究,查看system.rtti.pas可以了解大部分,好比其中的RawInvoke在x86是不须要封装到库文件中的(是开放源码的),估计x86或x86_64要构造一个JMP和CALL指令比较轻松吧。相反在ARM平台较简单的作法固然是在C++层使用va_start,va_list等宏来取得参数便可,估计这也是DispatchToImport封装到librtlhelper.a库的缘由(以及原理!),固然rtti功能不少,缘由估计也不止这一个,而在delphi中因为没有va_start,va_list宏,就须要使用寄存器取参数地址(X86或64平台在cdecl只要取EAX RAX等寄存器便可获得参数信息,因此在system.rtti.pas也能够看到相应源码,arm平台难道对EMB团队来讲很难吗,估计也不是,固然arm下我也没研究,估计相对较为麻烦,我相信EMB团队主要是为了之后的编译器统一使用LLVM的考虑吧,毕竟高深团队的建设成本过高了,EMB这么小众的市场估计支撑不起,抱大腿也是无奈之举)。