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

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

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

18年10月份浑测,手機(jī)QQ看點(diǎn)團(tuán)隊(duì)嘗試使用 Flutter募壕,做為iOS開發(fā),一接觸到Flutter就馬上感受到坦报,F(xiàn)lutter 雖然強(qiáng)大盟步,但不能像RN一樣動態(tài)化是阻礙我們使用她的唯一障礙了∷耍看Google團(tuán)隊(duì)對動態(tài)化的計(jì)劃菇存,短期內(nèi)應(yīng)該不會上線,所以擼起袖子自己動手邦蜜,啟動了這個(gè)技術(shù)探索項(xiàng)目依鸥。

簡介

項(xiàng)目代號:MXFlutter (Matrix Flutter)

核心思路是把 Flutter 的渲染邏輯中的三棵樹中的第一棵,放到 JavaScript 中生成悼沈。用 JavaScript 完整實(shí)現(xiàn)了 Flutter 控件層封裝贱迟,可以使用 JavaScript,用極其類似 Dart 的開發(fā)方式絮供,開發(fā)Flutter應(yīng)用衣吠,利用JavaScript版的輕量級Flutter Runtime,生成UI描述杯缺,傳遞給Dart層的UI引擎蒸播,UI引擎把UI描述生產(chǎn)真正的 Flutter 控件。所以在iOS上是完全動態(tài)化的 萍肆,完整代碼在github:

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

繼續(xù)前先瞥一眼整體的架構(gòu),一句話介紹MXFlutter亲铡,就是用JavaScript才写,以Flutter的寫法開發(fā)Flutter。汗...還是有點(diǎn)繞奖蔓,大家看下面貼出來的代碼吧赞草。

image.png

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


image.png

下面是UI截圖對應(yīng)的JS代碼厨疙,沒錯(cuò),你沒有眼花疑务,這個(gè)真的是 JavaScript 代碼沾凄,可以在 MXFlutter 的運(yùn)行時(shí)庫上渲染出 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… ,這是對應(yīng)UI撒蟀,已經(jīng)接近在線上版直接使用了叙谨。

image.png

這個(gè)漂亮的知乎頁面,是用Dart版轉(zhuǎn)JS而來保屯,在此鳴謝作者許吉友 手负,大家可以關(guān)注一下他。

現(xiàn)狀

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

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

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

Dart 本身是描述語言旺入,IDE 的 Outline 工具可以解析 Dart 代碼生成樹形結(jié)構(gòu),我們可以利用其源碼凯力,生成 JSON UI 描述茵瘾,相關(guān)代碼:github.com/flutter/flu… dart-sdk: analysis_server

image.png

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

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

image.png

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

Flutter 動態(tài)化方案二: 動態(tài)運(yùn)行 Dart 語言讹蘑,生產(chǎn)UI描述
和方案一靜態(tài)解析Dart對比末盔,第二個(gè)方案是寫一個(gè)極其輕量的運(yùn)行時(shí)庫,讓編寫UI的Dart 代碼運(yùn)行了起來座慰,生成樹形結(jié)構(gòu)陨舱,再序列化為 JSON(debug),F(xiàn)latBuffers (release)UI 描述版仔∮蚊ぃ可以稱之為動態(tài)解析方案

image.png

具體渲染邏輯
image.png

總體架構(gòu)
image.png

架構(gòu)也有了,方案也有了蛮粮,要Run起來還有幾個(gè)麻煩事要忙活益缎,DartVM 要抽出來,Dart JIT層的輕量級運(yùn)行時(shí)庫然想,Dart AOT層把DSL轉(zhuǎn)成真正Widget的UIEngin也要寫哦莺奔,就是圖中黃色和紅色的三部分
抽離DartVM
無法簡單修改編譯條件抽離
Dart源代碼在進(jìn)行編譯時(shí)會通過DART_PRECOMPILED_RUNTIME宏進(jìn)行條件編譯從而在Debug版編譯JIT模式,Release版編譯AOT模式变泄。并且這兩種模式是互斥的令哟,無法同時(shí)存在。
簡單的解決方法是
我們單獨(dú)編譯出一個(gè)DartVM妨蛹,打包成動態(tài)庫屏富,修改導(dǎo)出符號,避免符合沖突

