當 Flutter 遇見 Web,會有怎樣的祕密?

作者:haigecao,騰訊 CSIG Web 開發工程師

在線教育團隊(簡稱:OED)已經將 Flutter 這樣技術在業務中落地了,做為 IMWeb 前端團隊的我們也要進行一些嘗試。本文從前端角度進行 Flutter 開發的概況描述。主要是為了讓您瞭解和感受一下:Flutter to Web 的實例、Flutter 為什麼會出現、Flutter 設計實現原理、Flutter 技術特點和優勢。


前言

OED的客戶端團隊在 2019 年上半年 ,就已經把 Flutter 落地到企鵝輔導的業務中了。今年我們又一起去上海參加了 2019 年穀歌開發者大會,遇見了更多的 Flutter 開發者,這次體驗比第一次去的時候感覺熟悉了很多。希望未來有機會把他們邀請來深圳,進行一些 Flutter 的技術分享。此次開發者大會又恰逢 Flutter to Web 也已經正式合入 Master,那麼,前端同學是否可以趁著這股東風一起參與到 Flutter 的協同開發中呢,我想這問題會困擾著很多人?如果您有好的想法,可以在留言區參與評論。

本文不是一篇 Flutter 詳細的學習教程,更像是一個概覽,用盡可能平實的語言和對比的思路去描述它。本著依舊從前端同學的角度出發,去理解一項新的技術,但又不限於前端技術本身。希望您能通過這篇文章相對全面的理解 Flutter 這項技術本身。特別感謝領導的鼓勵和支持,讓我有機會去學習和理解 Flutter 框架,因為相對我而言,

OED 的客戶端團隊的同學經驗會遠超於我,他們已經完整經歷了業務從 0 到 1 的過程,這是一種非常有意思的體驗。


1、Flutter to Web 案例

