前幾篇中介紹了flutter整體框架以及dart語(yǔ)言基礎(chǔ)和特性闽巩。從這篇開(kāi)始分享flutter的相關(guān)內(nèi)容霹琼。本篇要分享的是flutter框架里的三棵樹(shù)widget俭识,element,renderObject。將從以下幾個(gè)問(wèn)題逐步探究這三棵樹(shù):
- widget尖阔,element顿仇,renderObject對(duì)應(yīng)關(guān)系
- 三棵樹(shù)是如何工作的寄症?
Flutter的理念是一切都是Widget(Everything is Widget)绣硝。開(kāi)發(fā)者在開(kāi)發(fā)Flutter app的時(shí)候主要都是在寫(xiě)很多Widget。對(duì)flutter有所了解的都知道flutter是聲明式UI框架叔营,與之對(duì)應(yīng)的是命令式屋彪。舉個(gè)例子:一個(gè)頁(yè)面上有N個(gè)TextView,在Android開(kāi)發(fā)中如果我們想給這N個(gè)TextView設(shè)置文案绒尊,那我們通常需要調(diào)用這N個(gè)TextView的setText方法來(lái)設(shè)置畜挥。而對(duì)于聲明式UI框架的Flutter,需要將數(shù)據(jù)變更婴谱,重新構(gòu)建WidgetTree蟹但。也就是我們?cè)趂lutter開(kāi)發(fā)過(guò)程中數(shù)據(jù)變更時(shí)做的setState。相信你一定會(huì)想到每次setState都會(huì)重新構(gòu)建WidgetTree勘究,應(yīng)該對(duì)性能損耗很大吧。flutter號(hào)稱高性能的跨平臺(tái)UI框架斟冕,那么是怎么解決這樣的問(wèn)題呢口糕?
我們?cè)谑褂肳idget是都是widget的構(gòu)造方法,另外我們使用的Widget大部分是繼承自 StateLessWidget或者StatefulWidget磕蛇。我們先看看StateLessWidget里都做了些什么呢景描?
abstract class StatelessWidget extends Widget {
/// Initializes [key] for subclasses.
const StatelessWidget({ Key key }) : super(key: key);
/// Creates a [StatelessElement] to manage this widget's location in the tree.
///
/// It is uncommon for subclasses to override this method.
@override
StatelessElement createElement() => StatelessElement(this);
/// Describes the part of the user interface represented by this widget.
@protected
Widget build(BuildContext context);
}
通過(guò)這段源碼我們不難看出十办,在createElement 方法里,widget將自己的引用傳給了StatelessElement超棺,我們?cè)诳聪耂tatelessElement:
class StatelessElement extends ComponentElement {
/// Creates an element that uses the given widget as its configuration.
StatelessElement(StatelessWidget widget) : super(widget);
@override
StatelessWidget get widget => super.widget as StatelessWidget;
@override
Widget build() => widget.build(this);
@override
void update(StatelessWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_dirty = true;
rebuild();
}
}
在這塊代碼中build()方法將widget跟element關(guān)聯(lián)了起來(lái)向族,也就是widget的build方法持有的buildContext就是element。同時(shí)我們也可以發(fā)現(xiàn)并沒(méi)有繪制棠绘,布局相關(guān)的內(nèi)容件相,那只能繼續(xù)跟進(jìn)父類ComponentElement,Element了氧苍,跟進(jìn)之后發(fā)現(xiàn)我們只能找到get renderObject卻并沒(méi)找到renderObject的創(chuàng)建夜矗,但是在Element里有這么一塊代碼:
/// The render object at (or below) this location in the tree.
///
/// If this object is a [RenderObjectElement], the render object is the one at
/// this location in the tree. Otherwise, this getter will walk down the tree
/// until it finds a [RenderObjectElement].
RenderObject? get renderObject {
RenderObject? result;
void visit(Element element) {
assert(result == null); // this verifies that there's only one child
if (element._lifecycleState == _ElementLifecycle.defunct) {
return;
} else if (element is RenderObjectElement) {
result = element.renderObject;
} else {
element.visitChildren(visit);
}
}
visit(this);
return result;
}
在獲取的時(shí)候?qū)嶋H上是在element 樹(shù)上去查找離當(dāng)前節(jié)點(diǎn)最近的RenderObjectElement,也就是說(shuō)此處返回的RenderObject是RenderObjectElement的element.renderObject 让虐。到這里我們也可以得到一個(gè)結(jié)論就是:Widget 跟 Element是一一對(duì)應(yīng)的紊撕,但是Element跟RenderObject并不是一一對(duì)應(yīng)的。下面我們來(lái)看下RenderObjectElement:
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
assert(() {
_debugDoingBuild = true;
return true;
}());
_renderObject = (widget as RenderObjectWidget).createRenderObject(this); /// 此處會(huì)去創(chuàng)建RenderObject
assert(!_renderObject!.debugDisposed!);
assert(() {
_debugDoingBuild = false;
return true;
}());
assert(() {
_debugUpdateRenderObjectOwner();
return true;
}());
assert(_slot == newSlot);
attachRenderObject(newSlot);
_dirty = false;
}
mount方法在生命周期里有講過(guò)赡突,會(huì)在頁(yè)面創(chuàng)建的時(shí)候調(diào)用对扶,也是在此時(shí)renderObject跟RenderObjectElement,RenderObjectWidget也就關(guān)聯(lián)上了惭缰,并且RenderObject持有element的引用浪南。那么接下來(lái)毫無(wú)疑問(wèn)看看RenderObject是干啥的吧。
/// Paint this render object into the given context at the given offset.
///
/// Subclasses should override this method to provide a visual appearance
/// for themselves. The render object's local coordinate system is
/// axis-aligned with the coordinate system of the context's canvas and the
/// render object's local origin (i.e, x=0 and y=0) is placed at the given
/// offset in the context's canvas.
///
/// Do not call this function directly. If you wish to paint yourself, call
/// [markNeedsPaint] instead to schedule a call to this function. If you wish
/// to paint one of your children, call [PaintingContext.paintChild] on the
/// given `context`.
///
/// When painting one of your children (via a paint child function on the
/// given context), the current canvas held by the context might change
/// because draw operations before and after painting children might need to
/// be recorded on separate compositing layers.
void paint(PaintingContext context, Offset offset) { }
void layout(Constraints constraints, { bool parentUsesSize = false })
在RenderObject中很容易就找到了跟布局相關(guān)的layout方法从媚,和跟繪制相關(guān)的paint方法逞泄,從而我們可以得出一個(gè)結(jié)論:就是RenderObject其實(shí)是真正做繪制布局相關(guān)操作的對(duì)象。
下面我們總結(jié)下widget,element,renderObject三者之間的關(guān)系:
- widget是面對(duì)開(kāi)發(fā)者使用的配置對(duì)象拜效,可以通過(guò)widget對(duì)實(shí)際繪制做相關(guān)的配置和描述喷众,比價(jià)輕量級(jí)
- element 是Widget在UI樹(shù)具體位置的一個(gè)實(shí)例化對(duì)象,是實(shí)際處理生命周期紧憾,UI樹(shù)位置相關(guān)的對(duì)象
- RenderObject是實(shí)際布局和繪制的對(duì)象
其中widget跟Element是一一對(duì)應(yīng)到千,但是跟RenderObject并非一一對(duì)應(yīng),在實(shí)際開(kāi)發(fā)中赴穗,一般renderObject要少憔四。
(在這里我們也可以思考下,如果沒(méi)有Widget直接將Element暴漏出去供大家使用會(huì)有什么問(wèn)題呢般眉,少了一層結(jié)構(gòu)會(huì)不會(huì)更簡(jiǎn)單呢了赵,為啥要設(shè)計(jì)成這種結(jié)構(gòu)呢?)
知道了三者之間關(guān)系那么我們下面我們繼續(xù)針對(duì)flutter這種架構(gòu)方式是如何做到優(yōu)化的甸赃?
我們?cè)?a href="http://www.reibang.com/p/904be3cf4db0" target="_blank">生命周期篇中有提到在創(chuàng)建或者更新時(shí)會(huì)執(zhí)行elemnet的如下方法:
class ComponentElement extends Element{
@override
void performRebuild() {
………
Widget build;
build = build();
………
_child = updateChild(_child, build, slot);
…………
}
}
接下來(lái)我們著重看下updateChild方法:
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
if (newWidget == null) { /// 如果新的widget是null
if (child != null) ///child 不是null柿汛,那么就將該elemnt從element tree上移除
deactivateChild(child);
return null;
}
///如果新的widget不是null時(shí)
final Element newChild;
if (child != null) {
bool hasSameSuperclass = true;
// When the type of a widget is changed between Stateful and Stateless via
// hot reload, the element tree will end up in a partially invalid state.
// That is, if the widget was a StatefulWidget and is now a StatelessWidget,
// then the element tree currently contains a StatefulElement that is incorrectly
// referencing a StatelessWidget (and likewise with StatelessElement).
//
// To avoid crashing due to type errors, we need to gently guide the invalid
// element out of the tree. To do so, we ensure that the `hasSameSuperclass` condition
// returns false which prevents us from trying to update the existing element
// incorrectly.
//
// For the case where the widget becomes Stateful, we also need to avoid
// accessing `StatelessElement.widget` as the cast on the getter will
// cause a type error to be thrown. Here we avoid that by short-circuiting
// the `Widget.canUpdate` check once `hasSameSuperclass` is false.
assert(() {
final int oldElementClass = Element._debugConcreteSubtype(child);
final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
hasSameSuperclass = oldElementClass == newWidgetClass;
return true;
}());
if (hasSameSuperclass && child.widget == newWidget) { // 兩個(gè)widget誰(shuí)同一個(gè)的話
// We don't insert a timeline event here, because otherwise it's
// confusing that widgets that "don't update" (because they didn't
// change) get "charged" on the timeline.
if (child.slot != newSlot) /// 如果位置不一樣更新element tree的位置
updateSlotForChild(child, newSlot);
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
final bool isTimelineTracked = !kReleaseMode && _isProfileBuildsEnabledFor(newWidget);
if (isTimelineTracked) {
Map<String, String> debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent;
assert(() {
if (kDebugMode) {
debugTimelineArguments = newWidget.toDiagnosticsNode().toTimelineArguments();
}
return true;
}());
Timeline.startSync(
'${newWidget.runtimeType}',
arguments: debugTimelineArguments,
);
}
child.update(newWidget);
if (isTimelineTracked)
Timeline.finishSync();
assert(child.widget == newWidget);
assert(() {
child.owner!._debugElementWasRebuilt(child);
return true;
}());
newChild = child;
} else { // 如果完全不一樣
deactivateChild(child);
assert(child._parent == null);
// The [debugProfileBuildsEnabled] code for this branch is inside
// [inflateWidget], since some [Element]s call [inflateWidget] directly
// instead of going through [updateChild].
newChild = inflateWidget(newWidget, newSlot);
}
} else { // 之前的widget就是null,那么就新inflate一個(gè)
// The [debugProfileBuildsEnabled] code for this branch is inside
// [inflateWidget], since some [Element]s call [inflateWidget] directly
// instead of going through [updateChild].
newChild = inflateWidget(newWidget, newSlot);
}
assert(() {
if (child != null)
_debugRemoveGlobalKeyReservation(child);
final Key? key = newWidget.key;
if (key is GlobalKey) {
assert(owner != null);
owner!._debugReserveGlobalKeyFor(this, newChild, key);
}
return true;
}());
return newChild;
}
這里著重看下canUpdate埠对,后面開(kāi)發(fā)對(duì)理解widget的更新有幫助
/// Whether the `newWidget` can be used to update an [Element] that currently
/// has the `oldWidget` as its configuration.
///
/// An element that uses a given widget as its configuration can be updated to
/// use another widget as its configuration if, and only if, the two widgets
/// have [runtimeType] and [key] properties that are [operator==].
///
/// If the widgets have no key (their key is null), then they are considered a
/// match if they have the same type, even if their children are completely
/// different.
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
下面我們用張表來(lái)總結(jié)上面更新的各種情況:
到這里我們基本把widget络断,element裁替,renderObject三者的關(guān)系,以及如何優(yōu)化的做了一定的分析貌笨。你以為flutter就只做了這些嗎弱判,只做了這些就稱得上是高效能的UI框架了嗎?后面我們會(huì)繼續(xù)分析flutter的layer以及fluttet渲染相關(guān)內(nèi)容锥惋。