轉載請聯絡: 微訊號: michaelzhoujay
原文請訪問我的部落格
如果你要使用Flutter開始構建一個頁面,那麼你可能從Widget開始一層一層搭建。
但是你曾經想過這些Widget是如何繪製到螢幕上的嗎?很多同學應該會熟悉Flutter的三個樹形結構:
- Widget Tree
- Element Tree
- RenderObject Tree
站在Flutter程式設計師的角度,打交道最多的應該是Widget了,那麼下面我帶領大家從Flutter原始碼讀起來看一下一個Widget是如何繪製到螢幕上的。
Opacity
首先我們來回憶一個熟悉的Widget:Opacity
。
這個Widget一般用來給一個佈局加上透明度,它只有一個引數 opacity
,1.0表示完全不透明,0.0表示完全透明。
在原始碼裡可以看到 Opacity 的繼承關係:
Opacity -> SingleChildRenderObjectWidget -> RenderObject -> Widget
我們比較一下最簡單的 StatelessWidget/StatefulWidget :
StatelessWidget -> Widget
StatefulWidget -> Widget
可以看到Opacity
多了SingleChildRenderObjectWidget
和RenderObject
這層關係,那這層關係到底是幹什麼的呢?我們接著往下看原始碼。
createRenderObject 方法
通過看RenderObject
的程式碼,我們知道只要繼承了 RenderObject
就需要實現createRenderObject
方法。
@override
RenderOpacity createRenderObject(BuildContext context) {
return RenderOpacity(
opacity: opacity,
alwaysIncludeSemantics: alwaysIncludeSemantics,
);
}
複製程式碼
可以發現其實最終返回了一個RenderOpacity
RenderOpacity
大概看一下RenderOpacity
的程式碼,可以知道這裡實現了最後的所謂的“繪製”也就是paint
的程式碼:
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (_alpha == 0) {
return;
}
if (_alpha == 255) {
context.paintChild(child, offset);
return;
}
assert(needsCompositing);
context.pushOpacity(offset, _alpha, super.paint);
}
}
複製程式碼
這裡的PaintingContext
實際上是個Canvas,所以最重要的程式碼就是context.pushOpacity
這個方法的呼叫,最終用上了 _alpha
,而這個_alpha
由最上層Opacity
Widget的唯一引數opacity
計算而來。
所以我們來總結一下這裡面的關係:
Opacity
直接繼承SingleChildRenderObjectWidget
Widget
不直接參與繪製,僅僅儲存繪製需要的資訊RenderOpacity
在這裡承擔了實際的 layout/render 邏輯
小結
我們日常所用的Widget僅僅是一些配置,RenderObject做了實際的 layout/render 等工作。這一點和Android的 View
、iOS的 UIView
有本質的區別,請注意。
在Flutter中,只要是build()
方法被呼叫了,就會建立一堆Widget,這個是OK的,上面說過Widget僅僅是配置檔案,所以即使頻繁地 建立/重建 它們,是不會帶來介面的重新整理的。
回想一下,如果你要在Flutter裡建立一個動畫,一般來說,需要在指定的Widget之上套上一層XXXTransition,然後動畫開始後用animationController
來不斷地讓Widget重新整理重建,但是整個頁面不會因此重新重新整理。
所以,Widget的重建和頁面真正的重新渲染沒有直接的聯絡,那麼頁面什麼時候才會重新渲染呢,我們接著看原始碼。
Element
上面我們以Opacity
為例,從原始碼中看出了它的繼承關係,並且推匯出一些結論,這裡面都沒有涉及到Element
,但實際上Element也十分重要。那什麼是Element呢?原始碼裡的註釋如下:
An instantiation of a Widget at a particular location in the tree.
我們知道,Widget是immutable的,也就是說一旦有了變化,Widget是反覆重建的,你在程式碼裡寫的Widget的建構函式會被重新執行。那對應的,肯定得有一個東西來消化掉這些變化中的不變,來做cache。
讓程式設計師不去儲存Widgets是好的,因為他不會因為管理無數的Widget而且煩惱,一旦有變化就當整個頁面都在變化,Widget的每一幀對應了一個State。這樣,一個指定的state就能精確描述一個Widget該怎麼展示。也就是說,程式設計師只要管理好狀態,那麼就能管理好整個頁面變化的邏輯。
當一個Widget首次被建立的時候,那麼這個Widget會過Widget.createElement
inflate成一個element
,掛在 element tree 上。
此後,當State發生變化,則Widget重建,但Element只會updates。
也就是說我們能大概有一個以下的一個流程:
程式設計師寫Widget -> Widget形成Element -> Element構建 RenderObject -> RenderObject描述Canvas繪製 -> 交給FlutterEngine 光柵化Rasterize
回到我們今天的主角Opacity
,從上面的分析,我們知道Opacity也是Widget,所以理應有對應的Element,但是上面看到為啥直接Widget就自己返回一個RenderOpacity
呢?看起來是Widget自己建立了RenderObject。那麼它的element是什麼時候建立的呢?看原始碼:
abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
...
@override
SingleChildRenderObjectElement createElement() => new SingleChildRenderObjectElement(this);
...
}
複製程式碼
Opacity繼承自SingleChildRenderObjectWidget
,
這裡面的SingleChildRenderObjectElement
,實際上是隻有一個child的Element。當第Widget第一次建立的時候,createElement
會被呼叫,返回一個SingleChildRenderObjectElement
那它的 RenderObject 實際上是被這個 Element 建立的,看一下為什麼:
class SingleChildRenderObjectElement extends RenderObjectElement {
SingleChildRenderObjectElement(SingleChildRenderObjectWidget widget) : super(widget);
...
}
複製程式碼
這個SingleChildRenderObjectElement
建構函式接受了一個SingleChildRenderObjectWidget
引數,它負責建立一個RenderObject
。那麼我們看這個 renderobject 實際在哪裡建立的:
class SingleChildRenderObjectElement {
...
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_child = updateChild(_child, widget.child, null);
}
...
}
複製程式碼
SingleChildRenderObjectElement裡有個mount方法,看起來沒啥,不過沒事兒我們往父類裡看:
class RenderObjectElement extends Element {
...
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
assert(() { _debugUpdateRenderObjectOwner(); return true; }());
assert(_slot == newSlot);
attachRenderObject(newSlot);
_dirty = false;
}
...
複製程式碼
終於看到magic了,所以在mount
方法裡,element呼叫了widget.createRenderObject
,並attach。
注意,這些都發生在mount
方法裡,意味著只有element mounted的時候才會執行,所以只有一次。
參考文獻: