基於JS的高效能Flutter動態化框架MXFlutter
導語:18年10月份,手機QQ看點團隊嘗試使用 Flutter,做為iOS開發,一接觸到Flutter就馬上感受到,Flutter 雖然強大,但不能像RN一樣動態化是阻礙我們使用她的唯一障礙了。看Google團隊對動態化的計劃,短期內應該不會上線,所以自己動手,啟動了這個技術探索專案。
基於JS的高效能Flutter動態化框架
可能是目前放出來的相對最完整的Flutter動態化方案
簡介
專案代號:MXFlutter (Matrix Flutter)
核心思路是把 Flutter 的渲染邏輯中的三棵樹中的第一棵,放到 JavaScript 中生成。用 JavaScript 完整實現了 Flutter 控制元件層封裝,可以使用 JavaScript,用極其類似 Dart 的開發方式,開發Flutter應用,利用JavaScript版的輕量級Flutter Runtime,生成UI描述,傳遞給Dart層的UI引擎,UI引擎把UI描述生產真正的 Flutter 控制元件。所以在iOS上是完全動態化的 ,完整程式碼在github,如果能幫助到大家,請給MXFlutter點個Star,給我們動力繼續更新下去^_*,github TGIF-iMatrix MXFlutter
繼續前先瞥一眼整體的架構,一句話介紹MXFlutter,就是用JavaScript,以Flutter的寫法開發Flutter。汗…還是有點繞,大家看下面貼出來的程式碼吧。
效果
以下截圖是在MXFlutter框架下用JS開發,大家可以把上面的原始碼下載下來,裡面有完整的JS程式碼示例:
這個是APP示例截圖
下面是UI截圖對應的JS程式碼,沒錯,你沒有眼花,這個是真的 JavaScript 程式碼,可以在 MXFlutter 的執行時庫上渲染出 Flutter 的UI
class JSPestoPage extends MXJSWidget { constructor() { super("JSPestoPage"); this.recipes = recipeList; } build(context) { let statusBarHeight = 24; let mq = MediaQuery.of(context); if (mq) { statusBarHeight = mq.padding.top } let w = new Scaffold({ appBar: new AppBar({ title: new Text("Pesto Demo") }), floatingActionButton: new FloatingActionButton({ child: new Icon(new IconData(0xe3c9)), onPressed: this.createCallbackID(function () { }), }), body: new CustomScrollView({ semanticChildCount: this.recipes.length, slivers: [ //this.buildAppBar(context, statusBarHeight), this.buildBody(context, statusBarHeight), ], }), //body:this.buildItems()[0] }); return w; } buildAppBar(context, statusBarHeight) { return SliverAppBar({ pinned: true, expandedHeight: _kAppBarHeight, actions: [ IconButton({ icon: new Icon(new IconData(1)), tooltip: 'Search', onPressed: this.createCallbackID(function () { }), }), ], flexibleSpace: LayoutBuilder({ builder: function (context, constraints) { size = constraints.biggest; appBarHeight = size.height - statusBarHeight; t = (appBarHeight - kToolbarHeight) / (_kAppBarHeight - kToolbarHeight); extraPadding = new Tween({ begin: 10.0, end: 24.0 }).transform(t); logoHeight = appBarHeight - 1.5 * extraPadding; return Padding({ padding: EdgeInsets.only({ top: statusBarHeight + 0.5 * extraPadding, bottom: extraPadding, }), child: Center({ child: new Icon(new IconData(1)) }), }); }, }), }); } buildBody(context, statusBarHeight) { let mediaPadding = EdgeInsets.all(0); let mq = MediaQuery.of(context); if (mq) { mediaPadding = MediaQuery.of(context).padding; } let padding = EdgeInsets.only({ top: 8.0, left: 8.0 + mediaPadding.left, right: 8.0 + mediaPadding.right, bottom: 8.0 }); return new SliverPadding({ padding: padding, sliver: new SliverGrid({ gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent({ maxCrossAxisExtent: _kRecipePageMaxWidth, crossAxisSpacing: 8.0, mainAxisSpacing: 8.0, }), delegate: new SliverChildBuilderDelegate( function (context, index) { let recipe = this.recipes[index]; let w = new RecipeCard({ recipe: recipe, onTap: function () { showRecipePage(context, recipe); }, }); return w; }, { childCount: this.recipes.length, }), }), }); }
原始碼中還有更豐滿的示例,高仿知乎頁面JSFlutter版
。
這是對應UI,已經接近線上上版直接使用了。
這個漂亮的知乎頁面,是用Dart版轉JS而來,在此鳴謝作者許吉友 ,大家可以關注一下他。
現狀
MXFlutter雖然各個模組已相對完整,但投入生產還需要解決其中的BUG,由於19年初,小組啟動新專案,非常繁忙,幾乎沒有時間繼續開發,從3月份一直暫停,目前人力仍然很緊張,如果大家有興趣,期待小夥伴們一起加入,共同豐富 MXFlutter 動態化能力。
0x00 分享下動態化探索過程中的幾個炮灰方案
Flutter 動態化方案一:靜態解析Dart語言,生成UI描述
Dart 本身是描述語言,IDE 的 Outline 工具可以解析 Dart 程式碼生成樹形結構,我們可以利用其原始碼,生成 JSON UI 描述,相關程式碼:
dart-sdk: analysis_server
靜態解析 Dart 缺點,不能寫邏輯,對編寫UI程式碼有很多限制,不能寫判斷語句,不能寫函式,要支援這些成本很高。所以只好放棄。
快速介紹下Flutter的核心渲染模組三棵樹
響應式UI框架
WidgetTree:Widget 裡面儲存了一個檢視的配置資訊,可以高效的建立(build)和銷燬
Element 是分離 WidgetTree 和真正的渲染物件的中間層, WidgetTree 用來描述對應的Element 屬性
RenderObject 來執行 Diff, Hit Test 佈局、繪製
第一棵樹有完整的UI描述資訊,那麼我只要JIT下透過 DartVM 建立第一棵樹,其他耗時的操作都丟到AOT裡去。
Flutter 動態化方案二:動態執行 Dart 語言,生產UI描述
和方案一靜態解析Dart對比,第二個方案是寫一個極其輕量的執行時庫,讓編寫UI的Dart 程式碼執行了起來,生成樹形結構,再序列化為 JSON(debug),FlatBuffers (release)UI 描述。可以稱之為動態解析方案
具體渲染邏輯
總體架構
架構也有了,方案也有了,要Run起來還有幾個麻煩事要忙活,DartVM 要抽出來,Dart JIT層的輕量級執行時庫,Dart AOT層把DSL轉成真正Widget的UIEngine也要寫哦,就是圖中黃色和紅色的三部分
抽離DartVM
無法簡單修改編譯條件抽離
Dart原始碼在進行編譯時會透過DART_PRECOMPILED_RUNTIME宏進行條件編譯從而在Debug版編譯JIT模式,Release版編譯AOT模式。並且這兩種模式是互斥的,無法同時存在。
簡單的解決方法是
我們單獨編譯出一個DartVM,打包成動態庫,修改匯出符號,避免符合衝突
引入DartVM還需要的工作
開發DartVM與Native互通介面,參考了Flutter,使用Native Extension和Dart_Invoke實現互相呼叫
雙DartVM除錯方案,兩個DartVM獨立執行,透過遠端埠單獨除錯DartFlutter
支援引入第三方庫,DartFlutter在打包釋出時會透過shell指令碼分析.packages檔案將依賴庫自動打包隨Dart File Zip一起隨包下發。
常用庫可以預先打包的App本地,減少下發檔案大小
一個暫時無法解決的問題
安裝包過大,DartVM增大安裝包30M,如果加上原本的AOT40M,整個Flutter安裝包會增大到70M,用DartVM不現實。怎麼辦呢。
0x01 最終方案JavasSriptCore 替換DartVM
可效能分析
JavasSriptCore 是iOS官方庫,不增加安裝包
Dart程式碼和JS程式碼非常相近,可以用工具轉換
JavasSriptCore 與 Native有更方便的互調介面
ReactNative 已驗證透過JS開發App能力是可行的
JS的執行效率是DartVM的3倍編碼1M的JSON只需 2毫秒
需要解決的問題
用JS開發假的Flutter Runtime
封裝JavasSriptCore與Native、 Flutter互調介面
0x02 講解下MXFlutter的渲染原理
渲染樹
兩個重要的資料結構
MXScriptWidget
MXWidgetTree
MXScriptWidget管理一個Script頁面或控制元件,負責建立管理 ScriptWidgetTree,以自增ID與Flutter對應Widget相互呼叫,每次Build都會建立一個新的MXWidgetTree
MXFlutter 事件
在 JS 側 buildWidget 時,我們會對 function 事件,生成自增的唯一 callbackID,並與 widgetID 組合拼接成 widgetID/callbackID,作為事件的唯一標識。使用者點選介面某個 button 時,事件由 Flutter 側傳到 JS 側,透過解析 widgetID/callbackID,找到對應 widget 的 callback,完成事件處理。
MXFlutter 高效的動態列表
透過在 JS 側,ListView 呼叫 Build 方法時,提前展開 child, 併為 ListView 增加 children 成員變數。此時,因為僅有資料配置,不會有多餘的 Layout 過程,所以速度是非常快的。
preBuild(jsWidget, buildContext) { if(this.builder) { for (let i = 0; i < this.childCount; ++i) { let w = this.builder(buildContext, i); this.children.push(w); } delete this.builder; } super.preBuild(jsWidget, buildContext); }
在 Flutter 側,ListView 仍然是動態建立,滑動列表,MXFlutter Engine 根據 Children 陣列裡的配置資料,建立真正的 Flutter WidgetCell,效率與原生相同完全一致。
ListView.builder( itemCount: children.length, itemBuilder: (context, index) { return UIEngine.toWidget(children[index]); }, )
MXFlutter 動畫的方案
動畫引數在VM層配置一次,動畫開始後在Flutter層閉環迴圈rebuild,形成動畫效果,這個是比較通用的做法了。
0x03 渲染最佳化
不管JSWidget建立有多快,總是有跨語言執行,所以減少Build次數和減小Build出來的DSL UI描述大小,可以最佳化效能。
渲染最佳化1-區域性重新整理:配置樹Diff
一個事實
自動對比兩次Widget 無論如何都沒有直接建立一個新的快,如果開發者不參與,由框架來自動計算Diff是得不償失的
可行的方法
犧牲響應式UI框架的設計模式
採用和Native、Web的方式,由開發者參與自己設定Diff的節點,即根據ID獲取對應Widget,修改Widget引數,Rebuild生成新DSL
渲染最佳化2-區域性重新整理-巢狀節點
MXScriptWidget 是一個具備Build WidgetTree,快取Callback對映表,動畫支援的基本單位。可以作為普通FlutterWidget來使用。
在Flutter層,如果Widget樹中節點有MXScriptWidget,則在對應節點上建立MXFlutterWidget自定義控制元件
兩個子樹可以相互對應獲得區域性重新整理,callback回撥,動畫支援,Rebuild時所生產的UI DSL 大大減少,加快重新整理速率
渲染最佳化3-可以分離動態和靜態控制元件
MXStatelessWidget 可以透過使用無狀態的ScriptWidget來向框架標示,其下面的子樹,在每次build中不會變化,其build結果會被快取,下次在Flutter層直接複用
記憶體-跨層映象物件的生命週期
VM層,Flutter層,Native層映象物件的生命週期如何控制?
參考蘋果 iOS JavaScriptCore 和 Objective-C的解決方法
以Flutter層的物件生命週期為主
在VM層增加WeakMap支援,不增加物件引用計數,Flutter層釋放之後,釋放VM層物件
在Native層使用 JSManagerValue,VM層物件釋放後,Native的引用被自動置空
執行緒問題
參照業界RN等框架的設計,VM層跑在一個單獨的後臺執行緒
從Flutter層透過Native通道呼叫到VM,發生兩次執行緒切換
Flutter UI層和MXScript層是非同步呼叫,限制動態控制元件的架構設計
一個可行方案
修改FlutterEngine ,定製開發Dart->Native->VM 這個通道,呼叫到VM不切換執行緒
VM不新建執行緒,直接由Flutter UI Thread 訊息迴圈驅動,這樣也同時支援了和Flutter UI 層的高效同步呼叫,但要注意從Native呼叫到VM,需要透過定製FlutterEngine的介面。
0x04 讓開發者寫出優雅的程式碼
讓開發者寫出優雅的程式碼,咳咳,這裡有點吹了,總之,我們想讓使用MXFlutter的開發同學寫出來的程式碼看來正規一些,好看一些。
完美支援Dart Flutter語法
定義所有Flutter 中同名Widget類,構建Widget的引數類,支援相同的Build方式,SetState觸發重新整理,事件響應函式
Callback函式自動生成CallbackID
Callback函式自動This繫結
ListView 像Dart層一樣開發,支援itemBuilder回撥函式
參考JS示例原始碼
TGIF-iMatrix home_page.js
0x05 MXFlutter 基礎建設
因為 JavaScript 不支援模組化開發,不能引用其他檔案程式碼,我們參照 RN,使用 Node.js 的模組化程式碼,在Native 層支援 require 語法。開發時,IDE最好選用 VSCode,因為可以按裝JS外掛,直接執行除錯JS
另外,我們透過重定向模擬器 JS 路徑檔案到開發機,使用者修改完 JS 檔案,便可直接看到相應修改,實現模擬器的頁面熱更新。
結語
由於時間緊張,MXFlutter還有很多遺留的問題,作為一個技術探索,非常辛苦但非常有趣,期待各位大牛指導,期待小夥伴們提出問題一起討論解決。
要了解全部,一定要拉下原始碼,執行起來看看,有問題可以留言一討論,MXFlutter會持續更新。
其他專案成員有luca浪哥,nice,yockie帥哥貢獻了動畫,控制元件,示例APP等核心實現, chaodong老師負責了DartVM方案,IP老師幫忙提供了單元測試,健身大神yofer老師負責了程式碼維護,工具建設。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559354/viewspace-2651796/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- MXFlutter:基於JS的Flutter框架,用JS也能寫出Flutter應用FlutterJS框架
- Flutter動態化框架ThreshFlutter框架
- 基於Oracle的高效能動態SQL程式開發OracleSQL
- Flutter 基於Bloc框架的封裝FlutterBloC框架封裝
- 基於Oracle的高效能動態SQL程式開發(轉)OracleSQL
- Flutter狀態管理Provider(三)基於Provider的程式碼框架FlutterIDE框架
- Sky:一個基於 Dart 的高效能移動應用 UI 框架DartUI框架
- swift - 基於TCL的自動化測試框架Swift框架
- 基於 Riverpod 的 Flutter 狀態管理Flutter
- Flutter 動態化方案探索Flutter
- 基於weex的考拉移動端動態化方案
- 基於Selenium + Python的web自動化框架PythonWeb框架
- Flutter動態化-Android(一)FlutterAndroid
- .NET 下基於動態代理的 AOP 框架實現揭祕框架
- Light-PHP 基於 swoole 的高效能 PHP 框架PHP框架
- GRPC——高效能基於HTTP/2的RPC框架RPCHTTP框架
- JkdYaf - 基於 YAF + SWOOLE 高效能API框架API框架
- Android Virtualview:淘寶、天貓 又一個動態化、高效能的UI框架力作AndroidViewUI框架
- 一種基於 cypress 的 UI 自動化測試框架UI框架
- ShutIt:一個基於 Python 的 shell 自動化框架Python框架
- 基於 Koa.js 的 Node.js MVC 框架Node.jsMVC框架
- ThinkJS基於Promise的Node.js MVC框架PromiseNode.jsMVC框架
- 基於NodeJS的14款Web框架NodeJSWeb框架
- 基於註解的6.0許可權動態請求框架——JPermission框架
- 基於動態圖互動網路的多意圖口語語言理解框架框架
- 基於動態規劃的強化學習演算法動態規劃強化學習演算法
- Flutter 動態化熱更新的思考與實踐Flutter
- 基於 D3.js 繪製動態進度條JS
- 美團外賣Flutter動態化實踐Flutter
- Bedrock——基於MVVM+Provider的Flutter快速開發框架MVVMIDEFlutter框架
- 基於Selenium+Python的web自動化測試框架PythonWeb框架
- 基於Python3.7 Robot Framework自動化框架搭建PythonFramework框架
- 基於AI的移動端自動化測試框架的設計與實踐AI框架
- 基於JDK的動態代理原理分析JDK
- 基於Promise的Node.js MVC框架——ThinkJS 1.0釋出PromiseNode.jsMVC框架
- 醫動力Android基於CC元件化框架的探索與實踐Android元件化框架
- 基於事件驅動的測試框架ETS事件框架
- 基於Python+requests搭建的自動化框架-實現流程化的介面串聯Python框架