1. 程式人生 > >【Delphi】探索FMX封裝JNI的祕密

【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指令比較輕鬆吧。