接下來我們就要來實際走訪一次整個渲染流程,看看Flutter App是怎麼啟動,三顆渲染樹是怎麼從無到有被建立起來,又是怎麼更新的。
首先介紹一下這次要使用的範例Widget Tree:
void main() {
runApp(Container(
alignment: Alignment.topCenter,
child: MyTimer(),
));
}
class MyTimer extends StatefulWidget {
@override
_MyTimerState createState() => _MyTimerState();
}
class _MyTimerState extends State<MyTimer> {
DateTime _currentDateTime = DateTime.now();
@override
void initState() {
super.initState();
Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
_currentDateTime = DateTime.now();
});
});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: RichText(
text: TextSpan(text: _currentDateTime.toString()),
textDirection: TextDirection.ltr,
),
);
}
}
簡單來說這個App會每秒更新現在時間,顯示在畫面正中央。看起來比之前的範例複雜許多,但其實它最終也只有Container, Align, MyTimer, Padding, RichText等五個Widget而已。
在開始之前,強烈建議你把範例APP跑起來,用Debug Mode跟著走一次,對於文章內容會更有感受。因為這篇真的很長,相信你不會想再看第二次。好,讓我們開始吧。程式從runApp進入scheduleAttachRootWidget,到達attachRootWidget:
// In WidgetsBinding
void attachRootWidget(Widget rootWidget) {
_readyToProduceFrames = true;
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>( // 1.
container: renderView, // 3.
debugShortDescription: '[root]',
child: rootWidget, // 2.
).attachToRenderTree( // 4.
buildOwner,
renderViewElement as RenderObjectToWidgetElement<RenderBox>,
);
}
此時候的三渲染樹:
接下來進入attachToRenderTree:
// In RenderObjectToWidgetAdapter
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
if (element == null) {
owner.lockState(() {
element = createElement(); // 1.
assert(element != null);
element.assignOwner(owner);
});
owner.buildScope(element, () {
element.mount(null, null); // 2.
});
SchedulerBinding.instance.ensureVisualUpdate();
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
此時的三渲染樹:
讓我們進入mount看看:
// In Element
@mustCallSuper
void mount(Element parent, dynamic newSlot) {
....
_parent = parent;
_slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1;
_active = true;
....
}
這裡我們先經過幾層super.mount到達最上層,看到mount最核心的工作,就是設定此一Element被掛在哪個parent的哪個slot上。
接著我們回到上一層的RenderObjectElement:
// In RenderObjectElement
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot); // 1.
....
_renderObject = widget.createRenderObject(this); // 2.
....
attachRenderObject(newSlot); // 3.
_dirty = false; // 4.
}
此時的三渲染樹:
最後我們回到RenderObjectToWidgetElement的mount:
static const Object _rootChildSlot = Object(); // 3
@override
void mount(Element parent, dynamic newSlot) {
assert(parent == null);
super.mount(parent, newSlot);
_rebuild(); // 1.
}
void _rebuild() {
try {
_child = updateChild(_child, widget.child, _rootChildSlot); // 2
assert(_child != null);
} catch (exception, stack) {
...
}
}
讓我們繼續進入updateChild:
// | | newWidget == null | newWidget != null |
// |child == null | Returns null. | Returns new [Element]. |
// |child != null | Remove old child, returns null | Old child updated if possible, returns child or new [Element]. |
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
....
if (child != null) {
....
} else {
newChild = inflateWidget(newWidget, newSlot);
}
....
}
整個Widget系統的核心邏輯就在這裡,code很複雜我就不貼了,直接看註解也滿容易懂的。對當前這個Parent Element而言:
我們之後會再看到4.是怎麼判斷更新或新建的。記得我們現在還在RenderObjectToWidgetElement嗎?這時候它還沒有child,newWidget則是我們最一開始從runApp傳入的Container(child:...),因此我們將進入2.,也就是inflateWidget的部份。
// In Element
@protected
Element inflateWidget(Widget newWidget, dynamic newSlot) {
....
final Element newChild = newWidget.createElement();
....
newChild.mount(this, newSlot);
....
return newChild;
}
inflateWidget最主要的工作就是透過Widget建立Element,並將它掛載到自己的一個slot上。這跟之前的attachToRenderTree是很相似的,只是attachToRenderTree是屬於Root Element的特殊情況,而這邊是開始遞迴的一般情況。這時的newWidget是Container,因此將產生StatelessElement來掛載,此時的三渲染樹:
我們再次進入mount:
In ComponentElement
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
assert(_child == null);
assert(_active);
_firstBuild();
assert(_child != null);
}
記得這時候的mount是來自Container的StatelessElement(ComponentElement),經過_firstBuild() -> rebuild() -> performRebuild()之後終於能找到我們最熟悉的build函數了:
// In ComponentElement
@override
void performRebuild() {
....
built = build();
....
_child = updateChild(_child, built, slot);
....
}
馬上來看看Container的build長什麼樣:
// In Container
@override
Widget build(BuildContext context) {
Widget current = child;
....
if (alignment != null)
current = Align(alignment: alignment, child: current);
final EdgeInsetsGeometry effectivePadding = _paddingIncludingDecoration;
if (effectivePadding != null)
current = Padding(padding: effectivePadding, child: current);
if (color != null)
current = ColoredBox(color: color, child: current);
....
return current;
}
這裡只留下幾個常用屬性的邏輯,感受一下Container的運作方式,其它都大同小異。我們可以看到,因為我們有設定alignment屬性,我們傳入的child(MyTimer)被多包了一層Align才回傳。此時的三渲染樹:
重複的程式碼我就不再貼了,可以隨時拉回去參考。build完之後我們再次進入updateChild,這次的parent是Container,child是Align,同樣因為沒有Element而進入
newChild = inflateWidget(newWidget, newSlot);
inflateWidget中會呼叫Align的createElement,得到SingleChildRenderObjectElement後再次進行mount。此時的三渲染樹:
同樣是RenderObjectElement的mount
void mount(Element parent, dynamic newSlot) {
....
_renderObject = widget.createRenderObject(this);
....
attachRenderObject(newSlot);
....
}
此時的widget是Align,createRenderObject會建立RenderPositionedBox。在它被掛載之前,我們先把它放在旁邊:
這是因為這次的attachRenderObject就有趣了,讓我們來看看:
@override
void attachRenderObject(dynamic newSlot) {
assert(_ancestorRenderObjectElement == null);
_slot = newSlot;
_ancestorRenderObjectElement = _findAncestorRenderObjectElement(); // 1.
_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot); // 2.
....
}
好的時間又不夠了,如果你能看到這裡我只能說你真是太神啦!但是讓我們彼此都休息一下吧,我們已經跑了一半囉!其實後面很多都大同小異了,只剩下State的建立和更新機制,應該會再花一些篇幅,我們下次再繼續吧。