Flutter 原理簡解

原文鏈接:mp.weixin.qq.com

Flutter 是 Google推出並開源的移動應用開發框架,主打跨平臺、高保真、高性能。開發者可以通過 Dart語言開發 App,一套代碼同時運行在 iOS 和 Android平臺。 Flutter提供了豐富的組件、接口,Flutter 是 Google推出並開源的移動應用開發框架,主打跨平臺、高保真、高性能。開發者可以通過 Dart語言開發 App,一套代碼同時運行在 iOS 和 Android平臺。 Flutter提供了豐富的組件、接口,開發者可以很快地為 Flutter添加 native擴展。同時 Flutter還使用 Native引擎渲染視圖,這無疑能為用戶提供良好的體驗。

好了,上面的官腔打完了,我們下面開始去理解 Flutter的實現原理。

繪圖基本原理

提到原理,我們要從屏幕顯示圖像的基本原理開始談起。 我們都知道顯示器以固定的頻率刷新,比如 iPhone的 60Hz、iPad Pro的 120Hz。當一幀圖像繪製完畢後準備繪製下一幀時,顯示器會發出一個垂直同步信號(VSync),所以 60Hz的屏幕就會一秒內發出 60次這樣的信號。 並且一般地來說,計算機系統中,CPU、GPU和顯示器以一種特定的方式協作:CPU將計算好的顯示內容提交給 GPU,GPU渲染後放入幀緩衝區,然後視頻控制器按照 VSync信號從幀緩衝區取幀數據傳遞給顯示器顯示。

Flutter 原理簡解

上圖來自 ibireme.com

所以,Android、iOS的 App在顯示 UI時是如此,Flutter也不例外,也遵循了這種模式。所以從這裡可以看出 Flutter和 React-Native之眾的本質區別:React-Native之類只是擴展調用 OEM組件,而 Flutter是自己渲染。 在 Flutter Architecture的解釋中,Google還提供了一張更為詳盡的圖來解釋 Flutter的原理:

Flutter 原理簡解

這張圖解釋得更清晰一些:Flutter只關心向 GPU提供視圖數據,GPU的 VSync信號同步到 UI線程,UI線程使用 Dart來構建抽象的視圖結構,這份數據結構在 GPU線程進行圖層合成,視圖數據提供給 Skia引擎渲染為 GPU數據,這些數據通過 OpenGL或者 Vulkan提供給 GPU。

所以 Flutter並不關心顯示器、視頻控制器以及 GPU具體工作,它只關心 GPU發出的 VSync信號,儘可能快地在兩個 VSync信號之間計算併合成視圖數據,並且把數據提供給 GPU。

幾個問題

瞭解 Flutter的基本概念後,自然有幾個疑問亟待解決。

1. 為什麼使用 Dart?

這是一個很有意思的問題,Flutter選擇了 Dart而不是 JavaScript。我覺得主要有以下幾個原因:

  1. Dart 的性能更好。Dart在 JIT模式下,速度與 JavaScript基本持平。但是 Dart支持 AOT,當以 AOT模式運行時,JavaScript便遠遠追不上了。速度的提升對高幀率下的視圖數據計算很有幫助。
  2. Native Binding。在 Android上,v8的 Native Binding可以很好地實現,但是 iOS上的 JavaScriptCore不可以,所以如果使用 JavaScript,Flutter 基礎框架的代碼模式就很難統一了。而 Dart的 Native Binding可以很好地通過 Dart Lib實現。
  3. Fuchsia OS,看起來不像原因的一個原因。Fuchsia OS內置的應用瀏覽器就是使用 Dart語言作為 App的開發語言。而且實際上,Flutter是 Fuchisa OS的應用框架概念上的一個子集。(Flutter源代碼和編譯工具鏈也充斥了大量的 Fuchsia宏)
  4. Dart是類型安全的語言,擁有完善的包管理和諸多特性。Google召集了如此多個編程語言界的設計專家開發出這樣一門語言,旨在取代 JavaScript,所以 Fuchsia OS內置了 Dart。Dart可以作為 embedded lib嵌入應用,而不用只能隨著系統升級才能獲得更新,這也是優勢之一。

2. Skia是什麼?

前面提到了 Flutter只關心如何構建視圖抽象結構,向 GPU提供視圖數據。Skia就是 Flutter向 GPU提供數據的途徑。

Skia是一個 2D的繪圖引擎庫,其前身是一個向量繪圖軟件,Chrome和 Android均採用 Skia作為繪圖引擎。Skia提供了非常友好的 API,並且在圖形轉換、文字渲染、位圖渲染方面都提供了友好、高效的表現。Skia是跨平臺的,所以可以被嵌入到 Flutter的 iOS SDK中,而不用去研究 iOS閉源的 Core Graphics / Core Animation。

