Flutter 是谷歌推出的移動 UI 框架,能夠快速在 iOS 和 Android 上構建高質量的原生用戶界面,被愈來愈多的開發者選擇和使用。拍樂雲也提供了功能強大的 Pano Flutter SDK,性能穩定且易用,覆蓋語音通話、視頻通話、互動白板、互動直播、雲端錄製等各類功能。在以前的一篇《Pano Flutter SDK 全新發布》中,咱們給你們介紹了SDK的詳細接入流程,今天將繼續聊聊咱們 Pano Flutter SDK 的設計思路與實踐經驗。java
#1git
整體結構github
Pano 針對原生應用開發提供了完備的高性能SDK,因此 Pano Flutter SDK 採用插件包的形式來封裝咱們的SDK。相似的,在 RN 中咱們採用 NativeModule 來實現 Pano RN SDK。Pano 移動端跨平臺 SDK 的整體結構以下圖所示:緩存
SDK 分爲三層結構,底層爲 Pano 原生 SDK(iOS&Android)。基於原生SDK 之上爲橋接層,因爲 Flutter 與 RN 中與原生層通訊均爲異步通訊,且需使用特定的通訊方式(Flutter 使用平臺通道方案,RN 則使用原生模塊方案),因此須要將跨平臺調用進行轉換才能調用原生 SDK 方法。所以橋接層將分爲兩個部分,原生 SDK 橋接與跨平臺(Flutter&RN)橋接,以達到最大化代碼複用的目的,將原生 SDK 接口二次封裝成通用異步接口,在其上分別對接 Flutter 和 RN 的通訊接口。SDK 最頂層則爲跨平臺層,對接原生層通訊接口封裝出 Flutter 或 RN 平臺的功能接口。markdown
雖然最終的結構比較簡潔明瞭,可是因爲 Flutter 或 RN 對於視圖更新機制與原生開發存在較大差別,以及跨平臺層與原生層數據結構的不一樣等問題,致使 SDK 的設計與實現中存在許多涉及數據轉換、對象映射、內存管理等難點或坑點,接下來將結合 SDK 的設計思路與實踐經驗,針對其中幾個典型的問題談談解決方案或須要注意的地方。數據結構
#2框架
工做流程異步
Pano Flutter SDK 提供的 API 基本上與原生 SDK 保持一一對應的關係,以便開發人員能夠輕鬆的將對接原生 SDK 開發經驗應用到 Flutter 中。但因爲 Flutter 特殊的平臺通道(Platform Channel)方案以及視圖更新機制,因此並非簡單的將原生 SDK 接口進行透傳封裝,SDK 的調用流程以下圖所示:ide
SDK 使用 Flutter 平臺通道中 MethodChannel 與 EventChannel 來實現Flutter 層與原生層通訊,其中 MethodChannel 用於 Flutter 向原生層方法調用,EventChannel 則用於原生層向 Flutter 層進行數據流通訊,這裏主要是傳遞原生層回調消息。當開發者調用 Flutter 層接口,SDK 使用對應的 MethodChannel 將方法名、參數傳遞到原生層,SDK 在這裏實現了 Flutter Native Bridge 來專門處理這些調用。工具
建議:當原生層接收到MethodChannel的方法調用時(例如:iOS爲-[FlutterPlugin handleMethodCall:result:]),採用反射調用(例如:iOS中使用NSSelectorFromString獲取selector,而後經過[NSObject performSelector:withObject:]調用)Native SDK Bridge方法,這樣能夠儘可能將Flutter的邏輯與原生橋接層邏輯隔離,一方面作薄對接Flutter層邏輯,另外一方面將須要常常跟隨原生SDK變更的原生橋接層邏輯與其它跨平臺框架(如RN)進行復用,減小維護成本。 注意:Flutter 中沒有現成的二進制數據類型,一般採用Uint8List來代替,但經過平臺通道轉換後,在iOS端會轉換成FlutterStandardTypedData類型,該類型不能自動轉換爲NSData類型,須要經過其屬性data來獲取實際的NSData對象。但在從原生層調用Flutter層時,能夠直接傳遞NSData對象,其將會在 Flutter 層被自動轉換爲Uint8List。 Flutter 中平臺通道其實是將傳遞的數據編碼成消息的形式,跨線程發送到該應用所在的宿主原生層。而且 Native SDK Bridge 對接原生 SDK,將原生 SDK 方法實行完畢後的返回值經過 callback 返回時,也是將數據編碼成消息經過一樣方式原路返回給 Flutter 層。整個過程的消息和響應是異步的,這也就是 Flutter 層接口都設計成異步接口的緣由。
注意:MethodChannel類型中,調用原生方法使用Future<T?> invokeMethod(String method, [ dynamic arguments ]),對於SDK返回Flutter支持的基本類型數據時,直接調用沒有任何問題,例如當獲取SDK版本號接口返回String類型,則Flutter層接口能夠實現爲:static Future getSdkVersion() { // iOS中NSString和Andorid中java.lang.String均可以自動轉換爲Flutter的String類型 return _methodChannel.invokeMethod('getSdkVersion'); } 但當返回非基本類型時,返回值就須要進行轉換,例如開啓音頻接口因爲可能有多種結果,因此返回值是枚舉類型ResultCode,若是直接按照如下寫法實現將會報錯:Future startAudio() { return _methodChannel.invokeMethod('startAudio');// 錯誤:返回值爲int不會自動轉換Flutter的枚舉類型 } 須要增長轉換邏輯,例如:Future startAudio() { return _methodChannel.invokeMethod('startAudio').then((value) { return ResultCodeConverter.fromValue(value).e as T; // ResultCodeConverter爲將int轉換ResultCode的工具類 }); } 建議:因爲 SDK 中存在大量的返回 ResultCode 的方法,在每一個接口實現處都增長轉換代碼繁瑣且冗餘,因此咱們對於這種狀況能夠提取一個公共模板方法,能很大程度提高代碼簡潔度,例如:Future_invokeMethod(String method, [Map<String, dynamic> arguments]) { if (T == ResultCode) { // 判斷當前範型爲ResultCode時,增長轉換邏輯 return _methodChannel.invokeMethod(method, arguments).then((value) { return ResultCodeConverter.fromValue(value).e as T; }); } else { // 其餘能夠自動轉換的狀況則返回調用結果 return _methodChannel.invokeMethod(method, arguments); } } 以上是 Flutter 調用原生層的流程,那當原生層須要回調事件給 Flutter層咱們應該怎麼作呢?這時就須要利用 EventChannel 來實現。先看下EventChannel 的基本流程:
原生層調用 setStreamHandler(iOS爲-[FlutterEventChannel setStreamHandler:])註冊 Handler 實現; EventChannel 初始化完成後,經過StreamHandler的onListen(iOS爲-[FlutterStreamHandler onListenWithArguments:eventSink:])回調接口獲取eventSink引用並保存; Flutter 層調用 EventChannel 的 receiveBroadcastStream 註冊監聽; 原生層經過調用 eventSink 發送事件消息。 建議:EventChannel 因爲是數據流通訊,跟 MethodChannel 不一樣之處在於沒有封裝出針對方法回調的模型,但目前 SDK 中原生層向Flutter 層均爲方法回調,因此咱們將回調數據組裝成特定格式的鍵值對,如:{ "methodName": xxxx, // 回調方法名 "data": [xxxx,xxxx...] // 回調參數列表 } 而後在 Flutter 層進行統一解析處理:void setEventHandler(RtcEngineEventHandler handler) { _handler = handler; ... _eventChannel.receiveBroadcastStream().listen((event) { final eventMap = Map<dynamic, dynamic>.from(event); final methodName = eventMap['methodName'] as String; final data = List.from(eventMap['data']); _handler?.process(methodName, data); }); } 至此經過以上方案,已經能夠封裝原生 SDK 的絕大部分功能以 Flutter SDK 形式提供出去了。但還剩一個重要的問題須要解決,那就是如何設置原生層視圖的邏輯。
#3
設置原生視圖
因爲 Flutter 提供的平臺通道方案本質上是以字節流的方式在線程間傳遞數據,因此對於原生層視圖等非序列化的對象是不支持的。Flutter 在如何內嵌原生層視圖的問題上,提供了平臺視圖(Platform-views)方案,開發者能夠在 Flutter 層建立原生視圖的映射(iOS爲UiKitView,Android 爲 AndroidView),並嵌入到 Widget 中。
那如何將生成的原生視圖對象傳遞給原生層 SDK?在 Flutter 建立原生視圖後,會返回視圖對應惟一的 id,因此最直觀的方法就是在 id 返回後,分別在原生層與 Flutter 層生成對應的 MethodChannel,組成鍵值對緩存起來,在調用時經過 id 查找 MethodChannel,而後經過 MethodChannel 傳遞方法調用消息。但這樣作有兩個明顯缺陷:
MethodChannel 沒有與 Widget 直接關聯,在 Widget 銷燬時須要手動清除鍵值對中的 MethodChannel; 採用 id 做爲原生視圖的標識,因爲缺乏有效性檢查,可能致使調用到無效 MethodChannel 拋出異常。 而且一般原生SDK方法中是須要原生視圖做爲參數傳入,但因爲只能經過與視圖對應的MethodChannel才能在原生層訪問到對應的原生視圖對象,致使無法直接在Flutter層設計出相似原生SDK的方法。
建議:Pano Flutter SDK中咱們爲了儘可能保持與原生SDK的接口一致性,採起了一種曲線救國的方案。在建立渲染視圖RtcSurfaceView(StatefulWidget)後,回調返回保存了MethodChannel的ViewModel對象: class RtcSurfaceViewModel { final MethodChannel _methodChannel;
Future invokeMethod(String method, [Map<String, dynamic> arguments]) { if (T == ResultCode) { return _methodChannel.invokeMethod(method, arguments).then((value) { return ResultCodeConverter.fromValue(value).e as T; }); } else { return _methodChannel.invokeMethod(method, arguments); } }
RtcSurfaceViewModel(this._methodChannel); } 而後按照須要原生視圖的SDK方法,定義出對應的Flutter層接口,接收ViewModel做爲參數,方法實現調用ViewModel的MethodChannel傳遞方法消息,例如開啓視頻時調用startVideo接口定義以下:Future startVideo(RtcSurfaceViewModel viewModel, {RtcRenderConfig config}) { config ??= RtcRenderConfig(); return viewModel.invokeMethod('startVideo', {'config': config.toJson()}); } 在原生層視圖對應的MethodChannel接收到方法調用,經過原生層內部緩存的engine對象,調用對應的SDK方法(如startVideo),傳入原生層視圖完成接口調用。這樣作,一方面讓MethodChannel與Widget關聯,另外一方面在接口調用上也使用ViewModel對象保證了傳值的有效性。而且接口上也基本與原生SDK保持了一致性,下降了對接SDK的開發人員的理解成本,也兼顧了代碼的維護成本。 #4
結語
現今廣大開發每每會遇到各類各樣跨平臺開發的需求或問題,而拍樂雲一直以來堅持以開發者爲先,和用戶在一塊兒。Pano Flutter SDK 所有開源,你能夠經過 GitHub(github.com/PanoVideo/P… )查看完整源碼。經過本篇介紹 Pano Flutter SDK 的跨平臺 SDK 設計與實踐經驗,但願能給你們帶來一些幫助與啓發。