深入理解Flutter Platform Channel

閒魚技術發表於2018-08-28

作者:閒魚技術-皓黯

​ 相信讀者們在閱讀了我們之前的文章後,對Platform Channel有了一定的理解和認識。但是由於篇幅有限,上文並未對Platform Channel的工作原理進行詳細的講解。Platform Channel如何工作,訊息如何從Flutter端傳遞到Platform端,訊息如何編解碼,Platform Channel工作在什麼執行緒上,是否執行緒安全,Platform Channel能否傳遞大記憶體資料塊?本文試圖結合官方例子,對上述問題進行詳細的講解。

1. 理解Platform Channel工作原理

Flutter定義了三種不同型別的Channel,它們分別是

  • BasicMessageChannel:用於傳遞字串和半結構化的資訊。
  • MethodChannel:用於傳遞方法呼叫(method invocation)。
  • EventChannel: 用於資料流(event streams)的通訊。

三種Channel之間互相獨立,各有用途,但它們在設計上卻非常相近。每種Channel均有三個重要成員變數:

  • name: String型別,代表Channel的名字,也是其唯一識別符號。
  • messager:BinaryMessenger型別,代表訊息信使,是訊息的傳送與接收的工具。
  • codec: MessageCodec型別或MethodCodec型別,代表訊息的編解碼器。

1.1. Channel name

​ 一個Flutter應用中可能存在多個Channel,每個Channel在建立時必須指定一個獨一無二的name,Channel之間使用name來區分彼此。當有訊息從Flutter端傳送到Platform端時,會根據其傳遞過來的channel name找到該Channel對應的Handler(訊息處理器)。

1.2. 訊息信使:BinaryMessenger

binaryMessager

​ 雖然三種Channel各有用途,但是他們與Flutter通訊的工具卻是相同的,均為BinaryMessager。

​ BinaryMessenger是Platform端與Flutter端通訊的工具,其通訊使用的訊息格式為二進位制格式資料。當我們初始化一個Channel,並向該Channel註冊處理訊息的Handler時,實際上會生成一個與之對應的BinaryMessageHandler,並以channel name為key,註冊到BinaryMessenger中。當Flutter端傳送訊息到BinaryMessenger時,BinaryMessenger會根據其入參channel找到對應的BinaryMessageHandler,並交由其處理。

​ Binarymessenger在Android端是一個介面,其具體實現為FlutterNativeView。而其在iOS端是一個協議,名稱為FlutterBinaryMessenger,FlutterViewController遵循了它。

​ Binarymessenger並不知道Channel的存在,它只和BinaryMessageHandler打交道。而Channel和BinaryMessageHandler則是一一對應的。由於Channel從BinaryMessageHandler接收到的訊息是二進位制格式資料,無法直接使用,故Channel會將該二進位制訊息通過Codec(訊息編解碼器)解碼為能識別的訊息並傳遞給Handler進行處理。

​ 當Handler處理完訊息之後,會通過回撥函式返回result,並將result通過編解碼器編碼為二進位制格式資料,通過BinaryMessenger傳送回Flutter端。

1.3. 訊息編解碼器:Codec

binaryMessager

​ 訊息編解碼器Codec主要用於將二進位制格式的資料轉化為Handler能夠識別的資料,Flutter定義了兩種Codec:MessageCodec和MethodCodec。

1.3.1. MessageCodec

​ MessageCodec用於二進位制格式資料與基礎資料之間的編解碼。BasicMessageChannel所使用的編解碼器就是MessageCodec。

​ Android中,MessageCodec是一個介面,定義了兩個方法:encodeMessage接收一個特定的資料型別T,並將其編碼為二進位制資料ByteBuffer,而decodeMessage則接收二進位制資料ByteBuffer,將其解碼為特定資料型別T。iOS中,其名稱為FlutterMessageCodec,是一個協議,定義了兩個方法:encode接收一個型別為id的訊息,將其編碼為NSData型別,而decode接收NSData型別訊息,將其解碼為id型別資料。