Android自帶了 Skia,所以 Flutter Android SDK要比 iOS SDK小很多。

3. Flutter是如何設計的?

上面說了這麼久的基礎,我們可能只知道 Flutter做了什麼,始終都還沒有從側面觀察 Flutter的整個架構設計,瞭解 Flutter如何去做。

Flutter 原理簡解

這張圖瞭解過 Flutter的人可能很多地方都看過,這邊來詳細解釋一下:

Flutter Framework: 這是一個純 Dart實現的 SDK,類似於 React在 JavaScript中的作用。它實現了一套基礎庫, 用於處理動畫、繪圖和手勢。並且基於繪圖封裝了一套 UI組件庫,然後根據 Material 和Cupertino兩種視覺風格區分開來。這個純 Dart實現的 SDK被封裝為了一個叫作 dart:ui的 Dart庫。我們在使用 Flutter寫 App的時候,直接導入這個庫即可使用組件等功能。

Flutter Engine: 這是一個純 C++實現的 SDK,其中囊括了 Skia引擎、Dart運行時、文字排版引擎等。不過說白了,它就是 Dart的一個運行時,它可以以 JIT、JIT Snapshot 或者 AOT的模式運行 Dart代碼。在代碼調用 dart:ui庫時,提供 dart:ui庫中 Native Binding 實現。 不過別忘了,這個運行時還控制著 VSync信號的傳遞、GPU數據的填充等,並且還負責把客戶端的事件傳遞到運行時中的代碼。

在瞭解屏幕繪圖的基本原理和 Flutter的一個整體概念後,我們下面詳細地來看一下 Flutter的大概實現。

由於我 Android知識不咋樣,所以僅分析 iOS平臺上的實現。Android可以參考本文思路理解代碼

Flutter應用的運行

要理解 Flutter的原理,我們從 entry point開始看 Flutter的代碼。由於應用框架大同小異,所以下文提及 Flutter的代碼即指代 Flutter Engine的代碼,而非 Flutter Dart Framework代碼。

下圖是我簡單整理了一下 Flutter應用啟動後的執行順序

Flutter 原理簡解

在應用的 View Controller 初始化後,會實例化一個 Flutter project的抽象(以下簡稱 project)。project會初始化一個 platform view的抽象實例,這個抽象實例會負責創建 Flutter 的運行時(以下簡稱 engine)。

當 View Controller將要顯示時,調用 project查找和組合 Flutter的應用資源 bundle,並且把資源提供給 engine。 engine在真正需要執行資源 bundle時才會創建 Dart執行的環境(懶加載,以下簡稱 Dart Controller),然後設置視圖窗口的一些屬性等東西(這是繪圖引擎必需的)。 然後 engine中的 Dart Controller會加載 Dart代碼並執行,執行的過程中會調用 dart:ui的 native binding實現向 GPU提供數據。

VSync信號的同步

要讓視圖動態化,僅僅能實現視圖繪製還不行,還得知道硬件何時發送了 VSync信號,通過獲取 VSync信號,計算並給 GPU提供數據來構建動態化的界面。不過每個平臺的 VSync信號的獲取方式不太一樣,我們這裡討論一下 iOS上的實現,以此管中窺豹。

源碼獲取於構建參見附錄

在 flutter/shell/platform/darwin/ios/framework/source/FlutterView.mm實現裡面可以看到,在 UIView的 CALayer delegate中調用了 SnapshotContentsSync函數,這個函數會回調到 GPU線程,從 GPU線程執行獲取 LayerTree,計算併合成位圖,然後把位圖信息傳遞給 Skia引擎,Skia引擎通過 CGContextRef把位圖信息傳遞給 GPU。

1- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context {
2 SnapshotContentsSync(context, self);
3}
1// 回調到 GPU線程計算,並且這裡用了一個 Dart future的 native版來同步等待 GPU線程執行結果
2void SnapshotContentsSync(CGContextRef context, UIView* view) {
3 auto gpu_thread = blink::Threads::Gpu();
4
5 if (!gpu_thread) {
6 return;
7 }
8
9 fxl::AutoResetWaitableEvent latch;
10 gpu_thread->PostTask([&latch, context, view]() {
11 SnapshotContents(context, [view isOpaque]);
12 latch.Signal();
13 });
14 latch.Wait();
15}
16
17// SnapshotContentsSync 內容太長不贅述,精簡看一下:
18{
19 // 獲取 LayerTree
20 flow::LayerTree* layer_tree = rasterizer->GetLastLayerTree();
21 if (layer_tree == nullptr) {
22 return;
23 }
24 // 獲取可合成圖層大小
25 auto size = layer_tree->frame_size();
26 if (size.isEmpty()) {
27 return;
28 }
29 SkCanvas canvas(bitmap);
30
31 {
32 // 合成圖層
33 flow::CompositorContext compositor_context(nullptr);
34 auto frame = compositor_context.AcquireFrame(nullptr, &canvas, false /* instrumentation */);
35 layer_tree->Raster(frame, false /* ignore raster cache. */);
36 }
37
38 canvas.flush();
39
40 // Draw the bitmap to the system provided snapshotting context.
41 // 把位圖使用 Skia傳遞給系統的 GPU緩衝區

42 SkCGDrawBitmap(context, bitmap, 0, 0);
43}

