【Delphi】探索FMX封装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类接口,得到该java类的方法表(使用RTTI方法),再结合TRawVirtualClass生成虚拟类同名java类接口,我们知道delphi里如果只是interface是无法直接使用的(非COM技术),必须要将接口继承到某个类再把方法实现了才能使用接口,而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内部继承自TJavaImport,名称规则是TJXXXX,这是一个类,delphi直接create就可以使用(在delphi的jni里不建议直接使用,通常会报错),或不用create即可使用其class类方法(即通常调用wrap方法,该方法就是一个class function,通过TJXXXX.Wrap调用)。

       实际中我们需要的方法都在Jbluetooth_BluetoothClass里,但我们知道Jbluetooth_BluetoothClass是一个delphi接口,在delphi里接口方法必须要实现了才能使用,而我们看到该接口只被继承到TJbluetooth_BluetoothClass,但TJbluetooth_BluetoothClass又没有看到对接口方法进行实现,且实际上其方法实现都在java的同名类里,不可能在delphi层实现的。那delphi怎么做到调用一个delphi接口的方法就能够直接调用java类的方法呢。

       在这里,我们先说明一下在JNI中调用java类实例的方法即可,也就是需要通过JNIEvn的CallXXXMethod来间接调用。 delphi却可以通过调用Jbluetooth_BluetoothClass接口方法就能等同JNI的一系列操作,其原理就像最上面说的,将Jbluetooth_BluetoothClass接口的方法表收集了并保存到TJavaVTable里,这样TJavaImport就可以根据TJavaVTable创建一个Jbluetooth_BluetoothClass接口的虚拟类,该虚拟类其实就是TJavaImport(可将TJavaImport理解为虚拟类的代理),如果需要获得Jbluetooth_BluetoothClass接口,直接使用TJavaImport.QueryInterface即可,而封装的最后就是TJbluetooth_BluetoothClass继承自TJavaGenericImport,TJavaGenericImport内部再创建TJavaImport,TJavaGenericImport是一个泛型类,其作用是传递TJavaImport所需要的Jbluetooth_BluetoothClass接口信息。

       讲这么绕口,其实只要理解,我们虽然没有在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代码的问题(有点绕,接下来讲为什么)。

       在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是librtlhelper.a库里实现的,librtlhelper.a没找到开源代码,FMX的秘密在此只能猜测了,所以以下是猜测结果(有兴趣的可以去反编译确认,我相信和猜测的结果大致相同):

        当我们调用某一个接口的方法时,实际上是调用该接口对应虚拟类的同名类实例方法,而类实例的方法又到了DispatchToImport函数中,DispatchToImport函数中根据参数再次调用ExecJNI函数,最终调用了JNI的CallXXXMethod接口。

         所以,即使librtlhelper.a库没有源码,我们若想调试也只需要在ExecJNI函数中下断点即可。

       delphi的一些秘密封装在运行时库中,如librtlhelper.a 和 librtl.a,主要实现了移动端RTTI的相关调用。当然如果很有兴趣一定要深入研究,查看system.rtti.pas能够了解大部分,比如其中的RawInvoke在x86是不需要的,估计x86或x86_64要构造一个JMP和CALL指令比较轻松吧。

猜你喜欢

转载自www.cnblogs.com/caibirdy1985/p/9955885.html