作者:閑魚技術-皓黯
相信讀者們在閱讀了我們之前的文章後,對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層發送了三個參數:
-
,String類型,代表Channel名稱name
-
,ByteData類型,即之前封裝的二進制資料data
-
,Function類型,用于結果回調callback
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的工作原理,有助于我們在從事這方面開發時能做到得心應手。
最後,閑魚技術團隊廣招各類方向的達人,無論你是精通移動端,前端,背景,還是機器學習,音視訊,自動化測試等,都歡迎投遞履歷加入我們,一同用技術改善生活!
履歷投遞:[email protected]