不過有一點不太確定的是,iOS的 Architecture中,並沒有地方明確地提到 CALayerDelegate是與 Vsync同步的。不過可以確定的是,CALayerDelegate是併發多線程的,這個在 CATiled Layer那裡可以得到體現,而 CALayer Delegate的數據確實提交給了 GPU緩衝區實現了屏幕圖像的顯示。

Flutter Engine的組成

我們現在再來回顧一下這張圖(我發誓我不是為了湊內容):

Flutter 原理簡解

我們找到了 VSync源,找到了 Skia把數據提交給 GPU的地方,這兩處對我們來說都是黑盒,所以就先不管了。而 UI Thread的 Dart,暫時不在目前的 engine討論範疇內。所以我們現在要分析的是給 UI 層提供能力的所有組件。

在我的理解中,整個 Flutter Engine可以粗略地劃分為三個部分:Dart UI、Runtime、Shell,我們一一道來。

1. Dart UI

Dart UI是一個 C++實現的 dart:ui庫的 Native Binding,並且 UI Lib也是 Dart GUI程序的應用主要入口。 Dart UI向上層提供了 window、text、canvas、geometry等通用的繪圖能力, Runtime在調用 Dart UI時,Dart UI根據傳遞的 main entrypoint 來執行並且向 window渲染圖像。 值得一提的是,Dart UI還向上層提供了 compsiting的繪圖能力,這其實就是一個 Skia的 Scene的封裝,上層在調用 compsiting時其實就會生成或掛載節點到 LayerTree上。然後通過 LayerTree的數據結構輔助 Skia進行 Scene合成位圖。

LayerTree是 flow庫中的圖層抽象類。flow 是 chrome項目中通用的繪圖數據結構抽象庫,可以適配到其他繪圖引擎上。

2. Runtime

Flutter Engine的 Runtime可謂比較複雜,並不是代碼多,而是使用了非常多的 Delegate模式,將平臺相關的代碼交由 Shell部分實現。

Runtime負責創建 Dart的運行時,並且在不同的開發階段運行的環境也不一樣。開發時期是保留 check mode的 Dart Snapshot VM,而生產環境是 Dart AOT Runtime。

Dart Snapshot VM 和 Dart JIT VM有著本質的區別。Dart Snapshot是指 token化的 Dart腳本,並非 human readable的。而 JIT VM 是直接以>

Flutter 原理簡解

上圖,紅框部分是在 Runtime部分執行的邏輯。engine的抽象處於 Shell層,而 ui_dart_state處於 Dart UI 層。 我們可以看到 Runtime會由 Shell層調用生成一個 runtime controller 實例,這個實例管理著當前的繪圖窗口window的屬性,一個 Dart的VM 的實例,以及一個 delegate,這個 delegate能打通 Shell層和 Dart UI層的通信,並且負責事件的傳遞。

3. Shell

這裡說的 Shell其實就是 “殼”,這個殼就是組合 Runtime、第三方工具庫、平臺特性等,實現調用和執行 Flutter應用的邏輯。

  1. Shell 封裝了一個 engine的抽象,這個抽象能夠調用 Runtime,實現 Runtime中的 Delegate,以此向 Runtime提供數據和回調。
  2. Shell 還封裝了 platform view的抽象,並且具體地實現了 platform view,在 iOS特定代碼中的表現就是遵循 Delegate方法並提供了 UIView實例的管理。
  3. Shell 提供了一些基礎工具的封裝,如 Future,可以實現 dart:io中的 Future相同的執行邏輯,並且還負責了處理 VSync信號,UI、GPU Thread的回調。
  4. Shell 提供了從 engine獲取 LayerTree的和調用渲染方法的封裝。

總的來說,Dart UI給 Dart提供了調用 Native繪圖的能力,Runtime為 Flutter提供了調用 Dart和 Dart UI的能力,而 Shell才是集大成者,Shell將他們組合起來,並且從他們生成的數據中實現渲染。