​ MessageCodec有多種不同的實現:

  • BinaryCodec

    BinaryCodec是最為簡單的一種Codec,因為其返回值型別和入參的型別相同,均為二進位制格式(Android中為ByteBuffer,iOS中為NSData)。實際上,BinaryCodec在編解碼過程中什麼都沒做,只是原封不動將二進位制資料訊息返回而已。或許你會因此覺得BinaryCodec沒有意義,但是在某些情況下它非常有用,比如使用BinaryCodec可以使傳遞記憶體資料塊時在編解碼階段免於記憶體拷貝。

  • StringCodec

    StringCodec用於字串與二進位制資料之間的編解碼,其編碼格式為UTF-8。

  • JSONMessageCodec

    JSONMessageCodec用於基礎資料與二進位制資料之間的編解碼,其支援基礎資料型別以及列表、字典。其在iOS端使用了NSJSONSerialization作為序列化的工具,而在Android端則使用了其自定義的JSONUtil與StringCodec作為序列化工具。

  • StandardMessageCodec

    StandardMessageCodec是BasicMessageChannel的預設編解碼器,其支援基礎資料型別、二進位制資料、列表、字典,其工作原理會在下文中詳細介紹。

1.3.2. MethodCodec

​ MethodCodec用於二進位制資料與方法呼叫(MethodCall)和返回結果之間的編解碼。MethodChannel和EventChannel所使用的編解碼器均為MethodCodec。

​ 與MessageCodec不同的是,MethodCodec用於MethodCall物件的編解碼,一個MethodCall物件代表一次從Flutter端發起的方法呼叫。MethodCall有2個成員變數:String型別的method代表需要呼叫的方法名稱,通用型別(Android中為Object,iOS中為id)的arguments代表需要呼叫的方法入參。

​ 由於處理的是方法呼叫,故相比於MessageCodec,MethodCodec多了對呼叫結果的處理。當方法呼叫成功時,使用encodeSuccessEnvelope將result編碼為二進位制資料,而當方法呼叫失敗時,則使用encodeErrorEnvelope將error的code、message、detail編碼為二進位制資料。

​ MethodCodec有兩種實現:

  • JSONMethodCodec

    JSONMethodCodec的編解碼依賴於JSONMessageCodec,當其在編碼MethodCall時,會先將MethodCall轉化為字典{"method":method,"args":args}。其在編碼呼叫結果時,會將其轉化為一個陣列,呼叫成功為[result],呼叫失敗為[code,message,detail]。再使用JSONMessageCodec將字典或陣列轉化為二進位制資料。

  • StandardMethodCodec

    MethodCodec的預設實現,StandardMethodCodec的編解碼依賴於StandardMessageCodec,當其編碼MethodCall時,會將method和args依次使用StandardMessageCodec編碼,寫入二進位制資料容器。其在編碼方法的呼叫結果時,若呼叫成功,會先向二進位制資料容器寫入數值0(代表呼叫成功),再寫入StandardMessageCodec編碼後的result。而呼叫失敗,則先向容器寫入資料1(代表呼叫失敗),再依次寫入StandardMessageCodec編碼後的code,message和detail。

1.4. 訊息處理器:Handler

​ 當我們接收二進位制格式訊息並使用Codec將其解碼為Handler能處理的訊息後,就該Handler上場了。Flutter定義了三種型別的Handler,與Channel型別一一對應。我們向Channel註冊一個Handler時,實際上就是向BinaryMessager註冊一個與之對應的BinaryMessageHandler。當訊息派分到BinaryMessageHandler後,Channel會通過Codec將訊息解碼,並傳遞給Handler處理。

1.4.1. MessageHandler

​ MessageHandler使用者處理字串或者半結構化的訊息,其onMessage方法接收一個T型別的訊息,並非同步返回一個相同型別result。MessageHandler的功能比較基礎,使用場景較少,但是其配合BinaryCodec使用時,能夠方便傳遞二進位制資料訊息。

1.4.2. MethodHandler

​ MethodHandler用於處理方法的呼叫,其onMessage方法接收一個MethodCall型別訊息,並根據MethodCall的成員變數method去呼叫對應的API,當處理完成後,根據方法呼叫成功或失敗,返回對應的結果。

1.4.3. StreamHandler

binaryMessager

​ StreamHandler與前兩者稍顯不同,用於事件流的通訊,最為常見的用途就是Platform端向Flutter端傳送事件訊息。當我們實現一個StreamHandler時,需要實現其onListenonCancel方法。而在onListen方法的入參中,有一個EventSink(其在Android是一個物件,iOS端則是一個block)。我們持有EventSink後,即可通過EventSink向Flutter端傳送事件訊息。

​ 實際上,StreamHandler工作原理並不複雜。當我們註冊了一個StreamHandler後,實際上會註冊一個對應的BinaryMessageHandler到BinaryMessager。而當Flutter端開始監聽事件時,會傳送一個二進位制訊息到Platform端。Platform端用MethodCodec將該訊息解碼為MethodCall,如果MethodCall的method的值為"listen",則呼叫StreamHandler的onListen方法,傳遞給StreamHandler一個EventSink。而通過EventSink向Flutter端傳送訊息時,實際上就是通過BinaryMessager的send方法將訊息傳遞過去。

2. 理解訊息編解碼過程

​ 在官方文件《Writing custom platform-specific code with platform channels》中的獲取裝置電量的例子中我們發現,Android端的返回值是java.lang.Integer型別的,而iOS端返回值則是一個NSNumber型別的(通過NSNumber numberWithInt:獲取)。而到了Flutter端時,這個返回值自動"變成"了dart語言的int型別。那麼這中間發生了什麼呢?

​ Flutter官方文件表示,standard platform channels使用standard messsage codecmessageresponse進行序列化和反序列化,messageresponse可以是booleans, numbers, Strings, byte buffers,List, Maps等等,而序列化後得到的則是二進位制格式的資料。

​ 所以在上文提到的例子中,java.lang.IntegerNSNumber型別的返回值先是被序列化成了一段二進位制格式的資料,然後該資料傳遞到傳遞到flutter側後,被反序列化成了dart語言中的int型別的資料。

​ Flutter預設的訊息編解碼器是StandardMessageCodec,其支援的資料型別如下:

binaryMessager

​ 當message或response需要被編碼為二進位制資料時,會呼叫StandardMessageCodec的writeValue方法,該方法接收一個名為value的引數,並根據其型別,向二進位制資料容器(NSMutableData或ByteArrayOutputStream)寫入該型別對應的type值,再將該資料轉化為二進位制表示,並寫入二進位制資料容器。

​ 而message或者response需要被解碼時,使用的是StandardMessageCodec的readValue方法,該方法接收到二進位制格式資料後,會先讀取一個byte表示其type,再根據其type將二進位制資料轉化為對應的資料型別。

​ 在獲取裝置電量的例子中,假設裝置的電量為100,當這個值被轉化為二進位制資料時,會先向二進位制資料容器寫入int型別對應的type值:3,再寫入由電量值100轉化而得的4個byte。而當Flutter端接收到該二進位制資料時,先讀取第一個byte值,並根據其值得出該資料為int型別,接著,讀取緊跟其後的4個byte,並將其轉化為dart型別的int。

binaryMessager

​ 對於字串、列表、字典的編碼會稍微複雜一些。字串使用UTF-8編碼得到的二進位制資料是長度不定的,因此會在寫入type後,先寫入一個代表二進位制資料長度的size,再寫入資料。列表和字典則是寫入type後,先寫入一個代表列表或字典中元素個數的size,再遞迴呼叫writeValue方法將其元素依次寫入。

3. 理解訊息傳遞過程

​ 訊息是如何從Flutter端傳遞到Platform端的呢?接下來我們以一次MethodChannel的呼叫為例,去理解訊息的傳遞過程。

3.1. 訊息傳遞:從Flutter到Platform

3.1.1. Dart層

​ 當我們在Flutter端使用MethodChannel的invokeMethod方法發起一次方法呼叫時,就開始了我們的訊息傳遞之旅。invokeMethod方法會將其入參messagearguments封裝成一個MethodCall物件,並使用MethodCodec將其編碼為二進位制格式資料,再通過BinaryMessages將訊息發出。(注意,此處提到的類名與方法名均為dart層的實現)

​ 上述過程最終會呼叫到ui.Window的_sendPlatformMessage方法,該方法是一個native方法,其實現在native層,這與Java的JNI技術非常類似。我們向native層傳送了三個引數:

  • name,String型別,代表Channel名稱
  • data,ByteData型別,即之前封裝的二進位制資料
  • callback,Function型別,用於結果回撥

3.1.2. Native層

​ 到native層後,window.cc的SendPlatformMessage方法接受了來自dart層的三個引數,並對它們做了一定的處理:dart層的回撥callback封裝為native層的PlatformMessageResponseDart型別的response;dart層的二進位制資料data轉化為std::vector<uint8_t>型別資料data;根據response,data以及Channel名稱name建立一個PlatformMessage物件,並通過dart_state->window()->client()->HandlePlatformMessage方法處理PlatformMessage物件。

dart_state->window()->client()是一個WindowClient,而其具體的實現為RuntimeController,RuntimeController會將訊息交給其代理RuntimeDelegate處理。

​ RuntimeDelegate的實現為Engine,Engine在處理Message時,會判斷該訊息是否是為了獲取資源(channel等於"flutter/assets"),如果是,則走獲取資源邏輯,否則呼叫Engine::Delegate的OnEngineHandlePlatformMessage方法。

​ Engine::Delegate的具體實現為Shell,其OnEngineHandlePlatformMessage接收到訊息後,會向PlatformTaskRunner新增一個Task,該Task會呼叫PlatformView的HandlePlatformMessage方法。值得注意的是,Task中的程式碼執行在Platform Task Runner中,而之前的程式碼均執行在UI Task Runner中。

binaryMessager

3.2. 訊息處理

​ PlatformView的HandlePlatformMessage方法在不同平臺有不同的實現,但是其基本原理是相同的。

3.2.1. PlatformViewAndroid

​ PlatformViewAndroid的是Platformview的子類,也是其在Android端的具體實現。當PlatformViewAndroid接收到PlatformMessage型別的訊息時,如果訊息中有response(型別為PlatformMessageResponseDart),則生成一個自增長的response_id,並以response_id為key,response為value存入字典pending_responses_中。接著,將channeldata均轉化為Java可識別的資料,通過JNI向Java層發起呼叫,將response_idchanneldata傳遞過去。

​ Java層中,被呼叫的程式碼為FlutterNativeView (BinaryMessager的具體實現)的handlePlatformMessage,該方法會根據channel找到對應的BinaryMessageHandler並將訊息傳遞給它處理。其具體處理過程我們已經在上文中詳細分析過了,此處不再贅述。

​ BinaryMessageHandler處理完成後,FlutterNativeView會通過JNI呼叫native的方法,將response_dataresponse_id傳遞到native層。

​ native層,PlatformViewAndroid的InvokePlatformMessageResponseCallback接收到了respond_idresponse_data。其先將response_data轉化為二進位制結果,並根據response_id,從panding_responses_中找到對應的PlatformMessageResponseDart物件,呼叫其Complete方法將二進位制結果返回。

binaryMessager

3.2.2. PlatformViewIOS

​ PlatformViewIOS是PlatformView的子類,也是其在iOS端的具體實現,當PlatformViewIOS接收到message時會交給PlatformMessageRouter處理。

​ PlatformMessageRouter通過PlatformMessage中的channel找到對應的FlutterBinaryMessageHandler,並將二進位制訊息其處理,訊息處理完成後,直接呼叫PlatformMessage物件中的PlatformMessageResponseDart物件的Complete方法將二進位制結果返回。

3.3. 結果回傳:從Platform到Flutter

​ PlatformMessageResponseDart的Complete方法向UI Task Runner新增了一個新的Task,這個Task的作用是將二進位制結果從native的二進位制資料型別轉化為Dart的二進位制資料型別response,並呼叫dart的callback將response傳遞到Dart層。

​ Dart層接收到二進位制資料後,使用MethodCodec將資料解碼,並返回給業務層。至此,一次從Flutter發起的方法呼叫就完整結束了。

4. 問題解析

4.1. Platform Channel的程式碼執行在什麼執行緒

​ 在文章《深入理解Flutter引擎執行緒模型》中提及,Flutter Engine自己不建立執行緒,其執行緒的建立於管理是由enbedder提供的,並且Flutter Engine要求Embedder提供四個Task Runner,分別是Platform Task Runner,UI Task Runner,GPU Task Runner和IO Task Runner。

​ 實際上,在Platform側執行的程式碼執行在Platform Task Runner中,而在Flutter app側的程式碼則執行在UI Task Runner中。在Android和iOS平臺上,Platform Task Runner跑在主執行緒上。因此,不應該在Platform端的Handler中處理耗時操作。

4.2. Platform Channel是否執行緒安全

​ Platform Channel並非是執行緒安全的,這一點在官方的文件也有提及。Flutter Engine中多個元件是非執行緒安全的,故跟Flutter Engine的所有互動(介面呼叫)必須發生在Platform Thread。故我們在將Platform端的訊息處理結果回傳到Flutter端時,需要確保回撥函式是在Platform Thread(也就是Android和iOS的主執行緒)中執行的。

4.3. 是否支援大記憶體資料塊的傳遞

​ Platform Channel實際上是支援大記憶體資料塊的傳遞,當需要傳遞大記憶體資料塊時,需要使用BasicMessageChannel以及BinaryCodec。而整個資料傳遞的過程中,唯一可能出現資料拷貝的位置為native二進位制資料轉化為Dart語言二進位制資料。若二進位制資料大於閾值時(目前閾值為1000byte)則不會拷貝資料,直接轉化,否則拷貝一份再轉化。

4.4. 如何將Platform Channel原理應用到開發工作中

​ 實際上Platform Channel的應用場景非常多,我們這裡舉一個例子:

​ 在平常的業務開發中,我們需要使用到一些本地圖片資源,但是Flutter端是無法使用Platform端已存在的圖片資源的。當Flutter端需要使用一個Platform端已有的圖片資源時,只有將該圖片資源拷貝一份到Flutter的Assert目錄下才能使用。實際上,讓Flutter端使用Platform端的資源並不是一件難事。

​ 我們可以使用BasicMessageChannel來完成這個工作。Flutter端將圖片資源名name傳遞給Platform端,Native端使用Platform端接收到name後,根據name定位到圖片資源,並將該圖片資源以二進位制資料格式,通過BasicMessageChannel,傳遞迴Flutter端。

總結

​ 在Flutter與Native混合開發的模式下,Platform Channel的應用場景非常多,理解Platform Channel的工作原理,有助於我們在從事這方面開發時能做到得心應手。

​ 最後,閒魚技術團隊廣招各類方向的達人,無論你是精通移動端,前端,後臺,還是機器學習,音視訊,自動化測試等,都歡迎投遞簡歷加入我們,一同用技術改善生活!

簡歷投遞:guicai.gxy@alibaba-inc.com

參考

flutter.io/platform-ch…

github.com/flutter/flu…

github.com/flutter/eng…

相關文章