引入DartVM還需要的工作

開發(fā)DartVM與Native互通接口蛙卤,參考了Flutter狠半,使用Native Extension和Dart_Invoke實(shí)現(xiàn)互相調(diào)用
雙DartVM調(diào)試方案,兩個(gè)DartVM獨(dú)立運(yùn)行颤难,通過遠(yuǎn)程端口單獨(dú)調(diào)試DartFlutter
支持引入第三方庫神年,DartFlutter在打包發(fā)布時(shí)會通過shell腳本分析.packages文件將依賴庫自動打包隨Dart File Zip一起隨包下發(fā)。
常用庫可以預(yù)先打包的App本地乐严,減少下發(fā)文件大小

一個(gè)暫時(shí)無法解決的問題
安裝包過大瘤袖,DartVM增大安裝包30M,如果加上原本的AOT40M昂验,整個(gè)Flutter安裝包會增大到70M,用DartVM不現(xiàn)實(shí)捂敌。怎么辦呢。
0x01 最終方案JavasSriptCore 替換DartVM
可性能分析

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

需要解決的問題
用JS開發(fā)假的Flutter Runtime
封裝JavasSriptCore與Native、 Flutter互調(diào)接口
0x02 講解下MXFlutter的渲染原理
渲染樹
兩個(gè)重要的數(shù)據(jù)結(jié)構(gòu)

MXScriptWidget
MXWidgetTree

MXScriptWidget管理一個(gè)Script頁面或控件甫恩,負(fù)責(zé)創(chuàng)建管理 ScriptWidgetTree逆济,以自增ID與Flutter對應(yīng)Widget相互調(diào)用
,每次Build都會創(chuàng)建一個(gè)新的MXWidgetTree

image.png

MXFlutter 事件
在 JS 側(cè) buildWidget 時(shí),我們會對 function 事件奖慌,生成自增的唯一 callbackID抛虫,并與 widgetID 組合拼接成 widgetID/callbackID,作為事件的唯一標(biāo)識简僧。用戶點(diǎn)擊界面某個(gè) button 時(shí)建椰,事件由 Flutter 側(cè)傳到 JS 側(cè),通過解析 widgetID/callbackID岛马,找到對應(yīng) widget 的 callback棉姐,完成事件處理。

image.png

MXFlutter 高效的動態(tài)列表
通過在 JS 側(cè)啦逆,ListView 調(diào)用 Build 方法時(shí)伞矩,提前展開 child, 并為 ListView 增加 children 成員變量。此時(shí)夏志,因?yàn)閮H有數(shù)據(jù)配置乃坤,不會有多余的 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 側(cè),ListView 仍然是動態(tài)創(chuàng)建溉贿,滑動列表,MXFlutter Engine 根據(jù) Children 數(shù)組里的配置數(shù)據(jù),創(chuàng)建真正的 Flutter WidgetCell浦旱,效率與原生相同完全一致宇色。ListView.builder(
itemCount: children.length,
itemBuilder: (context, index) {
return UIEngine.toWidget(children[index]);
},
)


image.png

MXFlutter 動畫的方案

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

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

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

MXScriptWidget 是一個(gè)具備Build WidgetTree劫樟,緩存Callback映射表,動畫支持的基本單位〉蓿可以作為普通FlutterWidget來使用奶陈。
在Flutter層,如果Widget樹中節(jié)點(diǎn)有MXScriptWidget附较,則在對應(yīng)節(jié)點(diǎn)上創(chuàng)建MXFlutterWidget自定義控件
兩個(gè)子樹可以相互對應(yīng)獲得局部刷新吃粒,callback回調(diào),動畫支持翅睛,Rebuild時(shí)所生產(chǎn)的UI DSL 大大減少声搁,加快刷新速率

image.png

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

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

內(nèi)存-跨層鏡像對象的生命周期
VM層扎酷,F(xiàn)lutter層檐涝,Native層鏡像對象的生命周期如何控制?
參考蘋果 iOS JavaScriptCore 和 Objective-C的解決方法