Flutter 原理簡解

幾個問題 Again

由於代碼量龐大,不說 Line by Line, File by File也是一項非常龐大的工作,所以大致瞭解原理後不再贅述。著重解答幾個問題:

1. Flutter能動態更新嗎?

原版不行。理論可行。動態下發意味著 Dart源代碼需要以 JIT或 JIT Snapshot的方式運行,而 Flutter的 production build是 AOT代碼,所以原版不行。但 Flutter的 debug build是 JIT Snapshot運行,可以動態更新。 那麼,既要 production build,又要 JIT Snapshot執行,該如何做呢? Flutter Engine SDK的 build option裡面可以設置 mode = release, AOT = false,那麼 打出來的 Engine SDK不會包含 Dart AOT Runtime。 並且需要注意 Flutter CLI TOOL的編譯方式,需要以 Snapshot方式編譯最終的 production代碼。 值得一提的是,JIT Snapshot方式執行性能可能稍差,60fps可能會達不到。

2. Flutter SDK體積為什麼非常大?

Flutter應用的體積由兩部分組成:應用代碼和 SDK代碼。應用代碼是 Dart編譯後的代碼,如果做成可動態下發,那麼這部分可以不計。 SDK代碼比較大就有點無奈了,SDK的組成部分有 Dart VM,Dart標準庫,libwebp、libpng、libboringssl等第三方庫,libskia,Dart UI庫,然後再加上 icu_data,可能在單 arch下(iOS),SDK體積達到 40+MB。其中僅僅 Dart VM(不包含標準庫)就達到了 7MB。 Flutter SDK是 dynamic framework,如此大的二進制體積可能會造成動態鏈接耗時長。而如果靜態鏈接,可能原來比較大的 App很有可能造成 TEXT段超標。

3. Flutter可以跑多個實例嗎?

理論上是可以的。雖然 Flutter Engine的 Shell層寫死了只會跑一個 Flutter View(只會跑一個 Runtime),但這是可以改變的,而且只需要少量的邏輯改動。唯一需要擔心的就是多個實例的內存消耗。

4. 去掉 Flutter的特性,只嵌入 Dart到應用中,可能嗎?

可行。Dart毫無疑問是一門優秀的編程語言,我也曾嘗試將 Dart獨立出來作為一個通用的 SDK。Dart SDK託管在 chromium項目中,並且提供了 cross building的選項。原版即提供了 Android Build腳本。但在 iOS上原版行不通,猜測主要是標準庫的問題。在 Flutter iOS項目中,Dart 標準庫提供了一份完全不同的實現。而且,想要把 Dart VM和標準庫從 Flutter剝離出來,太困難了。並且單個 arch下,Dart VM加標準庫的體積是 > 10MB的。

The End

本期 Flutter原理的簡解就到這兒,其實主要是高談闊論了一番 Flutter Engine的實現。至於 Flutter的 UI Framework,稍候幾天再水一篇吧。

附錄

構建 Flutter Engine (for iOS)

  1. fork engine 項目
  2. 設置開發環境
  • Mac (Xcode 9.0)

  • Python >= 2.7.10

  • 安裝 depot_tools (Google工具鏈)
1git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
  • 修改 .bashrc (zsh 修改 .zshrc),把 depot_tools添加到環境變量中
1export PATH=$PATH:/depot_tools
  • 上述操作完成後重啟命令行客戶端使設置生效
  • 確認你的命令行有工具命令 curl 和 wget

  • 如果使用了 Mac,確認安裝 brew install ant
  1. 把你的 engine項目拉到本地,存放目錄就叫做 engine
  2. 在 engine 目錄中創建文件 .gclient, 填寫內容如下:solutions = [ { "managed": False, "name": "src/flutter", "url": "[email protected]:/engine.git", "custom_deps": {}, "deps_file": "DEPS", "safesync_url": "", }, ]
  3. 在 engine目錄下執行命令 gclient sync, 此操作需要命令行網絡代理 ,推薦使用 ShadowSocksX-NG
  4. 在步驟5完成後,engine目錄下會多出一個 src目錄,這個目錄是真正寫代碼、編譯的地方。這個目錄下,添加 git upstream 源:
1git remote add upstream https://github.com/flutter/engine.git
  1. 在進行下一步前,確保代碼是最新,進行 fetch -> pull rebase upstream master
  2. 在 src目錄下面,執行命令:./flutter/tools/gn --ios --simulator --unoptimized 生成編譯文件
  3. 在 src目錄下,執行編譯命令:ninja -C out/ios_debug_sim_unopt
  4. 編譯完成後可以在 out/ios_debug_sim_unopt目錄下找到 Flutter.framework文件,即可集成進 iOS工程
  5. 打開 all.xcworkspace即可查看 Flutter源碼

發者可以很快地為 Flutter添加 native擴展。同時 Flutter還使用 Native引擎渲染視圖,這無疑能為用戶提供良好的體驗。

好了,上面的官腔打完了,我們下面開始去理解 Flutter的實現原理。

繪圖基本原理

提到原理,我們要從屏幕顯示圖像的基本原理開始談起。 我們都知道顯示器以固定的頻率刷新,比如 iPhone的 60Hz、iPad Pro的 120Hz。當一幀圖像繪製完畢後準備繪製下一幀時,顯示器會發出一個垂直同步信號(VSync),所以 60Hz的屏幕就會一秒內發出 60次這樣的信號。 並且一般地來說,計算機系統中,CPU、GPU和顯示器以一種特定的方式協作:CPU將計算好的顯示內容提交給 GPU,GPU渲染後放入幀緩衝區,然後視頻控制器按照 VSync信號從幀緩衝區取幀數據傳遞給顯示器顯示。

Flutter 原理簡解

上圖來自 ibireme.com

所以,Android、iOS的 App在顯示 UI時是如此,Flutter也不例外,也遵循了這種模式。所以從這裡可以看出 Flutter和 React-Native之眾的本質區別:React-Native之類只是擴展調用 OEM組件,而 Flutter是自己渲染。 在 Flutter Architecture的解釋中,Google還提供了一張更為詳盡的圖來解釋 Flutter的原理:

Flutter 原理簡解

這張圖解釋得更清晰一些:Flutter只關心向 GPU提供視圖數據,GPU的 VSync信號同步到 UI線程,UI線程使用 Dart來構建抽象的視圖結構,這份數據結構在 GPU線程進行圖層合成,視圖數據提供給 Skia引擎渲染為 GPU數據,這些數據通過 OpenGL或者 Vulkan提供給 GPU。

所以 Flutter並不關心顯示器、視頻控制器以及 GPU具體工作,它只關心 GPU發出的 VSync信號,儘可能快地在兩個 VSync信號之間計算併合成視圖數據,並且把數據提供給 GPU。

幾個問題

瞭解 Flutter的基本概念後,自然有幾個疑問亟待解決。

1. 為什麼使用 Dart?

這是一個很有意思的問題,Flutter選擇了 Dart而不是 JavaScript。我覺得主要有以下幾個原因:

  1. Dart 的性能更好。Dart在 JIT模式下,速度與 JavaScript基本持平。但是 Dart支持 AOT,當以 AOT模式運行時,JavaScript便遠遠追不上了。速度的提升對高幀率下的視圖數據計算很有幫助。
  2. Native Binding。在 Android上,v8的 Native Binding可以很好地實現,但是 iOS上的 JavaScriptCore不可以,所以如果使用 JavaScript,Flutter 基礎框架的代碼模式就很難統一了。而 Dart的 Native Binding可以很好地通過 Dart Lib實現。
  3. Fuchsia OS,看起來不像原因的一個原因。Fuchsia OS內置的應用瀏覽器就是使用 Dart語言作為 App的開發語言。而且實際上,Flutter是 Fuchisa OS的應用框架概念上的一個子集。(Flutter源代碼和編譯工具鏈也充斥了大量的 Fuchsia宏)
  4. Dart是類型安全的語言,擁有完善的包管理和諸多特性。Google召集了如此多個編程語言界的設計專家開發出這樣一門語言,旨在取代 JavaScript,所以 Fuchsia OS內置了 Dart。Dart可以作為 embedded lib嵌入應用,而不用只能隨著系統升級才能獲得更新,這也是優勢之一。

2. Skia是什麼?

前面提到了 Flutter只關心如何構建視圖抽象結構,向 GPU提供視圖數據。Skia就是 Flutter向 GPU提供數據的途徑。

Skia是一個 2D的繪圖引擎庫,其前身是一個向量繪圖軟件,Chrome和 Android均採用 Skia作為繪圖引擎。Skia提供了非常友好的 API,並且在圖形轉換、文字渲染、位圖渲染方面都提供了友好、高效的表現。Skia是跨平臺的,所以可以被嵌入到 Flutter的 iOS SDK中,而不用去研究 iOS閉源的 Core Graphics / Core Animation。

Android自帶了 Skia,所以 Flutter Android SDK要比 iOS SDK小很多。

3. Flutter是如何設計的?

上面說了這麼久的基礎,我們可能只知道 Flutter做了什麼,始終都還沒有從側面觀察 Flutter的整個架構設計,瞭解 Flutter如何去做。

