iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
0
Mobile Development

Why Flutter why? 從表層到底層,從如何到為何。系列 第 25

days[24] = "Flutter Web是怎麼運作的?(下)"

上一篇我們談到整個Flutter App+Framework在Web上可以直接編譯成Javascript來執行,而Flutter Framework在最終產出Layer Tree後,便會呼叫dart:ui來進行實際的渲染。我們也在上一篇看到,mobile上的dart:ui是透過Flutter Engine中的Skia來實作,而在web上則是使用DOM+Canvas。實際上到底怎麼實作,就讓我們繼續看下去。

Layer Tree

首先來看看這個我們一直提到的Layer Tree,實際上到底長什麼樣子。以Flutter官方經典的初始Counter App為例,它的Layer Tree最後會變成像這樣:
https://ithelp.ithome.com.tw/upload/images/20200925/20129053ASGEBhAQFu.png
這時候已經幾乎看不出我們最初的Widget Tree結構了,無關繪圖的一切都被移除,只留下最純粹的繪圖相關資訊。

根節點的Scene是dart:ui中,用來代表一個完整畫面的class,我們會呼叫Layer.addToScene,把圖層一個個加入Scene中。

Layer則是Flutter Framework中,用來表示各種圖層的class。TransformLayer和OffsetLayer應該很容易理解,PhysicalModel可以提供光線/陰影效果,在這裡就是AppBar和FloatingActionItem的陰影。除此之外,常見的Layer還有像是提供透明效果的OpcaityLayer、對圖像做濾鏡的ImageFilterLayer、用來裁切的ClipRRectLayer...等等。

最後的葉節點一定是PictureLayer,也就是真正要畫出一些東西的Layer。而PictureLayer裡包含的Picture,是dart:ui裡的一個class:

/// An object representing a sequence of recorded graphical operations.
///
/// To create a [Picture], use a [PictureRecorder].
///
/// A [Picture] can be placed in a [Scene] using a [SceneBuilder], via
/// the [SceneBuilder.addPicture] method. A [Picture] can also be
/// drawn into a [Canvas], using the [Canvas.drawPicture] method.
class Picture extends NativeFieldWrapperClass2

最後一步,就是透過Canvas.drawPicture來呼叫底層的繪圖函數,然後我們就可以進入瀏覽器的世界了。

  void _drawPicture(Picture picture) native 'Canvas_drawPicture';

DOM基本結構

再複習一次,在mobile上,dart:ui是透過呼叫C++寫成的Flutter Engine裡的Skia來繪圖的。而在web上,dart:ui是直接呼叫瀏覽器的DOM和Canvas API來進行繪圖,最後輸出的一個結合HTML, CSS, Canvas的DOM。這個DOM會長什麼樣子呢?我們可以把同一個Counter App佈署到瀏覽器上,再來檢視它:
https://ithelp.ithome.com.tw/upload/images/20200925/20129053RInGAlgpQp.png

  • 乍看之下可能會以為main.dart.js想必就是我們的main.dart,但實際上它是Flutter Framework的entry point,不是我們的entry point。我們等一下會看到我們的main.dart到底在那裡。
  • flt-glass-pane是Flutter用來攔截所有瀏覽器互動事件的元素,就像是在瀏覽器畫面上,蓋上一層手機的玻璃面板一樣。畢竟我們現在不是由HTML Element的onClick或input來進行互動,因此必須把所有事件攔截下來,交由Flutter Framework處理。
  • flt-scene-host就是用來掛載我們最終產生出來的Scene,之後會看到它裡面包含了和我們的Layer Tree非常相似的DOM Tree
  • flt-ruler-host就很有趣了,還記得我們上一篇提到的,關於Text Layout的小問題嗎?Flutter就是在這個隱藏的區域把Text交給瀏覽器實際渲染出來,再去讀取它的長寬、底線等數據,然後傳回RenderObject進行Layout。

flt-scene

https://ithelp.ithome.com.tw/upload/images/20200925/20129053XwjvqRVjAi.png
進入flt-scene-host之後,我們就會看到flt-scene,直接對應dart:ui的Scene class,代表一個完整的畫面。再進去的flt-transform自然就是對應到TransformLayer,可以看到它其實就是透過CSS的transform-origintransform來實作,雖然這裡最上層的TransformLayer沒有真的做任何transform,只是最為一個基底。後面所有Layer的效果,都是透過這樣的DOM Element+CSS一層層實現的。

把這部份完全展開的話會很難閱讀,因此剩下的部份我們還是畫成圖:
https://ithelp.ithome.com.tw/upload/images/20200925/20129053q4DhzAAESM.png
可以看到這裡的DOM Tree和Layer Tree幾乎是完全一對一的關係。除了明顯的flt-transform, flt-offset, flt-picture之外,PhysicalModel對應到的是兩層flt-clip和flt-clip-interior。

這裡的flt-picture被分成兩類,可以用DOM(HTML+CSS)來繪製的,和可以用Canvas來繪製的,而它們底下分別建立了flt-dom-canvasflt-canvas。可能要稍微釐清一下,這裡的flt-dom-canvas,或Flutter官方所謂的DomCanvas Backend,指的是把DOM當作Canvas來繪圖的一種實作方式,並不是DOM API+Canvas API。另一方面,flt-canvas,則是使用瀏覽器的Canvas API,被官方稱為BitmapCanvas的另一種實作方式。

一般來說,如果一個Picture用兩種模式繪製都可以,我們會優先選擇DomCanvas。主要原因是HTML+CSS是有被瀏覽器的Display List支援的,代表我們可以把Picture柵格化的優化完全交給瀏覽器的渲染引擎處理。同時,當我們進行任意的transform時,也不用擔心pixelation的問題。

沒辦法透過HTML+CSS繪製的部份,我們才會選擇使用Canvas API。Web的CanvasRenderingContext2D其實和Flutter的Canvas極為相似,因此dart:ui可以很輕易的進行對應。另外,Canvas的繪圖效能也相當高,因為我們不用去維護一個不斷變動的DOM Tree。Canvas在瀏覽器上是以Bitmap來顯示的,因此被稱為BitmapCanvas,這也是它最大的問題。不但平時佔用較多記憶體外,縮放時更需要重新分配記憶體,而且還會造成pixelation。因此,Flutter會盡量優先選擇透過DomCanvas來繪圖。

Javascript

最後,讓我們回到瀏覽器來找找看看,我們先前失蹤的main.dart在哪裡?如果我們查看main.dart.js的原始碼:

"use strict";

// Attach source mapping.
var mapperEl = document.createElement("script");
mapperEl.defer = true;
mapperEl.async = false;
mapperEl.src = "stack_trace_mapper.js";
document.head.appendChild(mapperEl);

// Attach require JS.
var requireEl = document.createElement("script");
requireEl.defer = true;
requireEl.async = false;
requireEl.src = "require.js";
// This attribute tells require JS what to load as main (defined below).
requireEl.setAttribute("data-main", "main_module.bootstrap"); <--- This is our lead
document.head.appendChild(requireEl);

然後我們再進入main_module.bootstrap,稍微往下拉一點:

....
let modulePaths = {
 "web_entrypoint.dart": "web_entrypoint.dart.lib",
 "packages/flutter_app/main.dart": "packages/flutter_app/main.dart.lib", // <---- Find it!
 "packages/flutter/widgets.dart": "packages/flutter/widgets.dart.lib",
 "packages/flutter/src/widgets/will_pop_scope.dart": "packages/flutter/src/widgets/will_pop_scope.dart.lib",
 "packages/flutter/src/widgets/widget_span.dart": "packages/flutter/src/widgets/widget_span.dart.lib",
 "packages/flutter/rendering.dart": "packages/flutter/rendering.dart.lib",
 "packages/flutter/src/rendering/wrap.dart": "packages/flutter/src/rendering/wrap.dart.lib",
 "packages/flutter/src/rendering/layer.dart": "packages/flutter/src/rendering/layer.dart.lib",
 "packages/flutter/painting.dart": "packages/flutter/painting.dart.lib",
....

這裡我們會看到整個Flutter Framework,連同我們的App,都被打包成packages讀取進來了。最後我們進入flutter_app/main.dart.lib(flutter_app是我的專案名稱):

....
  main.MyHomePage = class MyHomePage extends framework.StatefulWidget {
    get title() {
      return this[title$];
    }
    set title(value) {
      super.title = value;
    }
    createState() {
      return new main._MyHomePageState.new();
    }
  };
....

雖然經過dart2js編譯器的強大優化之後的Javascript程式碼並不適合直接閱讀,但一番搜索之後還是可以找到一些我們熟悉的文字。事實上,如果我們進入開發人員工具設定,把Javascript Source Mapping關掉:
https://ithelp.ithome.com.tw/upload/images/20200925/20129053WaVXvlmoFu.png
然後回到IDE下個中斷點並讓它中斷:
https://ithelp.ithome.com.tw/upload/images/20200925/20129053FYCNuHPhYj.png
再回到瀏覽器的開發人員工具...:
https://ithelp.ithome.com.tw/upload/images/20200925/20129053XLdbT4tiWV.png
這邊也同步中斷在對應的Javascript程式碼了!是不是很酷炫呢?


好了,有關Flutter Web的運作機制就先說到這。再次回顧整個Flutter架構,在mobile上是透過Flutter Engine的Skia來繪圖的,而在web上是透過瀏覽器的HTML, CSS和Canvas來繪圖的。

不過,不知道你有沒有想到,其實Chrome瀏覽器上也有Skia啊?這些HTML, CSS, Canvas,繞了一圈最後還不是靠Skia來繪圖。我們的dart:ui,難道就不能跳過HTML/CSS/Canvas,直接使用底層的Skia嗎?就算沒有Javascript的API,透過WebAssembly總可以了吧。

沒錯!如果你有在關注Flutter Web的新聞的話,應該有看到前陣子Flutter更新後加入的CanvasKit模式,大幅提昇了Flutter Web的效能,它背後正是靠WebAssembly+Skia來實現的。具體的運作機制,我們下次有機會再聊吧。


上一篇
days[23] = "Flutter Web是怎麼運作的?(上)"
下一篇
days[25] = "為什麼動畫需要Ticker?"
系列文
Why Flutter why? 從表層到底層,從如何到為何。30

尚未有邦友留言

立即登入留言