以Flutter層的對象生命周期為主
在VM層增加WeakMap支持法挨,不增加對象引用計(jì)數(shù)谁榜,F(xiàn)lutter層釋放之后,釋放VM層對象
在Native層使用 JSManagerValue凡纳,VM層對象釋放后窃植,Native的引用被自動置空

image.png

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

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

Flutter UI層和MXScript層是異步調(diào)用巷怜,限制動態(tài)控件的架構(gòu)設(shè)計(jì)
image.png

一個(gè)可行方案

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

image.png

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

讓開發(fā)者寫出優(yōu)雅的代碼,咳咳沼撕,這里有點(diǎn)吹了宋雏,總之,我們想讓使用MXFlutter的開發(fā)同學(xué)寫出來的代碼看來正規(guī)一些端朵,好看一些好芭。

  • 完美支持Dart Flutter語法
  • 定義所有Flutter 中同名Widget類,構(gòu)建Widget的參數(shù)類冲呢,支持相同的Build方式舍败,SetState觸發(fā)刷新,事件響應(yīng)函數(shù)
  • Callback函數(shù)自動生成CallbackID
  • Callback函數(shù)自動This綁定
  • ListView 像Dart層一樣開發(fā),支持itemBuilder回調(diào)函數(shù)

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

0x05 MXFlutter 基礎(chǔ)建設(shè)
因?yàn)?JavaScript 不支持模塊化開發(fā)邻薯,不能引用其他文件代碼裙戏,我們參照 RN,使用 Node.js 的模塊化代碼厕诡,在Native 層支持 require 語法累榜。開發(fā)時(shí),IDE最好選用 VSCode灵嫌,因?yàn)榭梢园囱bJS插件壹罚,直接運(yùn)行調(diào)試JS
另外,我們通過重定向模擬器 JS 路徑文件到開發(fā)機(jī)寿羞,用戶修改完 JS 文件猖凛,便可直可接看到相應(yīng)修改,實(shí)現(xiàn)模擬器的頁面熱更新绪穆。

最后小編這呢辨泳,給大家推薦一個(gè)優(yōu)秀的iOS交流平臺,平臺里的伙伴們都是非常優(yōu)秀的iOS開發(fā)人員玖院,我們專注于技術(shù)的分享與技巧的交流菠红,大家可以在平臺上討論技術(shù),交流學(xué)習(xí)难菌。歡迎大家的加入(想要進(jìn)入的可加小編QQ3140276761)试溯。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市郊酒,隨后出現(xiàn)的幾起案子耍共,更是在濱河造成了極大的恐慌,老刑警劉巖猎塞,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異杠纵,居然都是意外死亡荠耽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門比藻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铝量,“玉大人,你說我怎么就攤上這事银亲÷叮” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵务蝠,是天一觀的道長拍谐。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么轩拨? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任践瓷,我火速辦了婚禮,結(jié)果婚禮上亡蓉,老公的妹妹穿的比我還像新娘晕翠。我一直安慰自己,他們只是感情好砍濒,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布淋肾。 她就那樣靜靜地躺著,像睡著了一般爸邢。 火紅的嫁衣襯著肌膚如雪樊卓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天甲棍,我揣著相機(jī)與錄音简识,去河邊找鬼。 笑死感猛,一個(gè)胖子當(dāng)著我的面吹牛七扰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播陪白,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼颈走,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了咱士?” 一聲冷哼從身側(cè)響起立由,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎序厉,沒想到半個(gè)月后锐膜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弛房,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年道盏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片文捶。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡荷逞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出粹排,到底是詐尸還是另有隱情种远,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布顽耳,位于F島的核電站坠敷,受9級特大地震影響妙同,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜常拓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一渐溶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧弄抬,春花似錦茎辐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至懊亡,卻和暖如春依啰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背店枣。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工速警, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鸯两。 一個(gè)月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓闷旧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親钧唐。 傳聞我的和親對象是個(gè)殘疾皇子忙灼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容