當前位置:
首頁 > 最新 > Flutter Dart Framework原理簡解

Flutter Dart Framework原理簡解

從前一篇文章Flutter原理的分析可知,Flutter Engine向Framework層提供了通用的繪圖能力,Flutter渲染 UI的本質就是在 VSync信號間儘快地構建並提供視圖數據。所以 Flutter整個框架的精髓在 Dart UI Framework(更準確地叫作 Flutter Dart Framework)里,這裡面涉及了非常多的設計思想、優化措施,所以一篇文章是無法概括的。 所以在本篇文章中,我們只能概要性地分析一下 Flutter Dart Framework的大致原理。


UI框架繪圖的基本流程

在開始本文前,我們還是要了解一下通用的 UI框架繪圖的流程,以便我們理解 Flutter的 Dart Framework:

從上圖中我們可以看到,用戶的輸入才是驅動視圖更新的信號。當這個信號發生後,首先需要觸發的就是動畫的進度更新,框架需要開始視圖數據的「build」(也可以看做數據的填充)。

在這之後,視圖才會進行布局,計算各個部分的大小,然後進行「paint」,生成每個視圖的視覺數據。生成的視覺數據並不能直接用,因為往往視圖層級非常多, 這些數據直接向 GPU傳遞很低效,所以接下來需要進行視圖合成,將多個視圖數據合成為一個。

最後一步進行「光柵化」,前一步得到合成的視圖數據其實還是一份矢量描述數據,光柵化幫助把這份數據真正地生成一個一個的像素填充數據。在 Flutter中,光柵化這個步驟被放在了 Engine中,但是我們前文並沒有提到。

在了解 UI框架的通用流程後,我們自然而然能知道 Flutter Dart Framework這一層肯定做了以下這些事:

視圖的數據結構(視圖樹)

視圖數據的構建

視圖布局的計算

視圖的合成

與 Engine的數據同步和通信


視圖的結構

無論是比較底層的 UI框架(如 Cocoa、WebKit)還是比較上層的 (React),在向繪製引擎提供視圖數據都需要一份結構化的視圖數據,俗稱為「視圖樹」(View Tree)。Flutter 的視圖結構的抽象分為三部分:, , 。

三部分?在哪熟悉是不是? Cocoa Touch中,視圖樹被分為 模型樹、呈現樹、渲染樹, 在 Flutter中你也可以看待這三部分的意義。

Widget是 Flutter中控制項實現的基本單位,其意義類似於 Cocoa Touch中的 UIView。Widget裡面存儲了一個視圖的配置信息,包括布局、屬性等待。所以它只是一份輕量的,可直接使用的數據結構。在構建為結構樹,甚至重新創建和銷毀結構樹時都不存在明顯的性能問題。


Element是 Widget的抽象,它其實承載了視圖構建的上下文數據。構建系統通過遍歷 Element樹來構建 RenderObject數據,比如視圖更新時,只會標記 dirty Element,而不會標記 dirty Widget。所以 Widget「無狀態」,而 Element「有狀態」 (這個狀態指框架層的構建狀態)。


在 RenderObject樹中會發生 Layout、Paint的繪製事件,所以 Flutter中大部分的繪圖性能優化發生在這裡。RenderObject樹構建的數據會被加入到 Engine所需的 LayerTree中,Engine通過 LayerTree進行視圖合成並光柵化,提交給 GPU。

Flutter通過這三種概念,把原本比較複雜的視圖樹狀態、數據的維護和構建拆分得更單一、易於集中治理。


視圖數據的構建

這個話題其實比較大,涉及到的邏輯也很多。我們可以先從應用入口開始看起。


Flutter App 入口的部分發生於如下代碼:

函數接受一個 Widget類型的對象作為參數,也就是說在 Flutter的概念中,只存在 View,而其他的任何邏輯都只為 View的數據、狀態改變服務,不存在 ViewController(或者叫 Activity)。

接下來看 做了什麼:

在中,傳入的 widget被掛載到根 widget上。這個其實是一個單例,通過 mixin來使用框架中實現的其他 binding的 Service,比如 手勢、基礎服務、隊列、繪圖等等。然後會調用這個方法,從這個方法注釋可知,調用這個方法會主動構建視圖數據。這樣做的好處是因為 Flutter依賴 Dart的 MicroTask來進行幀數據構建任務的 schedule,這裡通過主動調用進行整個周期的 「熱身」,這樣最近的下次 VSync信號同步時就有視圖數據可提供,而不用等到 MicroTask的 next Tick。

然後我們再來看 這個函數幹了什麼:

把 widget交給了 這座橋樑,通過這座橋樑,Element被創建,並且同時能持有 Widget和 RenderObject的引用。然後我們從上文就知道後面發生的就是第一次的視圖數據構建了。

從這一部分能印證前面所說:Flutter應用通過 、、 三種樹結構來維護整個應用的視圖數據。


VSync信號其實在 Flutter Dart Framework層是透明的,視圖的數據構建和更新並不是由 VSync信號驅動的。我在前文說道:Flutter並不關心顯示器、視頻控制器以及 GPU具體工作,它只關心 GPU發出的 VSync信號,儘可能快地在兩個 VSync信號之間計算併合成視圖數據,並且把數據提供給 GPU。這個說法其實是不準確的,準確的說法其實應該是:

Flutter 並不關心顯示器、視頻控制器以及 GPU具體工作,它只關心能在 VSync信號間隔時間內儘快地合成視圖數據,等待 VSync信號同步來獲取這個視圖數據提供給 GPU。

那麼 Flutter是在什麼時機構建視圖數據的呢?前面就已經揭曉過答案:MicroTask 循環。

是不是和 Cocoa Touch很像?NSRunloop 的 main loop 也是通過事件循環來執行固定的視圖更新回調

有一個抽象的 ,它並不負責向 LayerTree提供數據,但在 Flutter中,它承擔了管理和回調視圖更新方法的重擔。在 rootView(rootElement)被創建時,ScheduleBinding便會調用 ,這會調用 ,而在 window 的 postFrameCallback 中,會再次被調用,這樣就形成了循環。

通過這種有序的周期循環,Flutter能源源不斷地提供幀視圖數據,VSync信號同步時便能獲得這個數據進行 Engine部分後續的操作。


前面提到,視圖樹被分為三類: 、、,Element同時持有 Widget和 RenderObject的引用。這是本段的大前提。

Widget是用戶能夠直接操作的數據結構,Flutter的設計理念中,Widget都是 immutable的,所以 Widget節點的改變其實就是節點的銷毀和重新創建。但如果只是最基本的 Widget手動組合的話,寫起來會很蛋疼,所以 Flutter使用 State來控制 Widget的創建與銷毀,以此來達到響應式 UI編程的目的。

細細品味,Widget的設計理念有沒有一點和 React像?

Widget在更新後,Element持有該 Widget的節點也會被標記為 dirtyElement,那麼在下一個周期的 drawFrame時,Element樹的這一部分子樹便會被觸發 performRebuild。在 Element樹更新完成後,便能獲得 RenderObject樹,接下來便會進入 Layout和 Paint的流程。


視圖的布局與繪製

在 Flutter中,視圖的布局與合成是整個 UI框架中最重要的部分,這部分的好壞決定了繪圖的性能以及框架的應用表現,所以 Google有一個專門的 TechTalk來講解這個原理:Flutter"s renderding pipeline


要獲得每個 Widget的真實視圖數據,布局是第一步。布局可以計算出每個節點所佔空間的真實大小。在構建視圖樹時,節點的 size約束是從樹頂部向底部的,而在計算布局的時候,遍歷樹是深度優先,所以獲得布局數據的順序是自下而上的。和 Web一樣,Flutter的布局也是基於盒子模型,並且參考了眾多布局方案設計而成 (畢竟負責這一部分的 Adam Barth也是 Blink項目的核心開發者),這部分的核心設計比較複雜,我們完全可以另開一篇文章,這裡不再展開。


繪製決定了一個視圖節點的真實外觀。和布局不太一樣的是繪製的順序,布局會優先計運算元節點,而繪製會優先繪製父節點。所以在數據流上看起來兩個流程的方向是相反的。

繪製要做的事就是計算出一個 Layer的外觀,這通常都不簡單,因為開發者經常會在視圖上放各種各樣的控制項,並且他們的層級也不一樣,所以繪製的步驟要做的事情就是決定一個 Layer的某一部分長什麼樣,這也可以看做視圖的局部合成。舉個例子:

在進行繪製時,每個節點都會先繪製自身,其次才是子節點。比如節點 2是一個背景色綠色的視圖,在繪製完自身後,繪製子節點 3和 4,它們可能具有 alpha屬性,所以繪製後當布局重疊時會合成紅色的節點 5。所以從數據流方向看的時候,獲得最終的 Layer的順序反而是自下而上的。

一個 Layer是一份矢量數據,掛載到 LayerTree後還會經過 Engine的合成和光柵化才能提交給 GPU。RenderObject並不是和 Layer一一對應的,所以需要 paint過程將 RenderObject轉化為 Layer


視圖樹會不斷更新,這就意味著布局和繪製是不間斷的。我們上面提到的布局和繪製只是九牛一毛,Flutter其實在這方面做了很多事情,不間斷的布局和繪製肯定會非常耗費性能,所以 Flutter也有對應的優化方案(在 Flutter的布局和繪製演算法足夠優秀的前提下)。

邊界:Flutter使用邊界標記需要重新布局和重新繪製的節點部分,這樣就可以避免其他節點被污染或者觸發重建,這個邊界被分別叫做 Relayout boundary 和 Repaint boundary。

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

Flutter 的渲染機制可以當做一本教科書,本文只能管中窺豹了


的抽象在 Flutter中是真實存在的,在經過 paint生成 Layer後,會通過 的 掛載到 LayerTree。

這是一個抽象介面,不同的 Layer(如 ContainerLayer、TextureLayer等)在繼承抽象類後自行處理邏輯,但無外乎都會最終調用 這類方法,把傳遞的 Layer真正地掛載上去。

接下來的事情上一篇文章也提過了,Skia在 VSync信號同步時直接從 LayerTree合成 Bitmap,(經過光柵化後)提交給 GPU


Flutter 的通信機制

讀完上面大概能了解到 Flutter怎麼通過固定的數據結構構建並渲染豐富多彩的視圖。但文章開頭說過,用戶輸入才是驅動視圖更新的源泉,所以需要專門討論一下 Flutter是如何獲得用戶輸入並與平台通信的。


在當今的移動設備上感測器有很多,屏幕輸入也可以算作一種。所以這裡以討論手勢輸入為例。

Flutter通過 Native方法捕捉用戶的屏幕輸入,這些數據被封裝成一個一個的數據包,通過 Dart的 runtime hook發送給 中的 方法。這個方法會直接將屏幕輸入數據分發給 ,所以在 Flutter中,我們可以將觸摸時間看作發生在全局的 上。

Flutter在接收到數據包後,識別觸摸點發生的坐標,通過數學計算來識別手勢。這和 Native框架的邏輯幾乎是一樣的。


但除了手勢這種特定的輸入,還有其他感測器和事件的傳遞被抽象得更通用,叫做 。這樣就可以避免用戶每次需要開發一個 Native extension時都要寫 Native Binding了。 MethodChannel的原理和手勢的傳遞幾乎是一樣的,但是 API會更通用一點。

在這篇教程中 PlatformChannels示例了一個如何通過 Native 獲取電池電量,並通過 PlatformChannel傳遞給 Flutter應用。而這個 PlatformChannel像手勢數據分發那樣,換了一個方法:

通過這種打開一個 Channel來進行 request/response 的抽象概念可以實現任何用戶自定義的 extension。

The End

Flutter是 engine和 Dart Framework的統稱,目前 Flutter還處於 beta階段,雖然說大致原理不會變,但一些細節部分還是會有不少改動。本文只能看個大概,因為,信息量實在是太大了。

好了,原理分析告一段落,在後面的使用中,我們再來細細探討 Flutter的一些設計理念。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 stephenw 的精彩文章:

Flutter 原理簡解

TAG:stephenw |