基于JS的高性能Flutter動態(tài)化框架MXFlutter

基于JS的高性能Flutter動態(tài)化框架

可能是目前放出來的相對最完整的Flutter動態(tài)化方案

18年10月份,手機QQ看點團隊嘗試使用 Flutter,做為iOS開發(fā),一接觸到Flutter就馬上感受到,Flutter 雖然強大,但不能像RN一樣動態(tài)化是阻礙我們使用她的唯一障礙了??碐oogle團隊對動態(tài)化的計劃,短期內應該不會上線,所以擼起袖子自己動手,啟動了這個技術探索項目。

簡介

項目代號:MXFlutter (Matrix Flutter)

核心思路是把 Flutter 的渲染邏輯中的三棵樹中的第一棵,放到 JavaScript 中生成。用 JavaScript 完整實現了 Flutter 控件層封裝,可以使用 JavaScript,用極其類似 Dart 的開發(fā)方式,開發(fā)Flutter應用,利用JavaScript版的輕量級Flutter Runtime,生成UI描述,傳遞給Dart層的UI引擎,UI引擎把UI描述生產真正的 Flutter 控件。所以在iOS上是完全動態(tài)化的 ,完整代碼在github:

如果能幫助到大家,請給MXFlutter點個Star,給我們有動力繼續(xù)更新下去^_*,也讓整個Flutter社區(qū)都能了解到我們中國開發(fā)者的貢獻。github TGIF-iMatrix MXFlutter

繼續(xù)前先瞥一眼整體的架構,一句話介紹MXFlutter,就是用JavaScript,以Flutter的寫法開發(fā)Flutter。汗...還是有點繞,大家看下面貼出來的代碼吧。

image.png

效果
以下截圖是在MXFlutter框架下用JS開發(fā),大家可以把上面的源碼下載下來,里面有完整的JS代碼示例:


image.png

下面是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版 github.com/TGIF-iMatri… ,這是對應UI,已經接近在線上版直接使用了。

image.png

這個漂亮的知乎頁面,是用Dart版轉JS而來,在此鳴謝作者許吉友 ,大家可以關注一下他。

現狀

MXFlutter雖然各個模塊已相對完整,但投入生產還需要解決其中的BUG,由于19年初,小組啟動新項目,非常繁忙,幾乎沒有時間繼續(xù)開發(fā),從3月份一直暫停,目前人力仍然很緊張,如果大家有興趣,期待小伙伴們一起加入,共同豐富 MXFlutter 動態(tài)化能力。

0x00 分享下動態(tài)化探索過程中的幾個炮灰方案

Flutter 動態(tài)化方案一: 靜態(tài)解析Dart語言,生成UI描述

Dart 本身是描述語言,IDE 的 Outline 工具可以解析 Dart 代碼生成樹形結構,我們可以利用其源碼,生成 JSON UI 描述,相關代碼:github.com/flutter/flu… dart-sdk: analysis_server

image.png

靜態(tài)解析 Dart 缺點,不能寫邏輯,對編寫UI代碼有很多限制,不能寫判斷語句,不能寫函數,要支持這些成本很高。所以只好放棄。
快速介紹下Flutter的核心渲染模塊三棵樹
響應式UI框架

WidgetTree:Widget 里面存儲了一個視圖的配置信息,可以高效的創(chuàng)建(build)和銷毀
Element 是分離 WidgetTree 和真正的渲染對象的中間層, WidgetTree 用來描述對應的Element 屬性
RenderObject 來執(zhí)行 Diff, Hit Test 布局、繪制

image.png

第一棵樹有完整的UI描述信息,那么我只要JIT下通過 DartVM 創(chuàng)建第一棵樹,其他耗時的操作都丟到AOT里去。
image.png

Flutter 動態(tài)化方案二: 動態(tài)運行 Dart 語言,生產UI描述
和方案一靜態(tài)解析Dart對比,第二個方案是寫一個極其輕量的運行時庫,讓編寫UI的Dart 代碼運行了起來,生成樹形結構,再序列化為 JSON(debug),FlatBuffers (release)UI 描述??梢苑Q之為動態(tài)解析方案

