我保證這是渲染樹系列最後一篇了,我們將走訪完MyTimer的渲染和更新,看完你就會成為全台灣少數幾個真正瞭解三顆渲染樹運作方式的人了。還在看的人撐下去啊,終點就在眼前了!
回顧一下目前的渲染樹:
我們上次進行到SingleChildRenderObjectElement的mount,而Element mount時主要會發生四件事
我們現在進行到4.,這次的Widget是MyTimer,同樣經過updateChild->inflateWidget->createElement,建立了StatefulElement。值得注意的是StatefulElement在建立的同時,就在Constructor一併建立了State進行管理:
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget)
此時的渲染樹:
StatefulElement是ComponentElement,因此進行3.,呼叫MyTimer的build,產生Padding(child: RichText(...)):
最後這兩個Widget都是RenderObjectWidget,流程都一樣我們就不重複了,直接快轉到建立完成:
好了終於要來更新Widget Tree了,我們來看看setState時發生了什麼事:
// In State
@protected
void setState(VoidCallback fn) {
....
final dynamic result = fn() as dynamic;
....
_element.markNeedsBuild();
}
// In Element
void markNeedsBuild() {
....
_dirty = true;
owner.scheduleBuildFor(this);
}
// In BuildOwner
void scheduleBuildFor(Element element) {
....
_dirtyElements.add(element);
element._inDirtyList = true;
....
}
這裡三層函數呼叫都有很長的例外處理,實際上的重點就是把Element設為dirty並加入dirty Element list,所有被加入的Element將在下一個Frame被更新。這裡渲染下一個Frame的呼叫根源來自Flutter Engine,再此先不細談,總之經過一連串呼叫後來到BuildOwner.buildScope:
void buildScope(Element context, [ VoidCallback callback ]) {
....
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
....
try {
_dirtyElements[index].rebuild();
} catch (e, stack) {
....
}
index += 1
....
}
....
}
同樣很多例外處理,但基本上就是while loop呼叫每個dirtyElement的rebuild。接著經過rebuild->performRebuild->build來到我們的MyTimer的build函數。build完再次呼叫updateChild,有趣的事終於要發生了:
// In Element
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
....
if (hasSameSuperclass && child.widget == newWidget) { // 1.
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {//2.
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget); // 3.
....
newChild = child;
} else {
deactivateChild(child);
assert(child._parent == null);
newChild = inflateWidget(newWidget, newSlot); // 4.
}
....
return newChild;
}
這次我們已經有child Element存在,也有新的Widget進來,因此我們要判斷
這邊2.的判斷就是鼎鼎大名的canUpdate,如果你曾經看過任何一篇關於渲染樹的文章或影片,應該都會看到它。如果你還沒看過,現在就讓你看看。
// In Widget
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
code本身就很簡單,再翻成白話就是,如果舊widget和新widget是同一類型,而且它們的key也是同一個(或都是null),就不需要重建Element,直接更新Element就好。
我們上次說updateChild是整個Widget系統的邏輯核心,而這個canUpdate正可以說是核心中的核心。如果看完這整個系列只記得一件事,請務必記得它。
以我們的範例來說舊和新widget都是Padding,也都沒有設key,因此canUpdate==true,而我們進入了3.,也就是child.update(newWidget)。
這時候child是SingleChildRenderObjectElement <- RenderObjectElement <- Element,因此update有這樣的call stack:
// In Element
void update(covariant Widget newWidget) {
....
_widget = newWidget;
}
// In RenderObjectElement
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
....
widget.updateRenderObject(this, renderObject);
....
_dirty = false;
}
// In SingleChildRenderObjectElement
void update(SingleChildRenderObjectWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_child = updateChild(_child, widget.child, null);
}
首先將自己的widget reference指向新widget,然後用新widget去更新renderObject,這裡Padding的updateRenderObject函數如下:
void updateRenderObject(BuildContext context, RenderPadding renderObject) {
renderObject
..padding = padding
..textDirection = Directionality.of(context);
}
最後因為是SingleChildRenderObjectElement,再次呼叫updateChild進行下一層RichText的更新。後續的更新流程就跟Padding一模一樣了,這邊就不再覆述。
終於結束了!稍微整理一下吧,我們一開始講到Widget, Element, RenderObject三種class各自的職責和相互關係,接著介紹了這三種class的各種subclass和它們之間的對應關係,最後透過實際走訪範例APP從啟動、建立渲染樹到更新渲染樹的整個流程,進一步了解了三顆渲染樹的運作細節。當然,這些其實還不是全部,但我想對任何心智正常的人來說,瞭解到這邊已經非常足夠了。如果你能從第一篇看到最後一篇還能保持神智清醒,現在請給自己一個愛的鼓勵,台灣Flutter社群的未來就靠你了!