【Delphi】探索FMX的Android子框架-封裝JNI的祕密

       因爲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

最關鍵的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調用)。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。

 

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包。

最關鍵的函數

        在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這麼小衆的市場估計支撐不起,抱大腿也是無奈之舉)。

相關文章
相關標籤/搜索