image.png

具體渲染邏輯
image.png

總體架構
image.png

架構也有了,方案也有了,要Run起來還有幾個麻煩事要忙活,DartVM 要抽出來,Dart JIT層的輕量級運行時庫,Dart AOT層把DSL轉成真正Widget的UIEngin也要寫哦,就是圖中黃色和紅色的三部分
抽離DartVM
無法簡單修改編譯條件抽離
Dart源代碼在進行編譯時會通過DART_PRECOMPILED_RUNTIME宏進行條件編譯從而在Debug版編譯JIT模式,Release版編譯AOT模式。并且這兩種模式是互斥的,無法同時存在。
簡單的解決方法是
我們單獨編譯出一個DartVM,打包成動態(tài)庫,修改導出符號,避免符合沖突

引入DartVM還需要的工作

開發(fā)DartVM與Native互通接口,參考了Flutter,使用Native Extension和Dart_Invoke實現互相調用
雙DartVM調試方案,兩個DartVM獨立運行,通過遠程端口單獨調試DartFlutter
支持引入第三方庫,DartFlutter在打包發(fā)布時會通過shell腳本分析.packages文件將依賴庫自動打包隨Dart File Zip一起隨包下發(fā)。
常用庫可以預先打包的App本地,減少下發(fā)文件大小

一個暫時無法解決的問題
安裝包過大,DartVM增大安裝包30M,如果加上原本的AOT40M,整個Flutter安裝包會增大到70M,用DartVM不現實。怎么辦呢。
0x01 最終方案JavasSriptCore 替換DartVM
可性能分析

JavasSriptCore 是iOS官方庫,不增加安裝包
Dart代碼和JS代碼非常相近,可以用工具轉換
JavasSriptCore 與 Native有更方便的互調接口
ReactNative 已驗證通過JS開發(fā)App能力是可行的
JS的執(zhí)行效率是DartVM的3倍編碼1M的JSON只需 2毫秒

需要解決的問題
用JS開發(fā)假的Flutter Runtime
封裝JavasSriptCore與Native、 Flutter互調接口
0x02 講解下MXFlutter的渲染原理
渲染樹
兩個重要的數據結構

MXScriptWidget
MXWidgetTree

MXScriptWidget管理一個Script頁面或控件,負責創(chuàng)建管理 ScriptWidgetTree,以自增ID與Flutter對應Widget相互調用
,每次Build都會創(chuàng)建一個新的MXWidgetTree

image.png

MXFlutter 事件
在 JS 側 buildWidget 時,我們會對 function 事件,生成自增的唯一 callbackID,并與 widgetID 組合拼接成 widgetID/callbackID,作為事件的唯一標識。用戶點擊界面某個 button 時,事件由 Flutter 側傳到 JS 側,通過解析 widgetID/callbackID,找到對應 widget 的 callback,完成事件處理。

image.png

MXFlutter 高效的動態(tài)列表
通過在 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 仍然是動態(tài)創(chuàng)建,滑動列表,MXFlutter Engine 根據 Children 數組里的配置數據,創(chuàng)建真正的 Flutter WidgetCell,效率與原生相同完全一致。ListView.builder(
itemCount: children.length,
itemBuilder: (context, index) {
return UIEngine.toWidget(children[index]);
},
)


image.png

MXFlutter 動畫的方案

動畫參數在VM層配置一次,動畫開始后在Flutter層閉環(huán)循環(huán)rebuild,形成動畫效果,這個是比較通用的做法了。
image.png

