天天看點

深入了解Flutter Platform Channel

作者:閑魚技術-皓黯

​ 相信讀者們在閱讀了我們之前的文章後,對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

深入了解Flutter Platform Channel

​ 雖然三種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

深入了解Flutter Platform Channel

​ 消息編解碼器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

深入了解Flutter Platform Channel

​ 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,其支援的資料類型如下:

深入了解Flutter Platform Channel

​ 當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。

深入了解Flutter Platform Channel

​ 對于字元串、清單、字典的編碼會稍微複雜一些。字元串使用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中。

深入了解Flutter Platform Channel

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

方法将二進制結果傳回。

深入了解Flutter Platform Channel

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]

參考

https://flutter.io/platform-channels/ https://github.com/flutter/flutter https://github.com/flutter/engine

繼續閱讀