Flutter 原理簡解

這張圖瞭解過 Flutter的人可能很多地方都看過,這邊來詳細解釋一下:

Flutter Framework: 這是一個純 Dart實現的 SDK,類似於 React在 JavaScript中的作用。它實現了一套基礎庫, 用於處理動畫、繪圖和手勢。並且基於繪圖封裝了一套 UI組件庫,然後根據 Material 和Cupertino兩種視覺風格區分開來。這個純 Dart實現的 SDK被封裝為了一個叫作 dart:ui的 Dart庫。我們在使用 Flutter寫 App的時候,直接導入這個庫即可使用組件等功能。

Flutter Engine: 這是一個純 C++實現的 SDK,其中囊括了 Skia引擎、Dart運行時、文字排版引擎等。不過說白了,它就是 Dart的一個運行時,它可以以 JIT、JIT Snapshot 或者 AOT的模式運行 Dart代碼。在代碼調用 dart:ui庫時,提供 dart:ui庫中 Native Binding 實現。 不過別忘了,這個運行時還控制著 VSync信號的傳遞、GPU數據的填充等,並且還負責把客戶端的事件傳遞到運行時中的代碼。

在瞭解屏幕繪圖的基本原理和 Flutter的一個整體概念後,我們下面詳細地來看一下 Flutter的大概實現。

由於我 Android知識不咋樣,所以僅分析 iOS平臺上的實現。Android可以參考本文思路理解代碼

Flutter應用的運行

要理解 Flutter的原理,我們從 entry point開始看 Flutter的代碼。由於應用框架大同小異,所以下文提及 Flutter的代碼即指代 Flutter Engine的代碼,而非 Flutter Dart Framework代碼。

下圖是我簡單整理了一下 Flutter應用啟動後的執行順序

Flutter 原理簡解

在應用的 View Controller 初始化後,會實例化一個 Flutter project的抽象(以下簡稱 project)。project會初始化一個 platform view的抽象實例,這個抽象實例會負責創建 Flutter 的運行時(以下簡稱 engine)。

當 View Controller將要顯示時,調用 project查找和組合 Flutter的應用資源 bundle,並且把資源提供給 engine。 engine在真正需要執行資源 bundle時才會創建 Dart執行的環境(懶加載,以下簡稱 Dart Controller),然後設置視圖窗口的一些屬性等東西(這是繪圖引擎必需的)。 然後 engine中的 Dart Controller會加載 Dart代碼並執行,執行的過程中會調用 dart:ui的 native binding實現向 GPU提供數據。

VSync信號的同步

要讓視圖動態化,僅僅能實現視圖繪製還不行,還得知道硬件何時發送了 VSync信號,通過獲取 VSync信號,計算並給 GPU提供數據來構建動態化的界面。不過每個平臺的 VSync信號的獲取方式不太一樣,我們這裡討論一下 iOS上的實現,以此管中窺豹。

源碼獲取於構建參見附錄

在 flutter/shell/platform/darwin/ios/framework/source/FlutterView.mm實現裡面可以看到,在 UIView的 CALayer delegate中調用了 SnapshotContentsSync函數,這個函數會回調到 GPU線程,從 GPU線程執行獲取 LayerTree,計算併合成位圖,然後把位圖信息傳遞給 Skia引擎,Skia引擎通過 CGContextRef把位圖信息傳遞給 GPU。

1- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context {
2 SnapshotContentsSync(context, self);
3}
1// 回調到 GPU線程計算,並且這裡用了一個 Dart future的 native版來同步等待 GPU線程執行結果
2void SnapshotContentsSync(CGContextRef context, UIView* view) {
3 auto gpu_thread = blink::Threads::Gpu();
4
5 if (!gpu_thread) {
6 return;
7 }
8
9 fxl::AutoResetWaitableEvent latch;
10 gpu_thread->PostTask([&latch, context, view]() {
11 SnapshotContents(context, [view isOpaque]);
12 latch.Signal();
13 });
14 latch.Wait();
15}
16
17// SnapshotContentsSync 內容太長不贅述,精簡看一下:
18{
19 // 獲取 LayerTree
20 flow::LayerTree* layer_tree = rasterizer->GetLastLayerTree();
21 if (layer_tree == nullptr) {
22 return;
23 }
24 // 獲取可合成圖層大小
25 auto size = layer_tree->frame_size();
26 if (size.isEmpty()) {
27 return;
28 }
29 SkCanvas canvas(bitmap);
30
31 {
32 // 合成圖層
33 flow::CompositorContext compositor_context(nullptr);
34 auto frame = compositor_context.AcquireFrame(nullptr, &canvas, false /* instrumentation */);
35 layer_tree->Raster(frame, false /* ignore raster cache. */);
36 }
37
38 canvas.flush();
39
40 // Draw the bitmap to the system provided snapshotting context.
41 // 把位圖使用 Skia傳遞給系統的 GPU緩衝區

42 SkCGDrawBitmap(context, bitmap, 0, 0);
43}

