真當Flutter不能熱更新?QQ團隊開源動態化Flutter

基於JS的高性能Flutter動態化框架

多是目前放出來的相對最完整的Flutter動態化方案
跨平臺新明星Flutter推出後,咱們團隊也嘗試引入 Flutter ,作爲iOS開發,立刻感覺到,Flutter 雖然強大,但不能像RN同樣動態化是阻礙咱們使用她的惟一障礙了。看Google團隊對動態化的計劃,短時間內應該不會上線,因此本身動手,啓動了這個技術探索項目。git

簡介

項目代號:MXFlutter (Matrix Flutter)核心思路是把 Flutter 的渲染邏輯中的三棵樹中的第一棵,放到 JavaScript 中生成。用 JavaScript 完整實現了 Flutter 控件層封裝,可使用 JavaScript,用極其相似 Dart 的開發方式,開發Flutter應用,利用JavaScript版的輕量級Flutter Runtime,生成UI描述,傳遞給Dart層的UI引擎,UI引擎把UI描述生產真正的 Flutter 控件。因此在iOS上是徹底動態化的 ,預覽版的完整代碼在github:若是能幫助到你們,請給MXFlutter點個Star,給咱們有動力繼續更新下去^_*,也讓整個Flutter社區都能瞭解到咱們中國開發者的貢獻。github TGIF-iMatrix MXFlutter繼續前先瞥一眼總體的架構,一句話介紹MXFlutter,就是用JavaScript,以Flutter的寫法開發Flutter。汗…仍是有點繞,你們看下面貼出來的代碼吧。github

效果

如下截圖是在MXFlutter框架下用JS開發,你們能夠把上面的源碼下載下來,裏面有完整的JS代碼示例:web

下面是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,已經接近在線上版直接使用了。shell

這個漂亮的知乎頁面,是用Dart版轉JS而來,在此鳴謝做者許吉友 ,你們能夠關注一下他。設計模式

現狀

MXFlutter雖然各個模塊已相對完整,但投入生產還須要解決其中的BUG,因爲19年初,小組啓動新項目,很是繁忙,幾乎沒有時間繼續開發,從3月份一直暫停,目前人力仍然很緊張,因此先放出技術方案和預覽版代碼,若是你們有興趣,期待小夥伴們一塊兒加入,共同豐富 MXFlutter 動態化能力。數組

0x00 分享下動態化探索過程當中的幾個炮灰方案

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

Dart 自己是描述語言,IDE 的 Outline 工具能夠解析 Dart 代碼生成樹形結構,咱們能夠利用其源碼,生成 JSON UI 描述,相關代碼:github.com/flutter/flu… dart-sdk: analysis_server緩存

靜態解析 Dart 缺點,不能寫邏輯,對編寫UI代碼有不少限制,不能寫判斷語句,不能寫函數,要支持這些成本很高。因此只好放棄。數據結構

快速介紹下Flutter的核心渲染模塊三棵樹

響應式UI框架架構

  1. WidgetTree:Widget 裏面存儲了一個視圖的配置信息,能夠高效的建立(build)和銷燬
  2. Element 是分離 WidgetTree 和真正的渲染對象的中間層, WidgetTree 用來描述對應的Element 屬性
  3. 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的UIEngin也要寫哦,就是圖中黃色和紅色的三部分

抽離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

可性能分析

  1. JavasSriptCore 是iOS官方庫,不增長安裝包
  2. Dart代碼和JS代碼很是相近,能夠用工具轉換
  3. JavasSriptCore 與 Native有更方便的互調接口
  4. ReactNative 已驗證經過JS開發App能力是可行的
  5. 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層直接複用image

內存-跨層鏡像對象的生命週期

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

  1. 以Flutter層的對象生命週期爲主
  2. 在VM層增長WeakMap支持,不增長對象引用計數,Flutter層釋放以後,釋放VM層對象
  3. 在Native層使用 JSManagerValue,VM層對象釋放後,Native的引用被自動置空

image

線程問題

參照業界RN等框架的設計,VM層跑在一個單獨的後臺線程

  1. 從Flutter層經過Native通道調用到VM,發生兩次線程切換
  2. 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還有不少遺留的問題,做爲一個技術探索,很是辛苦但很是有趣,期待各位大牛指導,期待小夥伴們提出問題一塊兒討論解決。

文章最後我給你們推薦一個最簡單也是最有效的學習提高方法:腦圖 + 視頻 + 資料

在這也分享一份本身收錄整理的阿里P6P7【安卓】進階資料分享+加薪跳槽必備面試題 ,還有高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料這些都是我閒暇還會反覆翻閱的精品資料。在腦圖中,每一個知識點專題都配有相對應的實戰項目,能夠有效的幫助你們掌握知識點。

總之也是在這裏幫助你們學習提高進階,也節省你們在網上搜索資料的時間來學習,也能夠分享給身邊好友一塊兒學習

若是你有須要的話,能夠點贊+評論關注我加Vx:q1607947758(備註簡書,須要進階資料)

相關文章
相關標籤/搜索