下面轉換的工程案例 是我們的 企鵝輔導APP 裡面的業務代碼(詳細操作流程:https://github.com/flutter/flutter_web/blob/master/OLD_README.md)。Flutter 官方文檔具有很好的說明,如果您單純轉一個完全不依賴 APP 的項目,您安裝完環境並且切換到 Master 版本就可以直接進行了,甚至不需要任何代碼修改。轉換業務產品中的代碼,還需要處理一些奇奇怪怪的問題,相信這對您來說,應該都不是問題。可以在這裡安裝環境配置(https://flutter.cn/docs/get-started/install)進行環境安裝。Flutter 官網提供了一個 案例(https://flutter.cn/docs/get-started/codelab)可以嘗試一下。官方給了一個開箱即用的 開發文檔(https://flutter.cn/docs/development/ui/widgets-intro)。

上面的視頻裡面展示我們 企鵝輔導 的第三個 TAB 內的一個上課頁的 Flutter 業務實踐,以及轉換後的 Web 頁面。可以明顯看到,確實有一局部有些失真,但是用戶完全可用。

打包壓縮之後,Web 端代碼只有 1.4M,Mac 桌面端體驗過程中,沒有出現卡頓問題。這裡 Web 頁面內的渲染是通過 Canvas 渲染DOM 進行的頁面填充。Dart 原本從天生就支持在 Chrome 中使用的,只是在 2015 年不幸夭折,但是,它長期支持轉換為 JavaScript。因此,可以遇見的未來,隨著 Flutter 的發展,Dart to Js 業務實踐的進化速度,可能會超過 WASM 的業務使用

下面是一個處理無限列表的場景,無論是在 Mac,還是在移動端時,依舊會有卡頓的現象,FPS 表現並不理想。

當 Flutter 遇見 Web,會有怎樣的秘密?

首先官方目前還不建議,在產品化中使用 。但既然已經合入 Master,相信這一天也不會遠了。

轉換這裡需要解決一些問題,整理了一下官方建議和實踐的體驗:

當 Flutter 遇見 Web,會有怎樣的秘密?

業務轉換使用的時候,需要把系統依賴解決掉,部分樣式問題跟 Flutter 排版組件有關,而系統相關的如本地存儲、網絡請求需要我們自定義轉化方案。例如:客戶端使用的是 WNS 協議,而前端需要使用的是 HTTPS。目前看 Flutter to Web 作為業務容災的策略還是可以的,總是優於 APP Crash 或者 影響範圍非常大的 BUG 導致用戶無法使用的情況出現。這裡 Skia 有個明顯的短板,就是 3D 動畫,如果您業務對 3D 動畫依賴比較強,一段時間內,就不要選擇 Flutter 作為業務的技術選型了。

當 Flutter 遇見 Web,會有怎樣的秘密?

上面簡單的羅列了一些前端在 Flutter 的工作範圍,前端定位更多是打輔助的!當然如果您是全能型開發,也可以全部都做。技術本質上沒有邊界,侷限的只有自己。從前端角度看 Flutter 的開發成本相比於 H5 確實還是多了一些成本,但學起來也不會太難,只是時間的問題。對於團隊有人力,並且希望嘗試新技術的,完全放在業務中嘗試使用。

至於未來是否能確定是 Flutter 這個框架成為行業標杆,還不得而知。但如果想徹底統一全端技術棧,Flutter 今天的設計思路是一個非常有突破性的存在!應該說,類 Flutter 的自繪引擎方案在未來會有機會大放異彩

站在前端的角度上,我們嘗試著在組件化和工程化的方向找到自己在 Flutter 生態中的定位。並且相比於 IOS 和 Android,Flutter 的 Dart 代碼是完全開源的,參與感是會讓開發者提升不少的,就類似 linux 和 windows 的感覺。

至於團隊是否要參與進去,很多時候是看綜合的成本和收益,做與不做,做到什麼程度,適合什麼時候進行業務跟進,其實,都是要以團隊的價值最大化為目標,沒有絕對的對與錯,結合團隊的實際情況量身定製就好。錯誤的時機進入,也會付出不小的成本,您自己考量。

如下圖,橫向對比行業開源方案:

當 Flutter 遇見 Web,會有怎樣的秘密?

簡單對比來看,結合團隊的技術實踐和能力。站在第三方的角度上。RN 和 Flutter 相對是 2 個比較好的跨平臺方案。而且其它方案或多或少都有一些侷限。

那真的放到業務上,又當怎麼選擇呢?

回答這個問題確實有些艱難,仁者見仁智者見智。無論是相對成熟的 React Native,還是新貴 Flutter,放眼到整個大前端技術史裡面看,都不如 Hybird 影響的深遠。但如今它們也都達到了商用的標準。我同時使用過 Hybird 和 RN 作為業務接入方,算是會有一點點經驗之談。

從目前行業的產品,以及社區生態來說,React Native 整體還是勝出 Flutter 一籌。畢竟早出來幾年,市場佔有率和行業積累還是在的。但是長遠來看,技術發展也有它的必然規律,Flutter 的技術理念已經領先了 React Native,作為大公司、或者大前端團隊的技術儲備和技術選型,科技公司要想在未來在行業有一席之地,使用 Flutter 這樣的技術,必然也會是一個趨勢。至於開發者對於技術本身,應該也會對 Flutter 保有濃厚的興趣吧,畢竟技術永遠都會向前發展,無論是誰,不進步一樣會被淘汰。因此,作為技術開發人員首先保持的就是一顆好奇的心,進而持續成長。

當然,未來可能成功的框架不一定就是 Flutter,但是它設計理念和設計思路是一脈相承的,類 Flutter 框架一樣也會出現。就像 React 出來了,Vue 也會跟著出來了。有的團隊使用 Vue,有的團隊使用 React。但是,您會發現它們越來越像了。使用 Vue 的也沒必要說使用 React 的同學水平不好,不存在的!理念很重要,有一句話說的很有意思,叫:思路決定出路!


2、Flutter 技術架構

1)擁有了 RN,為什麼又會出現 Flutter