不過有一點不太確定的是,iOS的 Architecture中,並沒有地方明確地提到 CALayerDelegate是與 Vsync同步的。不過可以確定的是,CALayerDelegate是併發多線程的,這個在 CATiled Layer那裡可以得到體現,而 CALayer Delegate的數據確實提交給了 GPU緩衝區實現了屏幕圖像的顯示。

Flutter Engine的組成

我們現在再來回顧一下這張圖(我發誓我不是為了湊內容):

Flutter 原理簡解

我們找到了 VSync源,找到了 Skia把數據提交給 GPU的地方,這兩處對我們來說都是黑盒,所以就先不管了。而 UI Thread的 Dart,暫時不在目前的 engine討論範疇內。所以我們現在要分析的是給 UI 層提供能力的所有組件。

在我的理解中,整個 Flutter Engine可以粗略地劃分為三個部分:Dart UI、Runtime、Shell,我們一一道來。

1. Dart UI

Dart UI是一個 C++實現的 dart:ui庫的 Native Binding,並且 UI Lib也是 Dart GUI程序的應用主要入口。 Dart UI向上層提供了 window、text、canvas、geometry等通用的繪圖能力, Runtime在調用 Dart UI時,Dart UI根據傳遞的 main entrypoint 來執行並且向 window渲染圖像。 值得一提的是,Dart UI還向上層提供了 compsiting的繪圖能力,這其實就是一個 Skia的 Scene的封裝,上層在調用 compsiting時其實就會生成或掛載節點到 LayerTree上。然後通過 LayerTree的數據結構輔助 Skia進行 Scene合成位圖。

LayerTree是 flow庫中的圖層抽象類。flow 是 chrome項目中通用的繪圖數據結構抽象庫,可以適配到其他繪圖引擎上。

2. Runtime

Flutter Engine的 Runtime可謂比較複雜,並不是代碼多,而是使用了非常多的 Delegate模式,將平臺相關的代碼交由 Shell部分實現。

Runtime負責創建 Dart的運行時,並且在不同的開發階段運行的環境也不一樣。開發時期是保留 check mode的 Dart Snapshot VM,而生產環境是 Dart AOT Runtime。

Dart Snapshot VM 和 Dart JIT VM有著本質的區別。Dart Snapshot是指 token化的 Dart腳本,並非 human readable的。而 JIT VM 是直接以>

Flutter 原理簡解

上圖,紅框部分是在 Runtime部分執行的邏輯。engine的抽象處於 Shell層,而 ui_dart_state處於 Dart UI 層。 我們可以看到 Runtime會由 Shell層調用生成一個 runtime controller 實例,這個實例管理著當前的繪圖窗口window的屬性,一個 Dart的VM 的實例,以及一個 delegate,這個 delegate能打通 Shell層和 Dart UI層的通信,並且負責事件的傳遞。

3. Shell

這裡說的 Shell其實就是 “殼”,這個殼就是組合 Runtime、第三方工具庫、平臺特性等,實現調用和執行 Flutter應用的邏輯。

  1. Shell 封裝了一個 engine的抽象,這個抽象能夠調用 Runtime,實現 Runtime中的 Delegate,以此向 Runtime提供數據和回調。
  2. Shell 還封裝了 platform view的抽象,並且具體地實現了 platform view,在 iOS特定代碼中的表現就是遵循 Delegate方法並提供了 UIView實例的管理。
  3. Shell 提供了一些基礎工具的封裝,如 Future,可以實現 dart:io中的 Future相同的執行邏輯,並且還負責了處理 VSync信號,UI、GPU Thread的回調。
  4. Shell 提供了從 engine獲取 LayerTree的和調用渲染方法的封裝。

總的來說,Dart UI給 Dart提供了調用 Native繪圖的能力,Runtime為 Flutter提供了調用 Dart和 Dart UI的能力,而 Shell才是集大成者,Shell將他們組合起來,並且從他們生成的數據中實現渲染。

Flutter 原理簡解

幾個問題 Again

由於代碼量龐大,不說 Line by Line, File by File也是一項非常龐大的工作,所以大致瞭解原理後不再贅述。著重解答幾個問題:

1. Flutter能動態更新嗎?

