最右JS2Flutter框架——通信機制

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong"}],"text":"1、概述"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"通信包括異步和同步兩種方式,異步可根據是否關注返回結果再細分成Request-Reply和One-Way兩種。JS2Flutter框架的通信機制也是在不斷的迭代中逐步完善,年初發的文章"},{"type":"link","attrs":{"href":"https://www.infoq.cn/article/MIa5AN2JE51uor4JeiPG","title":""},"content":[{"type":"text","text":"Flutter動態化在最右App中的實踐"}],"marks":[{"type":"size","attrs":{"size":12}}]},{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[1]中還是通過對JSWorkThread的阻塞,去實現Client側到Host側的單向同步,如今已經完成了Client和Host之間真正意義上的雙向同步直連。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong"}],"text":"2、異步"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong"}],"text":"2.1 One-Way"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"對於單向無需關注返回結果的調用,實現起來很簡單,在"},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/acee65b914dc4d0e32a5561a1","title":""},"content":[{"type":"text","text":"最右JS2Flutter框架——開篇"}],"marks":[{"type":"size","attrs":{"size":12}}]},{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[2]中,我們講述了Hello World實現的例子,這就是一個最簡單的One-Way調用,Client和Native之間可以通過建立通信,Native和Host之間可以通過PlatformChannel建立通信,這樣就完成Client和Host之間的通信。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong"}],"text":"2.2 Request-Reply"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"對於One-Way的調用,很容易就可以實現,假如我們需要一個返回結果呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"舉個例子,開發者想要獲取剪切板數據,調用了Clipboard.getData,在運行階段這個函數是在JS運行的,它必須向Host側真實的Clipboard.getData發起調用,Host側在拿到結果之後,通知Client側,Client側把結果給調用者。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"很容易想到Client側通過JSCore向Native註冊回調函數,Native通過MethodChannel設置MethodCallHandler,當Native收到Host側的回調時,Native調用Client側的回調函數。JS2Flutter框架就是這樣去實現的,但卻又不止於此,僅僅使用回調函數來做異步的話,當遇到大量的異步回調場景時,很可能陷入\"回調地獄\",其實Dart爲了避免類似的問題,引入了Future,JS也引入了Promise。回調函數只作爲Native回調Client側的一個句柄,真正在框架中發揮威力的是通過Future和Completer去實現的callFlutterWithReply。"}]},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"Map _completerMap = new Map();\n\nFuture callFlutterWithReply(String method, { dynamic key, String action, dynamic data }) async {\n int callbackId = generateGlobalId();\n invokeFlutter(method, _buildBody(key, action, data, callbackId));\n Completer completer = Completer();\n _completerMap[callbackId] = completer;\n T typedResult = await completer.future;\n return typedResult;\n}\n\n//當收到Native回調\n...\n Completer completer = _completerMap[callbackId];\n if(completer != null) {\n completer.complete(data);\n _completerMap.remove(callbackId);\n }\n..."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"至此,Client側面向Host側的異步調用便建立起來了,同理,可建立Host側面向Client側的異步調用,這樣就完成了雙向的異步通信機制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong"}],"text":"3、同步"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"同步的需求場景也很多,我們在"},{"type":"link","attrs":{"href":"https://www.infoq.cn/article/MIa5AN2JE51uor4JeiPG","title":""},"content":[{"type":"text","text":"Flutter動態化在最右App中的實踐"}],"marks":[{"type":"size","attrs":{"size":12}}]},{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[1]中也有列舉,比如你要通過TextPainter來測量文字的高度,是需要同步機制才能保證正確性的,當時我們是通過阻塞JSWorkThread去實現Client側的同步,因爲這是一個與UI繪製無關的線程,所以它的阻塞並不影響渲染和事件接收,但後來我們發現同步機制不僅是Client側需要的,Host側也有可能需要。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"舉個例子,我們在實現ListView控件時,如果是通過builder方式去構建,當遇到大量數據的時候,因爲每一項都是實時構建,需要先詢問Client側該位置的子樹並立即還原,如果是異步去刷新的話,很可能後幾項先顯示出來,從而造成視覺上的錯亂。所以,在Host側我們也需要建立同步機制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"那在Host側是否可以採用同樣的方案建立同步機制呢?當然也是可行的,但最右沒有采用這種方案,試想一下,本來JS到Flutter就需要經歷JSWorkThread、MainThread、Flutter UIThread,再來一層循環阻塞,效率肯定是大打折扣。我們採用了更高效的方式,讓JS運行在Flutter的UI線程裏,構建一套Client和Host之間直連的通信渠道,這樣就能實現真正意義上的雙向同步。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"這個過程中我們需要解決兩個問題:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"1:如何讓代碼塊兒能在Flutter UI線程執行?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"2:怎樣才能建立直連通信渠道?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"要解決第一個問題,我們需要了解Flutter UI線程Work的方式,這裏直接借用Gityuan的博客——"},{"type":"link","attrs":{"href":"http://gityuan.com/2019/06/15/flutteruidraw/","title":""},"content":[{"type":"text","text":"Flutter渲染機制——UI線程"}],"marks":[{"type":"size","attrs":{"size":12}}]},{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[3],Engine是通過UITaskRunner提交任務,從而讓任務在UI線程執行。理解了這一點,我們就可以擴充FlutterEngine的能力,增加一個postTaskOnUIThread函數,XCJSRuntime在啓動時,讓JS運行在Flutter UI線程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"第二個問題相對要複雜的多,牽涉到的面也比較廣,我們先看看系統是如何建立通信渠道的,不瞭解的同學可以看看Gityuan的博客——"},{"type":"link","attrs":{"href":"http://gityuan.com/2019/08/10/flutter_channel/","title":""},"content":[{"type":"text","text":"深入理解Flutter的Platform Channel機制"}],"marks":[{"type":"size","attrs":{"size":12}}]},{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[4],理解了系統的流程(博客以Android爲例,iOS側同理)之後,首先要思考的就是我們與系統不一樣的地方,Platform Channel是爲了在Flutter和Platform之間建立通信渠道,它們在不同的線程,而我們是在同一個線程,所以我們不需要再去轉線程。舉個例子:"}]},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"// |PlatformView::Delegate|\nvoid Shell::OnPlatformViewDispatchPlatformMessage(\n fml::RefPtr message) {\n FML_DCHECK(is_setup_);\n FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread());\n\n task_runners_.GetUITaskRunner()->PostTask(\n [engine = engine_->GetWeakPtr(), message = std::move(message)] {\n if (engine) {\n engine->DispatchPlatformMessage(std::move(message));\n }\n });\n}\n\n// |PlatformView::Delegate|\nvoid Shell::OnPlatformViewDispatchDirectMessage(\n fml::RefPtr message) {\n FML_DCHECK(is_setup_);\n FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread());\n\n engine_->DispatchDirectMessage(std::move(message));\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"有了這個基礎之後,我們就可以在MethodChannel上拓展一個invokeDirectMethod函數,參照系統的實現方式,修改Dart層和Engine層,修改的路線也基本就是MethodChannel的調用流程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"這裏我摘取了_DefaultBinaryMessenger中增加直連的修改,其後續的實現步驟基本上都是跟隨着PlatformChannel的流程,增加直連的函數。"}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"class _DefaultBinaryMessenger extends BinaryMessenger {\n const _DefaultBinaryMessenger._();\n\n // Handlers for incoming messages from platform plugins.\n // This is static so that this class can have a const constructor.\n static final Map _handlers =\n {};\n\n static final Map _directHandlers =\n {}; \n\n // Mock handlers that intercept and respond to outgoing messages.\n // This is static so that this class can have a const constructor.\n static final Map _mockHandlers =\n {};\n\n Future _sendPlatformMessage(String channel, ByteData message) {\n final Completer completer = Completer();\n // ui.window is accessed directly instead of using ServicesBinding.instance.window\n // because this method might be invoked before any binding is initialized.\n // This issue was reported in #27541. It is not ideal to statically access\n // ui.window because the Window may be dependency injected elsewhere with\n // a different instance. However, static access at this location seems to be\n // the least bad option.\n ui.window.sendPlatformMessage(channel, message, (ByteData reply) {\n try {\n completer.complete(reply);\n } catch (exception, stack) {\n FlutterError.reportError(FlutterErrorDetails(\n exception: exception,\n stack: stack,\n library: 'services library',\n context: ErrorDescription('during a platform message response callback'),\n ));\n }\n });\n return completer.future;\n }\n\n ByteData _sendDirectMessage(String channel, ByteData message) {\n return ui.window.sendDirectMessage(channel, message);\n }\n\n\n @override\n Future handlePlatformMessage(\n String channel,\n ByteData data,\n ui.PlatformMessageResponseCallback callback,\n ) async {\n ByteData response;\n try {\n final MessageHandler handler = _handlers[channel];\n if (handler != null) {\n response = await handler(data);\n } else {\n ui.channelBuffers.push(channel, data, callback);\n callback = null;\n }\n } catch (exception, stack) {\n FlutterError.reportError(FlutterErrorDetails(\n exception: exception,\n stack: stack,\n library: 'services library',\n context: ErrorDescription('during a platform message callback'),\n ));\n } finally {\n if (callback != null) {\n callback(response);\n }\n }\n }\n\n\n @override\n void handleDirectMessage(\n String channel,\n ByteData data,\n ui.DirectMessageResultCallback callback,\n ) async {\n ByteData result;\n try {\n final DirectMessageHandler handler = _directHandlers[channel];\n if (handler != null)\n result = handler(data);\n } catch (exception, stack) {\n FlutterError.reportError(FlutterErrorDetails(\n exception: exception,\n stack: stack,\n library: 'services library',\n context: ErrorDescription('during a direct message callback'),\n ));\n } finally {\n callback(result);\n }\n }\n\n\n @override\n Future send(String channel, ByteData message) {\n final MessageHandler handler = _mockHandlers[channel];\n if (handler != null)\n return handler(message);\n return _sendPlatformMessage(channel, message);\n }\n\n @override\n ByteData sendDirect(String channel, ByteData message) {\n return _sendDirectMessage(channel, message);\n }\n\n @override\n void setMessageHandler(String channel, MessageHandler handler) {\n if (handler == null)\n _handlers.remove(channel);\n else\n _handlers[channel] = handler;\n ui.channelBuffers.drain(channel, (ByteData data, ui.PlatformMessageResponseCallback callback) async {\n await handlePlatformMessage(channel, data, callback);\n });\n }\n\n @override\n void setDirectMessageHandler(String channel, DirectMessageHandler handler) {\n if (handler == null)\n _directHandlers.remove(channel);\n else\n _directHandlers[channel] = handler;\n }\n\n ...\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong"}],"text":"4、結束語"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"本⽂闡述了最右JS2Flutter框架的通信機制,基本上都是參照了Flutter的實現⽅式,對於同步直連的實現過程,涉及到的環節比較多,也只闡述了思想,對細節實現感興趣的同學可追溯源碼一探究竟。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":14}},{"type":"strong"}],"text":"5、參考文獻"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[1]:Flutter動態化在最右App中的實踐 "},{"type":"link","attrs":{"href":"https://www.infoq.cn/article/MIa5AN2JE51uor4JeiPG","title":""},"content":[{"type":"text","text":"https://www.infoq.cn/article/MIa5AN2JE51uor4JeiPG"}],"marks":[{"type":"size","attrs":{"size":12}}]},{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[2]:JS2Flutter框架——開篇 "},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/acee65b914dc4d0e32a5561a1","title":null},"content":[{"type":"text","text":"https://xie.infoq.cn/article/acee65b914dc4d0e32a5561a1"}],"marks":[{"type":"size","attrs":{"size":12}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[3]:Flutter渲染機制——UI線程 "},{"type":"link","attrs":{"href":"http://gityuan.com/2019/06/15/flutter_ui_draw/","title":null},"content":[{"type":"text","text":"http://gityuan.com/2019/06/15/flutter_ui_draw/"}],"marks":[{"type":"size","attrs":{"size":12}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"[4]:深入理解Flutter的Platform Channel機制 "},{"type":"link","attrs":{"href":"http://gityuan.com/2019/08/10/flutter_channel/","title":null},"content":[{"type":"text","text":"http://gityuan.com/2019/08/10/flutter_channel/"}],"marks":[{"type":"size","attrs":{"size":12}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章