【Delphi】探索FMX的Android子框架-封装JNI的秘密
由于android的APP由java开发,因此FMX在开发android时也遵循了JAVA的协议,而且是最常见的JNI协议,在JNI中我们知道使用JVM的env接口来对接java内部的各种类,实例,比如调用某个实例的方法。各种语言对JNI的封装程度不同,而且封装的质量往往体现在各自语言对JAVA的控制自由度上。比方说,如果只是导入了JNI的头文件,那即使最简单的调用ToString方法,也会变得非常麻烦。
基于对delphi的喜欢,我就斗胆说一句,目前所有语言对JNI的封装程度,唯有Delphi最高端(在Delphi面前,其他都是渣渣),因为你可以使用Delphi的类来直接调用JAVA实例或类的方法,而且使用过程中甚至感觉不到JNI的存在。
在FMX框架中,对于android的JNI支持,最关键的代码文件就是Androidapi.JNIBridge.pas和Androidapi.JNIMarshal.pas。
最关键的2个类
在Androidapi.JNIBridge.pas文件中,最关键的类是 TJavaImport 和 TJavaLocal
TJavaImport : 导入java类方法,使用RTTI动态生成与java类同名的interface接口;
大致原理是,通过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调用)。
关于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接口共同协作完成的。
那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。
TJavaLocal:本地化java类方法,从java中直接调用delphi类
这里先说明下,其实并不是本地化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包,我觉得FMX可以考虑使用javassist动态地自动创建接口和类来做到无java纯pascal。
最关键的函数
在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这么小众的市场估计支撑不起,抱大腿也是无奈之举)。