原版不行。理論可行。動態下發意味著 Dart源代碼需要以 JIT或 JIT Snapshot的方式運行,而 Flutter的 production build是 AOT代碼,所以原版不行。但 Flutter的 debug build是 JIT Snapshot運行,可以動態更新。 那麼,既要 production build,又要 JIT Snapshot執行,該如何做呢? Flutter Engine SDK的 build option裡面可以設置 mode = release, AOT = false,那麼 打出來的 Engine SDK不會包含 Dart AOT Runtime。 並且需要注意 Flutter CLI TOOL的編譯方式,需要以 Snapshot方式編譯最終的 production代碼。 值得一提的是,JIT Snapshot方式執行性能可能稍差,60fps可能會達不到。

2. Flutter SDK體積為什麼非常大?

Flutter應用的體積由兩部分組成:應用代碼和 SDK代碼。應用代碼是 Dart編譯後的代碼,如果做成可動態下發,那麼這部分可以不計。 SDK代碼比較大就有點無奈了,SDK的組成部分有 Dart VM,Dart標準庫,libwebp、libpng、libboringssl等第三方庫,libskia,Dart UI庫,然後再加上 icu_data,可能在單 arch下(iOS),SDK體積達到 40+MB。其中僅僅 Dart VM(不包含標準庫)就達到了 7MB。 Flutter SDK是 dynamic framework,如此大的二進制體積可能會造成動態鏈接耗時長。而如果靜態鏈接,可能原來比較大的 App很有可能造成 TEXT段超標。

3. Flutter可以跑多個實例嗎?

理論上是可以的。雖然 Flutter Engine的 Shell層寫死了只會跑一個 Flutter View(只會跑一個 Runtime),但這是可以改變的,而且只需要少量的邏輯改動。唯一需要擔心的就是多個實例的內存消耗。

4. 去掉 Flutter的特性,只嵌入 Dart到應用中,可能嗎?

可行。Dart毫無疑問是一門優秀的編程語言,我也曾嘗試將 Dart獨立出來作為一個通用的 SDK。Dart SDK託管在 chromium項目中,並且提供了 cross building的選項。原版即提供了 Android Build腳本。但在 iOS上原版行不通,猜測主要是標準庫的問題。在 Flutter iOS項目中,Dart 標準庫提供了一份完全不同的實現。而且,想要把 Dart VM和標準庫從 Flutter剝離出來,太困難了。並且單個 arch下,Dart VM加標準庫的體積是 > 10MB的。

The End

本期 Flutter原理的簡解就到這兒,其實主要是高談闊論了一番 Flutter Engine的實現。至於 Flutter的 UI Framework,稍候幾天再水一篇吧。

附錄

構建 Flutter Engine (for iOS)

  1. fork engine 項目
  2. 設置開發環境
  • Mac (Xcode 9.0)

  • Python >= 2.7.10

  • 安裝 depot_tools (Google工具鏈)
1git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
  • 修改 .bashrc (zsh 修改 .zshrc),把 depot_tools添加到環境變量中
1export PATH=$PATH:/depot_tools
  • 上述操作完成後重啟命令行客戶端使設置生效
  • 確認你的命令行有工具命令 curl 和 wget

  • 如果使用了 Mac,確認安裝 brew install ant
  1. 把你的 engine項目拉到本地,存放目錄就叫做 engine
  2. 在 engine 目錄中創建文件 .gclient, 填寫內容如下:solutions = [ { "managed": False, "name": "src/flutter", "url": "[email protected]:/engine.git", "custom_deps": {}, "deps_file": "DEPS", "safesync_url": "", }, ]
  3. 在 engine目錄下執行命令 gclient sync, 此操作需要命令行網絡代理 ,推薦使用 ShadowSocksX-NG
  4. 在步驟5完成後,engine目錄下會多出一個 src目錄,這個目錄是真正寫代碼、編譯的地方。這個目錄下,添加 git upstream 源:
1git remote add upstream https://github.com/flutter/engine.git
  1. 在進行下一步前,確保代碼是最新,進行 fetch -> pull rebase upstream master
  2. 在 src目錄下面,執行命令:./flutter/tools/gn --ios --simulator --unoptimized 生成編譯文件
  3. 在 src目錄下,執行編譯命令:ninja -C out/ios_debug_sim_unopt
  4. 編譯完成後可以在 out/ios_debug_sim_unopt目錄下找到 Flutter.framework文件,即可集成進 iOS工程
  5. 打開 all.xcworkspace即可查看 Flutter源碼
Flutter 原理簡解

有需要獲取如上資料或者Android其他資料的朋友,幫忙轉發後私信我“乾貨分享“獲取,謝謝支持。


分享到:


相關文章: