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

騰訊技術工程發表於2019-06-25

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

可能是目前放出來的相對完整的Flutter動態化方案

緣起:18年10月份,我們團隊的iOS產品嚐試引入 Flutter,做為iOS開發,一接觸到Flutter就馬上感受到,Flutter 雖然強大,但不能動態化是阻礙我們使用她的唯一障礙了。捨棄Native的開發方式,一個很大的訴求是獲取動態更新的能力。看Google團隊對動態化的措辭,應該指望不上了,擼起袖子自己動手豐衣足食,所以啟動了Flutter動態化的專案

簡介

專案代號:MXFlutter (Matrix Flutter)

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

繼續前先瞥一眼整體的架構

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

效果

先看看使用效果,以下截圖全部使用 MXFlutter,用JS開發,大家可以把原始碼下載下來,裡面有完整的JS程式碼例項:

這個是APP示例截圖

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

這個對應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,是不是非常像 Dart。

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

現狀

MXFlutter雖然各個模組已相對完整,但投入生產還需要解決其中的BUG,由於19年初,小組啟動新專案,非常繁忙,幾乎沒有時間繼續開發,暫停了一端時間,目前人力仍然很緊張,如果大家有興趣,期待小夥伴們一起加入,共同豐富 MXFlutter 動態化能力。

0x0 探索過程中的幾個炮灰方案

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

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

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

靜態解析 Dart 缺點,不能寫邏輯,對編寫UI程式碼有很多限制,不能寫判斷語句,不能寫函式,要支援這些成本很高。所以只好放棄。

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

響應式UI框架

  1. WidgetTree:Widget 裡面儲存了一個檢視的配置資訊,可以高效的建立(build)和銷燬
  2. Element 是分離 WidgetTree 和真正的渲染物件的中間層, WidgetTree 用來描述對應的Element 屬性
  3. RenderObject 來執行 Diff, Hit Test 佈局、繪製

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

第一棵樹有完整的UI描述資訊,那麼我只要JIT下通過 DartVM 建立第一棵樹,其他耗時的操作都丟到AOT裡去。

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

Flutter 動態化方案二: 動態執行 Dart 語言,生產UI描述

和方法二靜態解析 Dart 對應,第三個方案是寫一個極其輕量的執行時庫,讓編寫 UI 的 Dart 程式碼執行了起來,生成樹形結構,再序列化為 JSON(debug),FlatBuffers (release)UI 描述。可以稱之為動態解析方案

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

具體渲染邏輯

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

總體架構

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

架構也有了,方案也有了,要Run起來還有幾個麻煩事要忙活,DartVM 要抽出來喔,Dart 層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,用自增Seq與Flutter

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

MXFlutter 事件

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

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

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]);
    },
)
複製程式碼

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

MXFlutter 動畫的方案

動畫引數在VM層配置一次,動畫開始後在Flutter層閉環迴圈rebuild,形成動畫效果,這個是比較通用的做法了。

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

0x03 渲染優化

不管JSWidget建立有多快,總是有跨語言執行,所以減少Build次數和減小Build出來的DSL UI描述大小,可以優化效能。

渲染優化1-區域性重新整理:配置樹Diff

一個事實

自動對比兩次Widget 無論如何都沒有直接建立一個新的快,如果開發者不參與,由框架來自動計算Diff是得不償失的

可行的方法

犧牲響應式UI框架的設計模式 採用和Native、Web的方式,由開發者參與自己設定Diff的節點,即根據ID獲取對應Widget,修改Widget引數,Rebuild生成新DSL

渲染優化2-區域性重新整理-巢狀節點

  • MXScriptWidget 是一個具備BuildWidget樹,快取Callback對映表,動畫支援的基本單位。可以作為普通FlutterWidget來使用。
  • 在Flutter層,如果Widget樹中節點有MXScriptWidget,則在對應節點上建立MXFlutterWidget自定義控制元件
  • 兩個子樹可以相互對應獲得區域性重新整理,callback回撥,動畫支援,Rebuild時所生產的UI DSL 大大減少,加快重新整理速率

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

渲染優化3-可以分離動態和靜態控制元件

MXStatelessWidget 可以通過使用無狀態的ScriptWidget來向框架標示,其下面的子樹,在每次build中不會變化,其build結果會被快取,下次在Flutter層直接複用

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

記憶體-跨層映象物件的生命週期

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

  1. 以Flutter層的物件生命週期為主
  2. 在VM層增加WeakMap支援,不增加物件引用計數,Flutter層釋放之後,釋放VM層物件
  3. 在Native層使用 JSManagerValue,VM層物件釋放後,Native的引用被自動置空

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

執行緒問題

參照業界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

0x06 MXFlutter 基礎建設

因為 JSCore 不支援模組化開發,不能引用其他檔案程式碼,我們參照 RN,使用 Node.js 的模組化程式碼,在Native 層支援 require 語法。開發時,IDE最好選用 VSCode,因為可以按照JS外掛,直接執行除錯JS

另外,我們通過重定向模擬器 JS 路徑檔案到開發機,使用者修改完 JS 檔案,便可直接看到相應修改,實現模擬器的頁面熱更新。

結語

由於時間緊張,MXFlutter還有很多遺留的問題,作為一個技術探索,非常辛苦但非常有趣,期待各位大牛指導,期待小夥伴們提出問題一起討論解決。

要了解全部,一定要拉下原始碼,執行起來看看,有問題可以留言一討論,MXFlutter會持續更新。

專案成員luca浪哥,nice,yockie帥哥貢獻了動畫,控制元件,示例APP等核心實現, chaodong老師負責了DartVM方案,IP老師幫忙提供了單元測試。 TGIF-iMatrix 是一個技術氛圍濃厚,有美女帥哥超有愛的團隊,也正在招聘iOS開發,歡迎投遞簡歷。imatrixteam@qq.com

另外做個小廣告,大家輕拍, 看點視訊-騰訊短視訊,年輕人都愛看 看點視訊 apps.apple.com/cn/app/id14…

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

相關文章