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:
Widget是 Flutter中控制項實現的基本單位,其意義類似於 Cocoa Touch中的 UIView。Widget裡面存儲了一個視圖的配置信息,包括布局、屬性等待。所以它只是一份輕量的,可直接使用的數據結構。在構建為結構樹,甚至重新創建和銷毀結構樹時都不存在明顯的性能問題。
Element:
Element是 Widget的抽象,它其實承載了視圖構建的上下文數據。構建系統通過遍歷 Element樹來構建 RenderObject數據,比如視圖更新時,只會標記 dirty Element,而不會標記 dirty Widget。所以 Widget「無狀態」,而 Element「有狀態」 (這個狀態指框架層的構建狀態)。
RenderObject:
在 RenderObject樹中會發生 Layout、Paint的繪製事件,所以 Flutter中大部分的繪圖性能優化發生在這裡。RenderObject樹構建的數據會被加入到 Engine所需的 LayerTree中,Engine通過 LayerTree進行視圖合成並光柵化,提交給 GPU。
Flutter通過這三種概念,把原本比較複雜的視圖樹狀態、數據的維護和構建拆分得更單一、易於集中治理。
視圖數據的構建
這個話題其實比較大,涉及到的邏輯也很多。我們可以先從應用入口開始看起。
應用的 root view
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 的渲染機制可以當做一本教科書,本文只能管中窺豹了
掛載 LayerTree
的抽象在 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通信
但除了手勢這種特定的輸入,還有其他感測器和事件的傳遞被抽象得更通用,叫做 。這樣就可以避免用戶每次需要開發一個 Native extension時都要寫 Native Binding了。 MethodChannel的原理和手勢的傳遞幾乎是一樣的,但是 API會更通用一點。
在這篇教程中 PlatformChannels示例了一個如何通過 Native 獲取電池電量,並通過 PlatformChannel傳遞給 Flutter應用。而這個 PlatformChannel像手勢數據分發那樣,換了一個方法:
通過這種打開一個 Channel來進行 request/response 的抽象概念可以實現任何用戶自定義的 extension。
The End
Flutter是 engine和 Dart Framework的統稱,目前 Flutter還處於 beta階段,雖然說大致原理不會變,但一些細節部分還是會有不少改動。本文只能看個大概,因為,信息量實在是太大了。
好了,原理分析告一段落,在後面的使用中,我們再來細細探討 Flutter的一些設計理念。
![](https://pic.pimg.tw/zzuyanan/1488615166-1259157397.png)
![](https://pic.pimg.tw/zzuyanan/1482887990-2595557020.jpg)
TAG:stephenw |