作者:閒魚技術-皓黯
相信讀者們在閱讀了我們之前的文章後,對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
雖然三種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
訊息編解碼器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
StreamHandler與前兩者稍顯不同,用於事件流的通訊,最為常見的用途就是Platform端向Flutter端傳送事件訊息。當我們實現一個StreamHandler時,需要實現其onListen
和onCancel
方法。而在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 codec
對message
和response
進行序列化和反序列化,message
與response
可以是booleans
, numbers
, Strings
, byte buffers
,List
, Maps
等等,而序列化後得到的則是二進位制格式的資料。
所以在上文提到的例子中,java.lang.Integer
或NSNumber
型別的返回值先是被序列化成了一段二進位制格式的資料,然後該資料傳遞到傳遞到flutter側後,被反序列化成了dart語言中的int
型別的資料。
Flutter預設的訊息編解碼器是StandardMessageCodec,其支援的資料型別如下:
當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。
對於字串、列表、字典的編碼會稍微複雜一些。字串使用UTF-8編碼得到的二進位制資料是長度不定的,因此會在寫入type後,先寫入一個代表二進位制資料長度的size,再寫入資料。列表和字典則是寫入type後,先寫入一個代表列表或字典中元素個數的size,再遞迴呼叫writeValue
方法將其元素依次寫入。
3. 理解訊息傳遞過程
訊息是如何從Flutter端傳遞到Platform端的呢?接下來我們以一次MethodChannel的呼叫為例,去理解訊息的傳遞過程。
3.1. 訊息傳遞:從Flutter到Platform
3.1.1. Dart層
當我們在Flutter端使用MethodChannel的invokeMethod
方法發起一次方法呼叫時,就開始了我們的訊息傳遞之旅。invokeMethod
方法會將其入參message
和arguments
封裝成一個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中。
3.2. 訊息處理
PlatformView的HandlePlatformMessage
方法在不同平臺有不同的實現,但是其基本原理是相同的。
3.2.1. PlatformViewAndroid
PlatformViewAndroid的是Platformview的子類,也是其在Android端的具體實現。當PlatformViewAndroid接收到PlatformMessage型別的訊息時,如果訊息中有response
(型別為PlatformMessageResponseDart),則生成一個自增長的response_id
,並以response_id
為key,response
為value存入字典pending_responses_
中。接著,將channel
和data
均轉化為Java可識別的資料,通過JNI向Java層發起呼叫,將response_id
、channel
和data
傳遞過去。
Java層中,被呼叫的程式碼為FlutterNativeView (BinaryMessager的具體實現)的handlePlatformMessage
,該方法會根據channel
找到對應的BinaryMessageHandler並將訊息傳遞給它處理。其具體處理過程我們已經在上文中詳細分析過了,此處不再贅述。
BinaryMessageHandler處理完成後,FlutterNativeView會通過JNI呼叫native的方法,將response_data
和response_id
傳遞到native層。
native層,PlatformViewAndroid的InvokePlatformMessageResponseCallback
接收到了respond_id
和response_data
。其先將response_data
轉化為二進位制結果,並根據response_id
,從panding_responses_
中找到對應的PlatformMessageResponseDart物件,呼叫其Complete
方法將二進位制結果返回。
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