0x03 渲染優(yōu)化
不管JSWidget創(chuàng)建有多快,總是有跨語言執(zhí)行,所以減少Build次數和減小Build出來的DSL UI描述大小,可以優(yōu)化性能。
渲染優(yōu)化1-局部刷新:配置樹Diff
一個事實
自動對比兩次Widget 無論如何都沒有直接創(chuàng)建一個新的快,如果開發(fā)者不參與,由框架來自動計算Diff是得不償失的
可行的方法
犧牲響應式UI框架的設計模式
采用和Native、Web的方式,由開發(fā)者參與自己設置Diff的節(jié)點,即根據ID獲取對應Widget,修改Widget參數,Rebuild生成新DSL

渲染優(yōu)化2-局部刷新-嵌套節(jié)點

MXScriptWidget 是一個具備Build WidgetTree,緩存Callback映射表,動畫支持的基本單位??梢宰鳛槠胀‵lutterWidget來使用。
在Flutter層,如果Widget樹中節(jié)點有MXScriptWidget,則在對應節(jié)點上創(chuàng)建MXFlutterWidget自定義控件
兩個子樹可以相互對應獲得局部刷新,callback回調,動畫支持,Rebuild時所生產的UI DSL 大大減少,加快刷新速率

image.png

渲染優(yōu)化3-可以分離動態(tài)和靜態(tài)控件

MXStatelessWidget 可以通過使用無狀態(tài)的ScriptWidget來向框架標示,其下面的子樹,在每次build中不會變化,其build結果會被緩存,下次在Flutter層直接復用
image.png

內存-跨層鏡像對象的生命周期
VM層,Flutter層,Native層鏡像對象的生命周期如何控制?
參考蘋果 iOS JavaScriptCore 和 Objective-C的解決方法

以Flutter層的對象生命周期為主
在VM層增加WeakMap支持,不增加對象引用計數,Flutter層釋放之后,釋放VM層對象
在Native層使用 JSManagerValue,VM層對象釋放后,Native的引用被自動置空

image.png

線程問題
參照業(yè)界RN等框架的設計,VM層跑在一個單獨的后臺線程

從Flutter層通過Native通道調用到VM,發(fā)生兩次線程切換

Flutter UI層和MXScript層是異步調用,限制動態(tài)控件的架構設計
image.png

一個可行方案

修改FlutterEngine ,定制開發(fā)Dart->Native->VM 這個通道,調用到VM不切換線程
VM不新建線程,直接由Flutter UI Thread 消息循環(huán)驅動,這樣也同時支持了和Flutter UI 層的高效同步調用,但要注意從Native調用到VM,需要通過定制FlutterEngine的接口。

image.png

0x04 讓開發(fā)者寫出優(yōu)雅的代碼

讓開發(fā)者寫出優(yōu)雅的代碼,咳咳,這里有點吹了,總之,我們想讓使用MXFlutter的開發(fā)同學寫出來的代碼看來正規(guī)一些,好看一些。

  • 完美支持Dart Flutter語法
  • 定義所有Flutter 中同名Widget類,構建Widget的參數類,支持相同的Build方式,SetState觸發(fā)刷新,事件響應函數
  • Callback函數自動生成CallbackID
  • Callback函數自動This綁定
  • ListView 像Dart層一樣開發(fā),支持itemBuilder回調函數

參考JS示例源碼 TGIF-iMatrix home_page.js

0x05 MXFlutter 基礎建設
因為 JavaScript 不支持模塊化開發(fā),不能引用其他文件代碼,我們參照 RN,使用 Node.js 的模塊化代碼,在Native 層支持 require 語法。開發(fā)時,IDE最好選用 VSCode,因為可以按裝JS插件,直接運行調試JS
另外,我們通過重定向模擬器 JS 路徑文件到開發(fā)機,用戶修改完 JS 文件,便可直可接看到相應修改,實現模擬器的頁面熱更新。

最后小編這呢,給大家推薦一個優(yōu)秀的iOS交流平臺,平臺里的伙伴們都是非常優(yōu)秀的iOS開發(fā)人員,我們專注于技術的分享與技巧的交流,大家可以在平臺上討論技術,交流學習。歡迎大家的加入(想要進入的可加小編QQ3140276761)。

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容