在談及 Flutter 之前,我們還是要先簡單回顧一下,客戶端的上一次技術革新 —— ReactNative(此後簡稱 RN)。相信非常多的團隊都有去落地實踐 RN 的機會,很多 APP 的首屏渲染方案都是用 RN 技術棧進行的。我們自己的產品 企鵝輔導騰訊課堂 內的應用也是一樣。

這裡簡單回顧一下,在有客戶端開發的場景下,為什麼又出現了 RN ?

RN 的價值簡單來講就是—— 可接受的頁面性能 + 高效開發 + 熱更新。

更新:傳統的 APP 上架之後,出現了業務 BUG,用戶只能去更新 APP,進行 BUG 修復。客戶端實現熱更新修復 BUG,有多難,可以問問 IOS 的開發同學。大概率猜測,手 Q 和微信,應該還是有方案可以熱更新的。但是對很多小廠商這確實是非常艱難的事情。因此,得益於強大的動態化能力 RN 的價值也就完美的體現出來了。

高效:一個 APP 發佈上線,Android 和 IOS 同時需要開發兩個應用,而 RN 只需要一套代碼,就可以運行在雙平臺上,節省很大的人力成本。並且很多業務線有很強的業務運營訴求,可能會存在很短時間內的多次改版和發佈的情況出現,客戶端開發的人力瓶頸和發佈週期的限制,已經很難滿足這樣的業務場景了。尤其在一些有損發佈的情況下,趕著時間點,帶著 BUG 上線的場景,在後續進行增量的修復,再這樣的情況下,傳統客戶端的表現,簡直就是災難性的。

性能:RN 具有優於 H5 的性能體驗。畢竟是通過客戶端進行的頁面渲染,速度上比 WebView 渲染還是要快不少的。這個在 Weex、Hippy、Plato 上都有所體現,雖然低於 Native 的性能,但是在可接受範圍。

PS:這裡的表達,不是描述客戶端開發不好。只是單純從業務角度上看待問題,而把合適的技術放在合適的位置是非常重要的,這也是架構師核心價值之一。

回顧了以上三點,我們發現 RN 的出現,有它的必然性。那麼回到主題,RN 已經這麼優秀了,為什麼還要有 Flutter 的存在,有一次向 Ab 哥請教技術成長的時候,Ab 哥提到了很有意思的一個觀點,就是您對一項技術瞭解的深入程度,取決於是否能認清這項技術的侷限。 就像人一樣,他(她)有多少優點,就會存在多少缺點。沒發現,不等於不存在,因為一定存在。因此,順著這個思路,我們簡單的看一下 RN 的問題。

首先,看維護成本,雖然 RN 是一套代碼多端運行。但還是需要 IOS 和 Android 開發幫助我們去一個一個的繪製組件,尤其遇到特殊訴求的時候,還要 case by case 的處理,並且隨著 IOS 和 Android 系統本身的迭代和升級,以及框架自身發展的歷史包袱,我們可能還需要處理很多與原生系統之間的平臺差異,修復各種奇奇怪怪的 BUG,這對業務來說是很大的負擔。

其次,對性能訴求,無論是產品還是開發同學,對於用戶體驗的追求,永遠都不會停止。RN 存在諸多性能的短板,因此,才會有 Weex 這樣的產品出現,去定製化的解決業務場景下的問題。JS 和 Native 的通信,頁面的事件監聽,複雜動畫的渲染和交換成本,都是很大的性能挑戰。

最後,在存在更強的業務訴求的時候,人們就不得不去尋找更好的方式去實現。非常存感激的看待谷歌這家公司,都是定位於商業公司,但實際上對世界的影響力上面,公司與公司之間差距還是非常大的。這個課題範圍太大,以後有機會可以深度討論一下。

您看到了上面的描述,為了解決上面這些問題 —— 自繪引擎時代出現了,以 Flutter 為代表的技術方案會應運而生,相信一定不會只有 Flutter 一項跨平臺技術出現的,歷史總是驚人的相似。其實想到自繪引擎,我最先想到的是那些遊戲引擎。那現在又為什麼給出 自繪引擎 這樣的一個概念呢?H5 是依賴於瀏覽器渲染,RN 依賴於客戶端渲染,而 Flutter 基於 Skia 自己繪製的圖形界面。因此,Flutter 才能真正實現跨端!相信在不久的未來,在傳統客戶端上也能看到 Flutter 的身影,這樣才能真正達到多端統一。

最後,我們再簡單總結一下有哪些問題:

1、Web 性能差,跟原生 App 存在肉眼可見的差距;

2、React Native 跟 Web 相比,支持的能力非常有限,特定長場景問題,需要三端團隊一個一個處理;

3、Web 瀏覽器的安卓碎片化嚴重(感謝 X5,騰訊的同學過得相對輕鬆一些)。

為了解決上面的問題,Flutter 出現了:

一套代碼可以運行在兩端;自繪 UI,脫離平臺,也可以簡單的把它理解為一個瀏覽器的子集。

鋪墊了這麼多,就是為了幫助您回憶起技術發展的脈絡和技術趨勢,可以更好的理解下面即將要表達的文稿,下面我們正式開始介紹 Flutter。


2)Flutter 實現原理

Flutter 能介紹的技術點其實非常多,這裡找了一些具有代表性的技術項,結合自己的理解跟大家分享一下。包括設計思路、渲染方式、UI 的生命週期。因為這幾個點,跟 React 技術棧風格非常相似,以這種思考結構去對比介紹,可以幫助大家更好的理解這項技術本身。

Flutter 整體架構設計

當 Flutter 遇見 Web,會有怎樣的秘密?

Google 了一下關鍵詞,搜素得到了這張圖片,從下向上進行一些描述:

Embedder:是操作系統適配層,實現了渲染 Surface 設置,線程設置,以及平臺插件等平臺相關特性的適配。從這裡我們可以看到,Flutter 平臺相關特性並不多,這就使得從框架層面保持跨端一致性的成本相對較低。

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

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

PS:雖然很早知道 Flutter,但實際寫 Flutter 時間也比較短暫。引擎源碼層面,目前也沒有深入的涉獵。瞭解的方式可以通過自己閱讀源碼,或者找谷歌、阿里、美團、以及我司的開發者幫忙。從技術角度來了解這些,在需要的階段,不會成為大家的瓶頸。畢竟商業世界充滿了壁壘,而應用層面的技術本身是開放的。


3)Flutter 應用層語言 Dart

簡單描述一下 JIT 與 AOT

  • JIT
    在運行時即時編譯,在開發週期中使用,可以動態下發和執行代碼,開發測試效率高,但運行速度和執行性能則會因為運行時即時編譯受到影響。
  • AOT
    即提前編譯,可以生成被直接執行的二進制代碼,運行速度快、執行性能表現好,但每次執行前都需要提前編譯,開發測試效率低。

Dart 是什麼

它的目標在於成為下一代結構化 Web 開發語言。Dart 發佈於 2011 年 10 月 Google 的"GOTO 國際軟件開發大會"。是一種基於類編程語言(class-based programminglanguage),在所有瀏覽器都能夠有高性能的運行效率。Chrome 瀏覽器內置了 DartVM,可以直接高效的運行 dart 代碼(2015 年被移出)。支持 Dart 代碼轉成 Javascript,直接在 Javascript引擎上運行。dart2js(https://dart.dev/tools/dart2js)

Dart 的特點

  • 開發時 JIT,提升開發效率;發佈時 AOT,提升性能。
  • 不會面對 JS 與 Native 之間交互的問題了。
  • Dart 的內存策略,採用多生代算法(與 Node 有一些類似)。
  • 線程模型依舊是單線程 Event Loop 模型,通過 isolate 進行隔離,可以降低開發難度(與 Node 也非常類似)。
  • Dart 的生態,這個跟 Node.js 差距十分明顯,npm 還是行業中最活躍的。
  • 而靜態語法與排版方式,純前端入門還是有一定成本。

備註:

  • 1)TS 可以一定程度上幫助 JS 添加一些靜態檢測,但本質上依舊是無法達成這樣的效果;
  • 2) 關於入門成本這個問題,如果您想深入,我相信這都不會成為問題。關鍵看是否能為業務和團隊帶來價值。

Flutter 選擇 Dart 的原因

  • 健全的類型系統,同時支持靜態類型檢查和運行時類型檢查。
  • 代碼體積優化(TreeShaking),編譯時只保留運行時需要調用的代碼(不允許反射這樣的隱式引用),所以龐大的 Widgets 庫不會造成發佈體積過大。
  • 豐富的底層庫,Dart 自身提供了非常多的庫。多生代無鎖垃圾回收器,專門為 UI 框架中常見的大量 Widgets 對象創建和銷燬優化。
  • 跨平臺,iOS 和 Android 共用一套代碼。
  • JIT & AOT 運行模式,支持開發時的快速迭代和正式發佈後最大程度發揮硬件性能。
  • Native Binding。在 Android 上,v8 的 Native Binding 可以很好地實現,但是 iOS 上的 JavaScriptCore 不可以,所以如果使用 JavaScript,Flutter 基礎框架的代碼模式就很難統一了。而 Dart 的 Native Binding 可以很好地通過 Dart Lib 實現。



4)Flutter 實現思路

看到了上面的介紹,這裡總結一下 Flutter 的實現思路。它開闢了新的設計理念,實現了真正的跨平臺的方案,自研 UI 框架,它的渲染引擎是 Skia 圖形庫來實現的,而開發語言選擇了同時支持 JIT 和 AOT 的 Dart。不僅保證了開發效率,同時也提升了執行效率。由於 Flutter 自繪 UI 的實現方式,因此也儘可能的減少了不同平臺之間的差異。也保持和原生應用一樣的高性能。因此,Flutter 也是跨平臺開發方案中最靈活和徹底的那個,它重寫了底層渲染邏輯和上層開發語言的一整套完整解決方案

  • 徹底跨端:
  • Flutter 構建了一整套包括底層渲染、頂層設計的全套開發套件。
  • 這樣不僅可以保證視圖渲染在 Android 和 IOS 上面的高度一致,也可以保證渲染和交互性能(媲美原生應用)。
  • 與現有方案核心區別:
  • 類 RN 方案,JS 開發,Native 渲染。數據通信 bridge;
  • Hybird 瀏覽器渲染 + 原生組件繪製;
  • Flutter 設計自閉環,完成渲染和數據通信。



3、Flutter 的 UI 渲染方案

渲染方案是 Flutter 目前獨特的設計形態,就是由於渲染自閉環,才能真正跨平臺。談到 UI 渲染方案,作為前端開發,我們是繞不過現在如火如荼的三大框架的。為什麼要談類 React 方案呢?因為 Flutter 的設計方案,與 React 設計具有一樣的思路。在渲染這裡我們會談及控件、渲染原理、以及生命週期。

Flutter 是如何進行頁面渲染的呢?傳統 Web 是通過瀏覽器,而 Flutter 是自繪。所謂自繪就是用戶界面上 Flutter 自己繪製到界面,無需依賴 IOS 和 Android 原生能力,是通過一個叫做 Skia 引擎進行頁面繪圖。


1)介紹一下 Skia

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

Skia 是用 C++ 開發的、性能彪悍的 2D 圖像繪製引擎,其前身是一個向量繪圖軟件。Skia 在圖形轉換、文字渲染、位圖渲染方面都表現卓越,並提供了開發者友好的 API。Android 自帶了 Skia,所以 Flutter Android SDK 要比 iOS SDK 小很多。正是得益於 Skia 的存在:

  • Flutter 底層的渲染能力得到了統一,不在需要使用做雙端適配;
  • 通過 OpenGL、GPU,不需要依賴原生的組件渲染框架。
  • Flutter 可以最大限度的抹平平臺差異,提升渲染效率和性能。


2)Flutter 的渲染流程

用戶可以看到一張圖像展示,至少需要三類介質:CPU、GPU 和 顯示器。CPU 負責圖像的數據計算,GPU 負責圖像數據的渲染,而顯示器是最終圖片展示的載體。CPU 拿到需要上屏的數據做處理和加工,處理完成之後交給 GPU,GPU 在渲染之後將數據放入幀緩衝區,隨後隨著控制同步信號 (VSync)以週期性的頻率,從緩衝區內讀出數據,在顯示器上進行圖像呈現。而且操作系統就是一個無限循環的機制,不停的重複上面的操作,進行顯示器的更新.

當 Flutter 遇見 Web,會有怎樣的秘密?

Flutter 的渲染整體流程也是這樣的, Dart 進行視圖數據的合成,然後交給 Skia 引擎進行處理,處理之後再交給 GPU 進行數據合成,然後準備上屏。當一幀圖像繪製完畢後準備繪製下一幀時,顯示器會發出一個垂直同步信號(VSync),所以 60Hz 的屏幕就會一秒內發出 60 次這樣的信號。


3)Flutter 繪製流程

當 Flutter 遇見 Web,會有怎樣的秘密?

如上圖所示,Flutter 渲染流程分為 7 個步驟:

首先是獲取到用戶的操作,然後你的應用會因此顯示一些動畫 ;

接著 Flutter 開始構建 Widget 對象。Widget 對象構建完成後進入渲染階段,這個階段主要包括三步:

  • 佈局元素:決定頁面元素在屏幕上的位置和大小;
  • 繪製階段:將頁面元素繪製成它們應有的樣式;
  • 合成階段:按照繪製規則將之前兩個步驟的產物組合在一起

最後的光柵化由 Engine 層來完成。


4)佈局

佈局時 Flutter 深度優先遍歷渲染對象樹。數據流的傳遞方式是從上到下傳遞約束,從下到上傳遞大小。也就是說,父節點會將自己的約束傳遞給子節點,子節點根據接收到的約束來計算自己的大小,然後將自己的尺寸返回給父節點。整個過程中,位置信息由父節點來控制,子節點並不關心自己所在的位置,而父節點也不關心子節點具體長什麼樣子。

當 Flutter 遇見 Web,會有怎樣的秘密?

為了防止因子節點發生變化而導致的整個控件樹重繪,Flutter 加入了一個機制——RelayoutBoundary,在一些特定的情形下 Relayout Boundary 會被自動創建,不需要開發者手動添加。

邊界:Flutter 使用邊界標記需要重新佈局和重新繪製的節點部分,這樣就可以避免其他節點被汙染或者觸發重建。就是控件大小不會影響其他控件時,就沒必要重新佈局整個控件樹。有了這個機制後,無論子樹發生什麼樣的變化,處理範圍都只在子樹上。

當 Flutter 遇見 Web,會有怎樣的秘密?

緩存:要提升性能表現,緩存也是少不了的。在 Flutter 中,幾乎所有的 Element 都會具有一個 key,這個 key 是唯一的。當子樹重建後,只會刷新 key 不同的部分,而節點數據的複用就是依靠 key 來從緩存中取得。

在確定每個空間的位置和大小之後,就進入繪製階段。繪製節點的時候也是深度遍歷繪製節點樹,然後把不同的 RenderObject 繪製到不同的圖層上。


5)繪製

在佈局完成之後,渲染對象樹中的每個節點都有了明確的尺寸和位置。Flutter 會把所有的 Element 繪製到不同的圖層上。與佈局過程類似,繪製的過程也是深度優先遍歷,先繪製父節點,然後繪製子節點。以下圖為例:節點 1、節點 2、節點 3、4、5,最好繪製節點 6。

當 Flutter 遇見 Web,會有怎樣的秘密?

如上圖可以看到一種場景,就是比如視圖可能會合並,導致 節點 2 的子節點 5 與它的兄弟節點 6 處於同一個圖層,這樣會導致當 節點 2 需要重繪的時候,與其無關的節點 6 也會被重繪,帶來性能問題。

為了解決上面的問題,Flutter 提出了佈局邊界的機制 ——重繪邊界(Repaint-Boundary)。在重繪邊界內,Flutter 會強制切換新的圖層,這樣可以避免邊界內外的互相影響,避免無關內容雖然處於同一個層級導致的不必要的重繪。

當 Flutter 遇見 Web,會有怎樣的秘密?

重繪邊界的一個典型場景就是 ScrollView。ScorllView 滾動的時候會刷新視圖,從而觸發內容重繪,而當滾動內容重繪時,一般情況下其它內容是不需要被重繪的。這個時候重繪邊界就非常有價值了。

這裡簡單理解,就是更精細化的對控件的更新,進行了小範圍的控制。在時間複雜度和空間複雜度中進行權衡。未來我們優化業務,大概率也會優化這裡,找到自身業務的平衡點。


6)合成和渲染

最上面已經展示了 Flutter 的 7 層渲染流水線(Renderingpipline)的圖裡。這裡主要描述一下對合成和渲染的理解。渲染流水線是由垂直同步信號(Vsync)驅動的。這個概念很類似我們平時說的 FPS 的概念,每秒 60 幀,過低的頻率會顯得頁面很卡。當每一次 Vsync 信號到來以後,Flutter 框架會按照圖裡的順序執行一系列動作:動畫(Animate)、構建(Build)、佈局(Layout)和繪製(Paint)

最終生成一個場景(Scene)之後送往底層,由 GPU 繪製到屏幕上。

當 Flutter 遇見 Web,會有怎樣的秘密?

Flutter App 只有在狀態發生變化的時候需要觸發渲染流水線。當你的 App 無任何狀態改變的時候,Flutter 是不需要重新渲染頁面的。所以,Vsync 信號需要 Flutter App 去調度。比如,我們在 Widget 內使用了 setState 方法改變了控件的狀態。

整個渲染流水線是運行在 UI 線程裡的,以 Vsync 信號為驅動,在框架渲染完成之後會輸出 Layer Tree。Layer Tree 被送入 Engine,Engine 會把 Layer Tree 調度到 GPU 線程,在 GPU 線程內合成(compsite)Layer Tree,然後由 Skia 2D 渲染引擎渲染後送入 GPU 顯示。這裡提到 Layer Tree 是因為我們即將要分析的渲染流水線繪製階段最終輸出就是這樣的 LayerTree。所以繪製階段並不是簡單的調用 Paint 函數這麼簡單了,而是很多地方都涉及到 Layer Tree 的管理。

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

這裡描述一下合成的概念,所謂合成就是因為我們繪製的頁面結構複雜,如果直接交付給繪圖引擎去進行圖層渲染,可能會出現大量的渲染內容重繪,因此,需要先進性一次圖層合成,就是說先把所有的圖層根據大小、層級等規則計算出最終的顯示效果,將相同的圖層合併,簡化渲染樹,提升渲染效率。

Flutter 會將合成之後的數據,交給 Skia 進行頁面二維圖層的渲染。


4、Widget 控件的更新策略

在這一個部分我們對比著 React 的設計方式對比著看一下 Flutter 的實現,在 React 中您可以看到三種很重要的名稱。JSX、Virtual Dom、真實 Dom,而在 Flutter 中我們依然可以看到對應的三類抽象的數據結構分別是 Widget、Element 和 RenderObject,他們的功能與 React 內三個數據抽象有異曲同工之處。Flutter 繪製界面的基礎是 Widget,也就是描述頁面的最小模塊。

Flutter 的核心設計思想就是 "一切皆 Widget":

前端同學可以把 Widget 理解為 Web Component 的 組件 即可。一種結構化數據的抽象,包含了組件的佈局、渲染屬性、事件響應信息等。


1)Widget 類似 React VM 的 F(x) = Y 中的 x 存在

Flutter 中的 Widget 是完全不可變的!只要當視圖發生變化,Flutter 就會重新創建一個新的 Widget 進行更新。即是 React 也是有一定的數據 Diff 的策略,而這裡變更即創建的方式,會帶來大量的銷燬和重建的過程,是否非常消耗性能?

Widget 對標的是 標識 React 的虛擬 DOM 節點的 數據描述 JSX,不是真實渲染的頁面 DOM。只是數據的抽象,不涉及視圖渲染。並且 Widget 具有不可變性,也提升了 Widget 本身的複用性。因此並沒大量的性能消耗,而 Dart 的作為靜態語言的運行速度,也會有著超越 JS 的性能。


2)Element 是 Widget 的一個實例化對象

Element 承載了視圖構建的上下文數據,是連接結構化的配置信息到完成最終渲染的橋樑;Element 是一個可變的數據結構, 可以大致理解為 Virtual DOM。可以進行 diff 更新;可以將真正需要修改的數據同步到 RenderObject 中。最大程度的降低渲染視圖的修改,提升渲染效率。


3)RenderObject 負責視圖渲染的對象

Flutter 的渲染分為 4 個部分。佈局、繪製、合成、渲染,其中 佈局和繪製是在 RenderObject 中完成的。Flutter 採用深度優的方式渲染對象樹,確定樹中的各個對象的位置和尺寸,並把它繪製到不同圖層, 繪製完成之後交給
Skia 在 VSync 信號同步時從渲染樹合成位圖,然後交給 CPU 進而完成上屏。


4)Widget 同樣分為有狀態 和 無狀態組件

無狀態控件 StatelessWidget 類似 React 的 PFC。有狀態控件 StatefulWidget 就是 React 的 組件。如同 react 組件一樣,使用有狀態組件是有成本的。正確的評估你的需求,避免使用無意義的有狀態組件。

這裡比較大的區別,是 Flutter 直接把 Widget 設計成為了一個不可變的! 這也導致了技術方案的實現上存在了差異。


5)既然看到了 Widget,那一定會有生命週期的存在

由於篇幅限制,這裡就不在詳細介紹了,簡單描述一下,生命週期,讓您有個影響。

創建:構造函數 --> initState --> didChangeDependencies --> build

更新: 主要由三個方法觸發:setState、didChangeDependencies 和 didUpdateWidget。
銷燬: 系統會調用 diactivate 和 dispose 這兩個方法,來移除或銷燬組件。

這裡多了 APP 生命週期的概念,在傳統 Web 我們相對關注較少

從後臺切入前臺:paused -> inactive -> resumed;
從前臺退到後臺:resumed-> inactive-> paused;


5、學習路線

當 Flutter 遇見 Web,會有怎樣的秘密?

粗略了整理了一下我最近這 2 周體驗開發過程中認知的學習範圍,這上面除了跟 APP 相關的部分,大部分場景已經通過代碼體驗和實踐過了。這裡由於篇幅限制,就不在一一的介紹了。最重要的是關注 Flutter 的官方文檔。

其次,找了三個實例,跟您分享,您可以 clone 下來,更細節的體驗一下:

佈局案例:https://github.com/yang7229693/flutter-study

代碼實例: https://github.com/nisrulz/flutter-examples

FlutterDemo: https://github.com/OpenFlutter/Flutter-Notebook

除了上面列出的這些,還有很多要做,比如 運維、調試、自動化測試、兼容性、客戶端 SDK 封裝、國際化等等。當然對於開發者來說,工程化、調試、組件化都是非常重要的實踐內容。


6、引文

  • Flutter中文網:https://flutterchina.club/
  • 閒魚技術:https://www.yuque.com/xytech/flutter


7、總結

這裡再次感謝前人的沉澱,確實要學習的東西太多了,實際想要寫的文章,遠比這裡面描述要多很多,但是由於時間成本過高,不得不砍掉了非常多的內容。後面隨著對 Flutter 更深入的瞭解,有機會再跟您更詳細的分享 Flutter 的內部設計原理。我只是知識的搬運工,在應用層領域作為開發,最大的價值就是服務好產品,最大限度的用技術滿足產品訴求。至於核心的底層開發建設,人生能做到什麼程度,要看緣分;但是最大限度的服務好業務,只要有責任心就可以了。


分享到:


相關文章: