1、前言
本篇文章主要介紹Flutter 渲染框架及其渲染過程
Flutter是谷歌的移動UI框架,在此之前也有類似ReactNative、Weex等跨端方案,Flutter在一定程度上借鑑了ReactNative的思想,採用三棵樹 其中element tree diff管理,來觸發renderTree的重新整理,並且不同於android這種命令式檢視開發,採用了宣告式,下面將一一介紹。
2、程式設計正規化的改變
在Android檢視開發中是命令式的,view大多數都是在xml宣告,開發者然後通過id找出view,資料更新時,仍需要開發者關注需要變化的view,再呼叫方法比如 setText之類的使其發生改變;
但是在Flutter中檢視的開發是宣告式的,開發者需要維護好一套資料集合以及繫結好widgetTree,這樣後面資料變化時候widget會根據資料來渲染,開發者就不再關注每個元件,關心核心資料即可。
3、Flutter 渲染框架介紹
Flutter的渲染框架分為Framework和Engine兩層,應用是基於Framework層開發,其中
- Framework層負責渲染中的Build、Layout、Paint、生成Layer等環節,使用Dart語言
- Engine層是C++實現的渲染引擎,負責把Framework生成的Layer組合,生成紋理,然後通過OpenGL介面向GPU提交渲染資料
該跨平臺應用框架沒有使用webview或者平臺自帶的元件,使用自身的高效能渲染引擎Skia 自繪,元件之間可以任意組合
4、檢視樹
flutter中通過各種各樣的widget組合使用,檢視樹中包含了以下三種樹 Widget、Element、RenderObject,對應關係如下
- Widget:存放渲染內容、檢視佈局資訊,widget的屬性最好都是immutable
- Element:存放上下文,通過Element遍歷檢視樹,Element同時持有Widget和RenderObject(BuilderOwner)
- RenderObject:根據Widget的佈局屬性進行layout,paint Widget傳人的內容(PipeLineOwner)
通常 我們建立widget樹,然後呼叫runApp(rootWidget),將rootWidget傳給rootElement,作為rootElement的子節點,生成Element樹,由Element樹生成Render樹
widget是immutable,資料變化會重繪,如何避免資源消耗
Flutter介面開發是一種響應式的程式設計,當資料發生變化時通知到可變更的節點(statefullWidget或者rootwidget),但是每次資料變更,都會觸發widgetTree的重繪,由於widget只是持有一些渲染的配置資訊而已,不是真正觸發渲染的物件,非常輕量級,flutter團隊對widget的建立、銷燬做了優化,不用擔心整個widget樹重新建立帶來的效能問題。RenderObject才是真正渲染時使用,涉及到layout、paint等複雜操作,是一個真正渲染的view,二者被Element Tree持有,ElementTree通過Diff 演算法來將不斷變化的widget轉變為相對穩定的RenderObject。
當我們不斷改變widget時,BuilderOwner收到widgetTree會與之前的widgetTree作對比,在ElementTree上只更新變化的部分,當Elment變化之後 與之對應的RenderObject也就更新了,如下圖所示
其中 PipelineOwner類似於Android中的ViewRootImpl,管理著真正需要繪製的View,
最後PipelineOwner會對RenderObjectTree中發生變化節點的進行layout、paint、合成等等操作,最後交給底層引擎渲染。
Widget、Element、RenderObject之間的關係
在介紹Elment Tree的Diff規則之前,先介紹下,這三者之前的關係,之前也大致提到 Elment Tree持有了Element同時持有Widget和RenderObject(BuilderOwner),我們先從程式碼入手
可以看出 Widget抽象類有3個關鍵能力
- 保證自身唯一性的key
- 建立Element的create
- canUpdate
從上面類圖也可以看出,**Element和RenderObject都是由Widget建立出來,**也並不是每一個Widget都有與之對應的RenderObject
Widget、Element、RenderObject 的第一次建立與關聯
在Android中ViewTree
-PhoneWindow
- DecorView
- TitleView
- ContentView
複製程式碼
而在Flutter中則比較簡單,只有底層的root widget
- RenderObjectToWidgetAdapter<RenderBox>
- MyApp (自定義)
- MyMaterialApp (自定義)
複製程式碼
其中RenderObjectToWidgetAdapter 也是一個renderObjectWidget,通過註釋可以發現它是runApp啟動時“A bridge from a [RenderObject] to an [Element] tree.”
runApp程式碼
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
複製程式碼
WidgetsFlutterBinding 初始化了一系列的Binding,這些Binding持有了我們上面說的一些owner,比如BuildOwner,PipelineOwner,所以隨著WidgetsFlutterBinding的初始化,其他的Binding也被初始化了,
GestureBinding | 提供了 window.onPointerDataPacket 回撥,繫結 Framework 手勢子系統,是 Framework 事件模型與底層事件的繫結入口 |
---|---|
ServicesBinding | 提供了 window.onPlatformMessage 回撥, 用於繫結平臺訊息通道(message channel),主要處理原生和 Flutter 通訊 |
SchedulerBinding | 提供了 window.onBeginFrame 和 window.onDrawFrame 回撥,監聽重新整理事件,繫結 Framework 繪製排程子系統 |
PaintingBinding | 繫結繪製庫,主要用於處理圖片快取 |
SemanticsBinding | 語義化層與 Flutter engine 的橋樑,主要是輔助功能的底層支援 |
RendererBinding | 提供了 window.onMetricsChanged 、window.onTextScaleFactorChanged 等回撥。它是渲染樹與 Flutter engine 的橋樑 |
WidgetsBinding | 提供了 window.onLocaleChanged、onBuildScheduled 等回撥。它是 Flutter widget 層與 engine 的橋樑 |
繼續跟進下attachRootWidget(app)
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement);
}
複製程式碼
內部建立了 RenderObjectToWidgetAdapter 並將我們傳入的app 自定義widget做了child,接著執行attachToRenderTree這個方法,建立了第一個Element和RenderObject
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
if (element == null) {
owner.lockState(() {
element = createElement(); //建立rootElement
element.assignOwner(owner); //繫結BuildOwner
});
owner.buildScope(element, () { //子widget的初始化從這裡開始
element.mount(null, null); // 初始化子Widget前,先執行rootElement的mount方法
});
} else {
...
}
return element;
}
複製程式碼
我們解釋一下上面的圖片,Root的建立比較簡單:
- 1.
attachRootWidget(app)
方法建立了Root[Widget](也就是 RenderObjectToWidgetAdapter) - 2.緊接著呼叫
attachToRenderTree
方法建立了 Root[Element] - 3.Root[Element]嘗試呼叫
mount
方法將自己掛載到父Element上,因為自己就是root了,所以沒有父Element,掛空了 - 4.mount的過程中會呼叫Widget的
createRenderObject
,建立了 Root[RenderObject]
它的child,也就是我們傳入的app是怎麼掛載父控制元件上的呢?
- 5.我們將app作為Root[Widget](也就是 RenderObjectToWidgetAdapter),appp[Widget]也就成了為root[Widget]的child[Widget]
- 6.呼叫
owner.buildScope
,開始執行子Tree的建立以及掛載,敲黑板!!!這中間的流程和WidgetTree的重新整理流程是一模一樣的,詳細流程我們後面講! - 7.呼叫
createElement
方法建立出Child[Element] - 8.呼叫Element的
mount
方法,將自己掛載到Root[Element]上,形成一棵樹 - 9.掛載的同時,呼叫
widget.createRenderObject
,建立Child[RenderObject] - 10.建立完成後,呼叫
attachRenderObject
,完成和Root[RenderObject]的連結
就這樣,WidgetTree、ElementTree、RenderObject建立完成,並有各自的連結關係。
這裡有兩個操作需要注意下,
mount
abstract class Element:
void mount(Element parent, dynamic newSlot) {
_parent = parent; //持有父Element的引用
_slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1;//當前節點的深度
_active = true;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner; //每個Element的buildOwner,都來自父類的BuildOwner
...
}
複製程式碼
我們先看一下Element的掛載,就是讓_parent
持有父Element的引用,因為RootElement 是沒有父Element的,所以引數傳了null:element.mount(null, null);
還有兩個值得注意的地方:
- 節點的深度_depth 也是在這個時候計算的,深度對重新整理很重要
- 每個Element的buildOwner,都來自父類的BuildOwner,這樣可以保證一個ElementTree,只由一個BuildOwner來維護。
RenderObjectElement
abstract class RenderObjectElement:
@override
void attachRenderObject(dynamic newSlot) {
...
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
...
}
複製程式碼
RenderObject與父RenderObject的掛載稍微複雜了點。通過程式碼我們可以看到需要先查詢一下自己的AncestorRenderObject
,這是為什麼呢?
還記得之前我們講過,每一個Widget都有一個對應的Element,但Element不一定會有對應的RenderObject。所以你的父Element並不一有RenderObject,這個時候就需要向上查詢。
RenderObjectElement _findAncestorRenderObjectElement() {
Element ancestor = _parent;
while (ancestor != null && ancestor is! RenderObjectElement)
ancestor = ancestor._parent;
return ancestor;
}
複製程式碼
通過程式碼我們也可以看到,find方法在向上遍歷Element,直到找到RenderObjectElement,RenderObjectElement肯定是有對應的RenderObject了,這個時候在進行RenderObject子父間的掛載。
5、渲染過程
當需要更新UI的時候,Framework通知Engine,Engine會等到下個Vsync訊號到達的時候,會通知Framework,然後Framework會進行animations,
build,layout,compositing,paint,最後生成layer提交給Engine。Engine會把layer進行組合,生成紋理,最後通過Open Gl介面提交資料給GPU,
GPU經過處理後在顯示器上面顯示。整個流程如下圖:
6、渲染觸發 (setState)
setState背後發生了什麼
在Flutter開發應用的時候,當需要更新的UI的時候,需要呼叫一下setState方法,然後就可以實現了UI的更新,我們接下來分析一下該方法做哪些事情。
void setState(VoidCallback fn) {
...
_element.markNeedsBuild(); //通過相應的element來實現更新,關於element,widget,renderOjbect這裡不展開討論
}
複製程式碼
繼續追蹤
void markNeedsBuild() {
...
if (dirty)
return;
_dirty = true;
owner.scheduleBuildFor(this);
}
複製程式碼
widget對應的element將自身標記為dirty
狀態,並呼叫owner.scheduleBuildFor(this);
通知buildOwner進行處理
void scheduleBuildFor(Element element) {
...
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled(); //這是一個callback,呼叫的方法是下面的_handleBuildScheduled
}
_dirtyElements.add(element); //把當前element新增到_dirtyElements陣列裡面,後面重新build會遍歷這個陣列
element._inDirtyList = true;
}
複製程式碼
後續MyStatefulWidget的build方法一定會被執行,執行後,會建立新的子Widget出來,原來的子Widget便被拋棄掉了,原來的子Widget肯定是沒救了,但他們的Element大概率還是有救的,此時 buildOwner會將所有dirty的Element新增到_dirtyElements當中
經過Framework一連串的呼叫後,最終呼叫scheduleFrame來通知Engine需要更新UI,Engine就會在下個vSync到達的時候通過呼叫_drawFrame來通知Framework,然後Framework就會通過BuildOwner進行Build和PipelineOwner進行Layout,Paint,最後把生成Layer,組合成Scene提交給Engine。
底層引擎最終回到Dart層,並執行buildOwner的buildScope方法,首先從Engine回撥Framework的入口開始。
void _drawFrame() { //Engine回撥Framework入口
_invoke(window.onDrawFrame, window._onDrawFrameZone);
}
//初始化的時候把onDrawFrame設定為_handleDrawFrame
void initInstances() {
super.initInstances();
_instance = this;
ui.window.onBeginFrame = _handleBeginFrame;
ui.window.onDrawFrame = _handleDrawFrame;
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
}
void _handleDrawFrame() {
if (_ignoreNextEngineDrawFrame) {
_ignoreNextEngineDrawFrame = false;
return;
}
handleDrawFrame();
}
void handleDrawFrame() {
_schedulerPhase = SchedulerPhase.persistentCallbacks;//記錄當前更新UI的狀態
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
}
}
void initInstances() {
....
addPersistentFrameCallback(_handlePersistentFrameCallback);
}
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
}
void drawFrame() {
...
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement); //先重新build widget
super.drawFrame();
buildOwner.finalizeTree();
}
複製程式碼
核心方法 buildScope
void buildScope(Element context, [VoidCallback callback]){
...
}
複製程式碼
需要傳入一個Element的引數,這個方法通過字面意思應該理解就是對這個Element以下範圍rebuild
void buildScope(Element context, [VoidCallback callback]) {
...
try {
...
_dirtyElements.sort(Element._sort); //1.排序
...
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
try {
_dirtyElements[index].rebuild(); //2.遍歷rebuild
} catch (e, stack) {
}
index += 1;
}
} finally {
for (Element element in _dirtyElements) {
element._inDirtyList = false;
}
_dirtyElements.clear(); //3.清空
...
}
}
複製程式碼
這裡對上面方法做下解釋
- 第1步:按照Element的深度從小到大,對_dirtyElements進行排序
由於父Widget的build方法必然會觸發子Widget的build,如果先build了子Widget,後面再build父Widget時,子Widget又要被build一次。所以這樣排序之後,可以避免子Widget的重複build。
- 第2步:遍歷執行_dirtyElements當中element的rebuild方法
值得一提的是,遍歷執行的過程中,也有可能會有新的element被加入到_dirtyElements集合中,此時會根據dirtyElements集合的長度判斷是否有新的元素進來了,如果有,就重新排序。
element的rebuild方法最終會呼叫
performRebuild()
,而performRebuild()
不同的Element有不同的實現
- 第3步:遍歷結束之後,清空dirtyElements集合
因此setState()過程主要工作是記錄所有的髒元素,新增到BuildOwner物件的_dirtyElements成員變數,然後呼叫scheduleFrame來註冊Vsync回撥。 當下一次vsync訊號的到來時會執行handleBeginFrame()和handleDrawFrame()來更新UI。
Element的Diff
在上面的第二步會遍歷執行element的build方法
_dirtyElements[index].rebuild(); //2.遍歷rebuild
element的rebuild方法最終會呼叫performRebuild()
,而performRebuild()
不同的Element有不同的實現,以下面兩個為例
ComponentElement的performRebuild()
void performRebuild() {
Widget built;
try {
built = build();
}
...
try {
_child = updateChild(_child, built, slot);
}
...
}
複製程式碼
執行element的build();
,以StatefulElement的build方法為例:Widget build() => state.build(this);
。 就是執行了我們複寫的StatefulWidget的state的build方法,此時建立出來的當然就是這個StatefulWidget的子Widget了
下面看下核心方法 Element updateChild(Element child, Widget newWidget, dynamic newSlot)
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
...
//1
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
if (child != null) {
//2
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
//3
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
return child;
}
deactivateChild(child);
}
//4
return inflateWidget(newWidget, newSlot);
}
複製程式碼
引數child 是上一次Element掛載的child Element, newWidget 是剛剛build出來的。updateChild有四種可能的情況
- 1.如果剛build出來的widget等於null,說明這個控制元件被刪除了,child Element可以被刪除了。
- 2.如果child的widget和新build出來的一樣(Widget複用了),就看下位置一樣不,不一樣就更新下,一樣就直接return了。Element還是舊的Element
- 3.看下Widget是否可以update,
Widget.canUpdate
的邏輯是判斷key值和執行時型別是否相等。如果滿足條件的話,就更新,並返回。
中間商的差價哪來的呢?只要新build出來的Widget和上一次的型別和Key值相同,Element就會被複用!由此也就保證了雖然Widget在不停的新建,但只要不發生大的變化,那Element是相對穩定的,也就保證了RenderObject是穩定的!
- 4.如果上述三個條件都沒有滿足的話,就呼叫
inflateWidget()
建立新的Element
這裡再看下inflateWidget()
方法:
Element inflateWidget(Widget newWidget, dynamic newSlot) {
final Key key = newWidget.key;
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild;
}
}
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
}
複製程式碼
首先會嘗試通過GlobalKey去查詢可複用的Element,複用失敗就呼叫Widget的方法建立新的Element,然後呼叫mount方法,將自己掛載到父Element上去,mount之前我們也講過,會在這個方法裡建立新的RenderObject。
RenderObjectElement的performRebuild()
@override
void performRebuild() {
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
複製程式碼
與ComponentElement的不同之處在於,沒有去build,而是呼叫了updateRenderObject
方法更新RenderObject。到這裡我們基本就明白了Element是如何在中間應對Widget的多變,保障RenderObject的相對不變了