透过源码理解Flutter中widget、state和element的关系

1、framework源码组成

Flutter中widget、state、element的源码位于framework.dart中,整个文件6693行(版本Flutter 3.12.0-14.0.pre.28)。整个代码可划分为若干部分,主要包括key、widget、state、element四部分。

1.1 key

关于key的代码65行到272行,这里的key包括ObjectKey、GlobalKey、LabeledGlobalKey、GlobalObjectKey。整个key体系的代码还包括key.dart这个文件,里面包括Key、LocalKey、UniqueKey和ValueKey。Key是GlobalKey和LocalKey的抽象基类。LabeledGlobalKey和GlobalObjectKey是GlobalKey的抽象子类。ObjectKey、UniqueKey和ValueKey是LocalKey的三个具体子类。

1.2 widget

关于widget的代码274行到1922行。包括10个抽象类:Widget、StatelessWidget、StatefulWidget、ProxyWidget、ParentDataWidget、InheritedWidget、RenderObjectWidget、LeafRenderObjectWidget、SingleChildRenderObjectWidget、MultiChildRenderObjectWidget。这些类,大体可以把Widget分为组合式、渲染、功能性三种。

1.3 state

State的代码在823附近。State是一个抽象类。State首先起着一个枢纽的作用,它持有widget,也持有element。从state里获取context,只是简单返回持有的element。另一方面,State对外提供了widget的生命周期:initState、didUpdateWidget、reassemble、deactivate、activate、dispose、didChangeDependencies。这些生命周期方法是系统提供给我们的钩子。如果我们要主动发起渲染请求的话,就要调用State提供给我们的setState方法。而build则是我们告诉系统如何渲染这个widget的地方。前者提供时机,后者提供内容。

1.4 BuildContext

BuildContext是一个抽象类,代码位于2129-2485行。Element实现了BuildContext。

1.5 BuildOwner

BuildOwner位于2511-3168行。

1.6 element

element相关的代码位于3259行到6597行之间。接近一半的代码量,可以看出element是核心部分。

2 StatelessWidget和StatefulWidget

Widget是一个抽象类,里面关键的三个东西:

final Key? key;Element createElement();static bool canUpdate(Widget oldWidget, Widget newWidget) {return oldWidget.runtimeType == newWidget.runtimeType&& oldWidget.key == newWidget.key;}

我们要关注下canUpdate这个方法的实现,决定我们能否复用这个widget是由这个widget的runtimeType和key决定的。runtimeType表明了widget的类型,不同类型的widget是不能复用的。key是我们人为指定的一个值,它可以在同类型widget之间产生个体差异,以便我们或者渲染系统找到它。不设置的时候就是null,这时候只比较runtimeType就行。

我们先来介绍widget中的组合式子类:StatelessWidget和StatefulWidget。StatelessWidget创建的element是StatelessElement:

@overrideStatelessElement createElement() => StatelessElement(this);

而StatefulWidget创建的是StatefulElement,并且还能创建state:

@overrideStatefulElement createElement() => StatefulElement(this);@protected@factoryState createState();

StatelessWidget和StatefulWidget仍然是抽象类,需要我们子类化。根据源码,我们发现StatefulWidget和StatelessWidget只负责创建对应element,并不持有它。而Statefulwidget只负责创建state,同样也并不持有它。

3 RenderObjectWidget

RenderObjectWidget有三个抽象子类LeafRenderObjectWidget、SingleChildRenderObjectWidget、MultiChildRenderObjectWidget。分别代表了没有孩子、只有一个孩子和有多个孩子的三种RenderObjectWidget。源码我们不再展开,想必你也猜到了是一个啥都没,一个有个child,最后一个有个children属性罢了。

相比于前面的StatelessWidget,RenderObjectWidget返回自己独特的RenderObjectElement,并且还多了一个RenderObject:

RenderObject createRenderObject(BuildContext context);@protectedvoid updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }@protectedvoid didUnmountRenderObject(covariant RenderObject renderObject) { }

RenderObjectWidget我们后面在RenderObjectElement的performRebuild里讲到了,它会调用updateRenderObject进行更新。这里我们无法展开讲updateRenderObject,需要去看具体子类里的实现。 

3.1 ColoredBox

我们以SingleChildRenderObjectWidget的一个具体子类ColoredBox为例:

final Color color;@overrideRenderObject createRenderObject(BuildContext context) {return _RenderColoredBox(color: color);}@overridevoid updateRenderObject(BuildContext context, RenderObject renderObject) {(renderObject as _RenderColoredBox).color = color;}

我们发现它创建的RenderObject是一个私有类_RenderColoredBox。而我们前面提到的updateRenderObject在这里只是设置一下新的颜色。

我们再转去看_RenderColoredBox的实现:

class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {_RenderColoredBox({ required Color color }): _color = color,super(behavior: HitTestBehavior.opaque);/// The fill color for this render object.////// This parameter must not be null.Color get color => _color;Color _color;set color(Color value) {if (value == _color) {return;}_color = value;markNeedsPaint();}@overridevoid paint(PaintingContext context, Offset offset) {// It's tempting to want to optimize out this `drawRect()` call if the// color is transparent (alpha==0), but doing so would be incorrect. See// https://github.com/flutter/flutter/pull/72526#issuecomment-749185938 for// a good description of why.if (size > Size.zero) {context.canvas.drawRect(offset & size, Paint()..color = color);}if (child != null) {context.paintChild(child!, offset);}}
}

主要是根据颜色在canvas上绘制背景色和child。设置新颜色会引起markNeedsPaint。markNeedsPaint相关代码在RenderObject里。

3.2 markNeedsPaint

void markNeedsPaint() {if (_needsPaint) {return;}_needsPaint = true;// If this was not previously a repaint boundary it will not have// a layer we can paint from.if (isRepaintBoundary && _wasRepaintBoundary) {if (owner != null) {owner!._nodesNeedingPaint.add(this);owner!.requestVisualUpdate();}} else if (parent is RenderObject) {parent!.markNeedsPaint();} else {// If we are the root of the render tree and not a repaint boundary// then we have to paint ourselves, since nobody else can paint us.// We don't add ourselves to _nodesNeedingPaint in this case,// because the root is always told to paint regardless.//// Trees rooted at a RenderView do not go through this// code path because RenderViews are repaint boundaries.if (owner != null) {owner!.requestVisualUpdate();}}}

这里出现了新的Owner:PipelineOwner。markNeedsPaint标记自己需要重新绘制,如果自己是绘制边界,就把自己加入需要绘制的节点列表里。如果不是绘制边界,就调用父节点的markNeedsPaint。这里只是简单标记和放入列表,真正执行绘制的时机是在WidgetsBinding.drawFrame里的flushPaint:

void drawFrame() {buildOwner!.buildScope(renderViewElement!); // 1.重新构建widgetsuper.drawFrame();//下面几个是在super.drawFrame()执行的pipelineOwner.flushLayout();          // 2.更新布局pipelineOwner.flushCompositingBits();     //3.更新“层合成”信息pipelineOwner.flushPaint();               // 4.重绘if (sendFramesToEngine) {renderView.compositeFrame();            // 5. 上屏,将绘制出的bit数据发送给GPU}
}

3.3 markNeedsLayout

上面的代码里,我们也看到了布局是在这之前的flushLayout执行的。RenderBox源码里PipelineOwner通过markNeedsLayout标记、收集需要布局节点:

void markNeedsLayout() {if (_needsLayout) {return;}if (_relayoutBoundary == null) {_needsLayout = true;if (parent != null) {// _relayoutBoundary is cleaned by an ancestor in RenderObject.layout.// Conservatively mark everything dirty until it reaches the closest// known relayout boundary.markParentNeedsLayout();}return;}if (_relayoutBoundary != this) {markParentNeedsLayout();} else {_needsLayout = true;if (owner != null) {owner!._nodesNeedingLayout.add(this);owner!.requestVisualUpdate();}}}void markParentNeedsLayout() {assert(_debugCanPerformMutations);_needsLayout = true;assert(this.parent != null);final RenderObject parent = this.parent!;if (!_doingThisLayoutWithCallback) {parent.markNeedsLayout();} else {assert(parent._debugDoingThisLayout);}assert(parent == this.parent);}

我们发现布局标记和绘制标记的实现是类似的,都需要标记自身,都需要向上寻找布局或者绘制的边界。PipelineOwner最终对其调用performLayout和markNeedsPaint:

void flushLayout() {try {while (_nodesNeedingLayout.isNotEmpty) {final List<RenderObject> dirtyNodes = _nodesNeedingLayout;_nodesNeedingLayout = <RenderObject>[];dirtyNodes.sort((RenderObject a, RenderObject b) => a.depth - b.depth);for (int i = 0; i < dirtyNodes.length; i++) {if (_shouldMergeDirtyNodes) {_shouldMergeDirtyNodes = false;if (_nodesNeedingLayout.isNotEmpty) {_nodesNeedingLayout.addAll(dirtyNodes.getRange(i, dirtyNodes.length));break;}}final RenderObject node = dirtyNodes[i];if (node._needsLayout && node.owner == this) {node._layoutWithoutResize();}}// No need to merge dirty nodes generated from processing the last// relayout boundary back._shouldMergeDirtyNodes = false;}for (final PipelineOwner child in _children) {child.flushLayout();}} finally {_shouldMergeDirtyNodes = false;}}void _layoutWithoutResize() {RenderObject? debugPreviousActiveLayout;try {performLayout();markNeedsSemanticsUpdate();} catch (e, stack) {_reportException('performLayout', e, stack);}_needsLayout = false;markNeedsPaint();}

performLayout这个方法在RenderBox里实现为空,需要子类自行实现。

前面ColoredBox这个例子里我们在updateRenderObject里改变颜色并不会引起布局变化。现在我们找一个RenderPositionedBox的源码来看看。

3.3 RenderPositionedBox

RenderPositionedBox是Align使用的renderObject。我们看看它的updateRenderObject实现:

void updateRenderObject(BuildContext context, RenderPositionedBox renderObject) {renderObject..alignment = alignment..widthFactor = widthFactor..heightFactor = heightFactor..textDirection = Directionality.maybeOf(context);}

再进到RenderPositionedBox的set alignment实现:

set alignment(AlignmentGeometry value) {if (_alignment == value) {return;}_alignment = value;_markNeedResolution();}void _markNeedResolution() {_resolvedAlignment = null;markNeedsLayout();}

我们发现设置新的alignment,会引起markNeedsLayout的调用。

4 三个功能性widget:ProxyWidget、ParentDataWidget、InheritedWidget

暂不展开

5 StatefulElement和StatelessElement

我们再跳到StatelessElement构造方法:


class StatelessElement extends ComponentElement {/// Creates an element that uses the given widget as its configuration.StatelessElement(StatelessWidget super.widget);@overrideWidget build() => (widget as StatelessWidget).build(this);
}

StatelessElement自身可以通过build返回子element对应的widget。

而StatefulElement构造方法:

 /// Creates an element that uses the given widget as its configuration.StatefulElement(StatefulWidget widget): _state = widget.createState(),super(widget) {state._element = this;state._widget = widget;}

我们发现在创建element的时候,会先调用widget的createState创建state,并指向它,然后state就伸出两只手,一只手拉着widget,另一只手拉着element。element里面有一个重要的方法:

  Widget build() => state.build(this);

这里我们可以认为state build出来的是element持有的widget的“child”。事实上,无论StatelessElement还是Statefulwidget,它们都没child这个概念,但是对应的element是有一个child的属性的。所以我们姑且这么看待它们的关系。这里把element传进去,只是因为我们可能需要用到element树一些上下文信息。

3.1 setState

现在看看我们的老朋友,state里的setState方法的实现:

  void setState(VoidCallback fn) {_element!.markNeedsBuild();}void markNeedsBuild() {if (dirty) {return;}_dirty = true;owner!.scheduleBuildFor(this);}void scheduleBuildFor(Element element) {_dirtyElements.add(element);element._inDirtyList = true;}void rebuild() {performRebuild();}void performRebuild() {_dirty = false;}

完整的流程如下:

中间省略一些代码,我们直接跳到performRebuild实现。对于基类Element的实现,只是简单标记为dirty。Element分为渲染和组件两种类型,前者与渲染相关,后者用于组成其他element。

3.2 performRebuild

对于跟渲染相关的RenderObjectElement的performRebuild,则需要更新它的renderObject:

void _performRebuild() {(widget as RenderObjectWidget).updateRenderObject(this, renderObject);super.performRebuild(); // clears the "dirty" flag}

对于跟组件相关的ComponentElement的performRebuild实现:

  void performRebuild() {Widget? built;try {built = build();} catch (e, stack) {} finally {// We delay marking the element as clean until after calling build() so// that attempts to markNeedsBuild() during build() will be ignored.super.performRebuild(); // clears the "dirty" flag}try {_child = updateChild(_child, built, slot);} catch (e, stack) {_child = updateChild(null, built, slot);}}

这里的核心是会调用build方法创建新的widget,然后使用这个widget去更新child element。从前面的代码中我们可以看到statefulElement和statelessElement的build实现是有差异的。但是返回的widget,其实都是它们的child对应的widget。在多个渲染周期,child element会一直存在,而需要更新时widget就会重新创建。更新后的Element设置为当前Element的child。至于怎么更新,我们等下再讲。

ComponentElement是ProxyElement、StatefulElement和StatelessElement的父类。但是只有StatefulElement覆写了performRebuild。进一步来到StatefulElement的performRebuild实现:

void performRebuild() {if (_didChangeDependencies) {state.didChangeDependencies();_didChangeDependencies = false;}super.performRebuild();}

StatefulElement增加的任务是如果依赖发生了变化,要触发state的didChangeDependencies方法。

3.3 updateChild

回到前文,我们再来看Element的updateChild实现:

Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {// 如果'newWidget'为null,而'child'不为null,那么我们删除'child',返回null。if (newWidget == null) {if (child != null) {deactivateChild(child);}return null;}final Element newChild;if (child != null) {// 两个widget相同,位置不同更新位置。先更新位置,然后返回child。这里比较的是hashCodeif (child.widget == newWidget) {if (child.slot != newSlot) {updateSlotForChild(child, newSlot);}newChild = child;} else if (Widget.canUpdate(child.widget, newWidget)) {//两个widget不同,但是可以复用。位置不同则先更新位置。然后用新widget更新elementif (child.slot != newSlot) {updateSlotForChild(child, newSlot);}child.update(newWidget);newChild = child;} else {// 如果无法更新复用,那么删除原来的child,然后创建一个新的Element并返回。deactivateChild(child);newChild = inflateWidget(newWidget, newSlot);}} else {// 如果是初次创建,那么创建一个新的Element并返回。newChild = inflateWidget(newWidget, newSlot);}return newChild;}

这里关键的两个方法是update和inflateWidget。

3.4 update

对于不同类型的child的update方法是不一样的。基类Element只是用新的替换旧的而已:

void update(covariant Widget newWidget) {_widget = newWidget;}

对于StatelessElement的update,就是直接更换widget:

 @overridevoid update(StatelessWidget newWidget) {//直接更换widgetsuper.update(newWidget);assert(widget == newWidget);rebuild(force: true);}

对于StatefulElement的update,除了更换widget,还要更换state指向的widget:

void update(StatefulWidget newWidget) {super.update(newWidget);final StatefulWidget oldWidget = state._widget!;state._widget = widget as StatefulWidget;final Object? debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) as dynamic;rebuild(force: true);}

最后都通过调用rebuild,标记自身dirty。

对于SingleChildRenderObjectElement就是对它的child调用updateChild,对于MultiChildRenderObjectElement就是对它的children调用updateChildren:

void update(SingleChildRenderObjectWidget newWidget) {super.update(newWidget);_child = updateChild(_child, (widget as SingleChildRenderObjectWidget).child, null);}void update(MultiChildRenderObjectWidget newWidget) {super.update(newWidget);final MultiChildRenderObjectWidget multiChildRenderObjectWidget = widget as MultiChildRenderObjectWidget;_children = updateChildren(_children, multiChildRenderObjectWidget.children, forgottenChildren: _forgottenChildren);_forgottenChildren.clear();}

而对于ProxyElement主要是更新widget和通知:

@overridevoid update(ProxyWidget newWidget) {final ProxyWidget oldWidget = widget as ProxyWidget;//使用新的widget更新持有的widgetsuper.update(newWidget);//通知其他关联widget自己发生了变化updated(oldWidget);//标记dirtyrebuild(force: true);}@protectedvoid updated(covariant ProxyWidget oldWidget) {notifyClients(oldWidget);}

3.5 updateChildren

updateChild前面我们已经提到了,而对于updateChildren的实现:

List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element>? forgottenChildren, List<Object?>? slots }) {Element? replaceWithNullIfForgotten(Element child) {return forgottenChildren != null && forgottenChildren.contains(child) ? null : child;}Object? slotFor(int newChildIndex, Element? previousChild) {return slots != null? slots[newChildIndex]: IndexedSlot<Element?>(newChildIndex, previousChild);}// This attempts to diff the new child list (newWidgets) with// the old child list (oldChildren), and produce a new list of elements to// be the new list of child elements of this element. The called of this// method is expected to update this render object accordingly.// The cases it tries to optimize for are://  - the old list is empty//  - the lists are identical//  - there is an insertion or removal of one or more widgets in//    only one place in the list// If a widget with a key is in both lists, it will be synced.// Widgets without keys might be synced but there is no guarantee.// The general approach is to sync the entire new list backwards, as follows:// 1. Walk the lists from the top, syncing nodes, until you no longer have//    matching nodes.// 2. Walk the lists from the bottom, without syncing nodes, until you no//    longer have matching nodes. We'll sync these nodes at the end. We//    don't sync them now because we want to sync all the nodes in order//    from beginning to end.// At this point we narrowed the old and new lists to the point// where the nodes no longer match.// 3. Walk the narrowed part of the old list to get the list of//    keys and sync null with non-keyed items.// 4. Walk the narrowed part of the new list forwards://     * Sync non-keyed items with null//     * Sync keyed items with the source if it exists, else with null.// 5. Walk the bottom of the list again, syncing the nodes.// 6. Sync null with any items in the list of keys that are still//    mounted.int newChildrenTop = 0;int oldChildrenTop = 0;int newChildrenBottom = newWidgets.length - 1;int oldChildrenBottom = oldChildren.length - 1;final List<Element> newChildren = List<Element>.filled(newWidgets.length, _NullElement.instance);Element? previousChild;// 从前往后依次对比,相同的更新Element,记录位置,直到不相等时跳出循环。.while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);final Widget newWidget = newWidgets[newChildrenTop];// 注意这里的canUpdate,本例中在没有添加key时返回true。// 因此直接执行updateChild,本循环结束返回newChildren。后面因条件不满足都在不执行。// 一旦添加key,这里返回false,不同之处就此开始。if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) {break;}final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;newChildren[newChildrenTop] = newChild;previousChild = newChild;newChildrenTop += 1;oldChildrenTop += 1;}// 从后往前依次对比,记录位置,直到不相等时跳出循环。while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);final Widget newWidget = newWidgets[newChildrenBottom];if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) {break;}oldChildrenBottom -= 1;newChildrenBottom -= 1;}// 至此,就可以得到新旧List中不同Weiget的范围。final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;Map<Key, Element>? oldKeyedChildren;
// 如果存在中间范围,扫描旧children,获取所有的key与Element保存至oldKeyedChildren。if (haveOldChildren) {oldKeyedChildren = <Key, Element>{};while (oldChildrenTop <= oldChildrenBottom) {final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);if (oldChild != null) {if (oldChild.widget.key != null) {oldKeyedChildren[oldChild.widget.key!] = oldChild;} else {deactivateChild(oldChild);}}oldChildrenTop += 1;}}// 更新中间不同的部分,如果新旧key相同就更新一下重新利用,否则新的widget就没有旧的对应,是插入行为while (newChildrenTop <= newChildrenBottom) {Element? oldChild;final Widget newWidget = newWidgets[newChildrenTop];if (haveOldChildren) {final Key? key = newWidget.key;if (key != null) {// key不为null,通过key获取对应的旧ElementoldChild = oldKeyedChildren![key];if (oldChild != null) {if (Widget.canUpdate(oldChild.widget, newWidget)) {// we found a match!// remove it from oldKeyedChildren so we don't unsync it lateroldKeyedChildren.remove(key);} else {// Not a match, let's pretend we didn't see it for now.oldChild = null;}}}}final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;newChildren[newChildrenTop] = newChild;previousChild = newChild;newChildrenTop += 1;}// We've scanned the whole list.// 重置newChildrenBottom = newWidgets.length - 1;oldChildrenBottom = oldChildren.length - 1;// 将后面相同的Element更新后添加到newChildren,至此形成新的完整的children。while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {final Element oldChild = oldChildren[oldChildrenTop];final Widget newWidget = newWidgets[newChildrenTop];final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;newChildren[newChildrenTop] = newChild;previousChild = newChild;newChildrenTop += 1;oldChildrenTop += 1;}// 清除旧列表中多余的带key的Elementif (haveOldChildren && oldKeyedChildren!.isNotEmpty) {for (final Element oldChild in oldKeyedChildren.values) {if (forgottenChildren == null || !forgottenChildren.contains(oldChild)) {deactivateChild(oldChild);}}}return newChildren;}

dif算法相对比较复杂,可能理解起来比较困难。值得一提的是,无论 updateChild还是updateChildren都实现在基类element里。同层diff算法里使用key并不是出于性能考虑,没有key能够就地复用,使用key能够指定复用对象。有时候就地复用会有一些问题,譬如某个widget自身有一些状态,你如果就地复用其他widget,就会导致这些状态的丢失。

3.6 inflateWidget

再来看看inflateWidget的实现,它主要是用来创建新的element,并且mount。如果widget有GlobalKey的话,则会尝试获取对应的element,然后更新后返回。

Element inflateWidget(Widget newWidget, Object? newSlot) {try {//如果widget带key,并且是GlobalKey,则尝试获取一下对应的element,并用新的widget更新它然后返回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!;}}// 这里就调用到了createElement,重新创建了Elementfinal Element newChild = newWidget.createElement();newChild.mount(this, newSlot);return newChild;} }

3.7 mount

我们再来看看element基类的mount:

void mount(Element? parent, Object? newSlot) {_parent = parent;_slot = newSlot;_lifecycleState = _ElementLifecycle.active;_depth = _parent != null ? _parent!.depth + 1 : 1;if (parent != null) {// Only assign ownership if the parent is non-null. If parent is null// (the root node), the owner should have already been assigned.// See RootRenderObjectElement.assignOwner()._owner = parent.owner;}final Key? key = widget.key;if (key is GlobalKey) {owner!._registerGlobalKey(key, this);}_updateInheritance();attachNotificationTree();}

mount就是将自身插入父element的某个slot中。我们发现Element在mount的时候,会将父element的ower设置给自己。如果widget带有key,那么ower会将这个element注册到自己的map里。

而对于组合式Element的mount有所差异,除了上述基类行为,还会调用_firstBuild:

@overridevoid mount(Element? parent, Object? newSlot) {super.mount(parent, newSlot);_firstBuild();}void _firstBuild() {// StatefulElement overrides this to also call state.didChangeDependencies.rebuild(); // This eventually calls performRebuild.}

对于StatelessElement,_firstBuild的实现只是单纯rebuild一下。而对于StatefulElement:

@overridevoid _firstBuild() {final Object? debugCheckForReturnedFuture = state.initState() as dynamic;state.didChangeDependencies();super._firstBuild();}

我们发现_firstBuild里调用了state的initState方法,这里说明我们在state里实现的生命周期方法,其实会被StatefulElement根据自身的不同状态而调用。因此其他方法我们不再赘述。

3.8 why?

在参考文章里有一个问题,我们来分析一下,增加我们对本文的理解程度。现在我们有如下一段代码:

import 'dart:math';import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(title: 'Flutter Demo',theme: ThemeData(primarySwatch: Colors.blue,),home: MyHomePage(title: 'Home Page'),);}
}class MyHomePage extends StatefulWidget {MyHomePage({Key key, this.title}) : super(key: key);final String title;@override_MyHomePageState createState() => _MyHomePageState();
}class _MyHomePageState extends State<MyHomePage> {List<Widget> widgets;@overridevoid initState() {super.initState();widgets = [StatelessColorfulTile(),StatelessColorfulTile()];}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text(widget.title),),body: Row(children: widgets,),floatingActionButton: FloatingActionButton(child: Icon(Icons.refresh),onPressed: _swapTile,),);}_swapTile() {setState(() {widgets.insert(1, widgets.removeAt(0));});}
}class StatelessColorfulTile extends StatelessWidget {final Color _color = Utils.randomColor();@overrideWidget build(BuildContext context) {return Container(height: 150,width: 150,color: _color,);}
}class Utils {static Color randomColor() {var red = Random.secure().nextInt(255);var greed = Random.secure().nextInt(255);var blue = Random.secure().nextInt(255);return Color.fromARGB(255, red, greed, blue);}
}

代码可以直接复制到DartPad中运行查看效果。 或者点击这里直接运行。

效果很简单,就是两个彩色方块,点击右下角的按钮后交换两个方块的位置。上面的方块是StatelessWidget,那我们把它换成StatefulWidget呢?。

class StatefulColorfulTile extends StatefulWidget {StatefulColorfulTile({Key key}) : super(key: key);@overrideStatefulColorfulTileState createState() => StatefulColorfulTileState();
}class StatefulColorfulTileState extends State<StatefulColorfulTile> {final Color _color = Utils.randomColor();@overrideWidget build(BuildContext context) {return Container(height: 150,width: 150,color: _color,);}
}

 再次执行代码,发现方块没有“交换”。这是为什么?结论是widget层面而言,两个widget的确发生了交换,但是Element并没有发生交换,原来位置的Element持有的state build出原来颜色的Container。

6 key

可以看参考,这里暂不展开

7 BuildOwner

buildOwner是framework这些代码背后的大boss。我们来看看它做了哪些事情。每个element都指向一个Owner用来维护它的生命周期:

BuildOwner? get owner => _owner;
BuildOwner? _owner;

为什么我们能用globalKey找到对应的element,没有什么神奇的,因为buildOwner有一个map维护着globalKey和element的对应关系:

  final Map<GlobalKey, Element> _globalKeyRegistry = <GlobalKey, Element>{};void _registerGlobalKey(GlobalKey key, Element element)
void _unregisterGlobalKey(GlobalKey key, Element element)

buildOwner另一个作用是维护着element的build列表:

  final List<Element> _dirtyElements = <Element>[];void scheduleBuildFor(Element element) {if (element._inDirtyList) {_dirtyElementsNeedsResorting = true;return;}if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {_scheduledFlushDirtyElements = true;onBuildScheduled!();}_dirtyElements.add(element);element._inDirtyList = true;}

WidgetsBinding会通过WidgetsBinding.drawFrame调用buildOwner的buildScope:

void drawFrame() {buildOwner!.buildScope(renderViewElement!); // 1.重新构建widgetsuper.drawFrame();//下面几个是在super.drawFrame()执行的pipelineOwner.flushLayout();          // 2.更新布局pipelineOwner.flushCompositingBits();     //3.更新“层合成”信息pipelineOwner.flushPaint();               // 4.重绘if (sendFramesToEngine) {renderView.compositeFrame();            // 5. 上屏,将绘制出的bit数据发送给GPU}
}

buildScope对_dirtyElements里的element调用rebuild:

void buildScope(Element context, [ VoidCallback? callback ]) {if (callback == null && _dirtyElements.isEmpty) {return;}try {_scheduledFlushDirtyElements = true;if (callback != null) {_dirtyElementsNeedsResorting = false;try {callback();}}_dirtyElements.sort(Element._sort);_dirtyElementsNeedsResorting = false;int dirtyCount = _dirtyElements.length;int index = 0;while (index < dirtyCount) {final Element element = _dirtyElements[index];try {element.rebuild();} index += 1;if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting!) {_dirtyElements.sort(Element._sort);_dirtyElementsNeedsResorting = false;dirtyCount = _dirtyElements.length;while (index > 0 && _dirtyElements[index - 1].dirty) {// It is possible for previously dirty but inactive widgets to move right in the list.// We therefore have to move the index left in the list to account for this.// We don't know how many could have moved. However, we do know that the only possible// change to the list is that nodes that were previously to the left of the index have// now moved to be to the right of the right-most cleaned node, and we do know that// all the clean nodes were to the left of the index. So we move the index left// until just after the right-most clean node.index -= 1;}}}} finally {for (final Element element in _dirtyElements) {assert(element._inDirtyList);element._inDirtyList = false;}_dirtyElements.clear();_scheduledFlushDirtyElements = false;_dirtyElementsNeedsResorting = null;}}

后面的流程就回到了我们前面的performRebuild方法 。

8 没有覆盖的内容

本文没有提及具体的布局逻辑,将在后面的文章里进行讲述。

9 图示

文中出现的一些关键类的继承关系:

参考

1.说说Flutter中最熟悉的陌生人 —— Key_flutter globalkey 源码_唯鹿的博客-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/43575.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

GuLi商城-前端基础Vue-生命周期和钩子函数

下图展示了实例的生命周期。你不需要立马弄明白所有的东西&#xff0c;不过随着你的不断学习和使用&#xff0c;它 的参考价值会越来越高。 VUE 的生命周期指的是组件在创建、运行和销毁过程中所经历的一系列事件&#xff0c;通过这些事件可以 让开发者在不同阶段进行相应的…

vue3 + antv/x6 实现拖拽侧边栏节点到画布

前篇&#xff1a;vue3ts使用antv/x6 自定义节点 前篇&#xff1a;vue3antv x6自定义节点样式 1、创建侧边栏 用antd的menu来做侧边栏 npm i --save ant-design-vue4.x//入口文件main.js内 import Antd from ant-design-vue; import App from ./App; import ant-design-vue/…

安卓的代码加固和其他安全问题

文章目录 安卓加固apk文件结构dex加固过程 其它安全问题 安卓加固 从App的加固技术来看:主流分为dex加密和so加密,目前来看保护dex文件更为重要,因为dex反编译后的java代码可读性更强。 android-ndk: Native Development Kit 官网解释&#xff1a;这套工具使您能在 Android 应…

Kvm配置ovs网桥

环境&#xff1a;部署在kvm虚拟环境上&#xff08;让虚拟机和宿主机都可以直接从路由器获取到独立ip&#xff09; 1、安装ovs软件安装包并启动服务&#xff08;一般采用源码安装&#xff0c;此处用yum安装&#xff09; yum install openvswitch-2.9.0-3.el7.x86_64.rpm syste…

Git常见操作

一、全局配置命令 配置级别&#xff1a; –local&#xff08;默认&#xff0c;高级优先&#xff09;&#xff1a;只影响本地仓库 –global(中优先级)&#xff1a;只影响所有当前用户的git仓库 –system&#xff08;低优先级&#xff09;&#xff1a;影响到全系统的git仓库 1…

剑指 Offer 40. 最小的k个数(C+实现)

剑指 Offer 40. 最小的k个数https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/ 法1&#xff1a;二叉堆 通过最小堆&#xff0c;直接筛选出最小的k个数 vector<int> getLeastNumbers(vector<int>& arr, int k) {priority_queue<int, vector<int>…

YOLOv8改进后效果

数据集 自建铁路障碍数据集-包含路障&#xff0c;人等少数标签。其中百分之八十作为训练集&#xff0c;百分之二十作为测试集 第一次部署 版本&#xff1a;YOLOv5 训练50epoch后精度可达0.94 mAP可达0.95.此时未包含任何改进操作 第二次部署 版本&#xff1a;YOLOv8改进版本 首…

WebRTC | ICE详解

目录 一、Candidate种类与优先级 二、ICE策略 1. iceServers 2. iceTransportPolicy 三、P2P连接 1.Nat类型 &#xff08;1&#xff09;完全锥型NAT &#xff08;2&#xff09;IP限制锥型NAT &#xff08;3&#xff09;端口限制锥型NAT &#xff08;4&#xff09;对称…

iPhone 15受益:骁龙8 Gen 3可能缺席部分安卓旗舰机

明年一批领先的安卓手机的性能可能与今年的机型非常相似。硅成本的上涨可能是原因。 你可以想象&#xff0c;2024年许多最好的手机都会在Snapdragon 8 Gen 3上运行&#xff0c;这是高通公司针对移动设备的顶级芯片系统的更新&#xff0c;尚未宣布。然而&#xff0c;来自中国的…

C#生产流程控制(串行,并行混合执行)

开源框架CsGo https://gitee.com/hamasm/CsGo?_fromgitee_search 文档资料&#xff1a; https://blog.csdn.net/aa2528877987/article/details/132139337 实现效果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37…

Windows11 Docker Desktop 启动 -wsl kernel version too low

系统环境&#xff1a;windows11 1&#xff1a;docker下载 Docker: Accelerated Container Application Development 下载后双击安装即可 安装后启动Docker提示&#xff1a;Docker Desktop -wsl kernel version too low 处理起来也是非常方便 1:管理员身份启动&#xff1a;…

C#程序随系统启动例子 - 开源研究系列文章

今天讲讲C#中应用程序随系统启动的例子。 我们知道&#xff0c;应用程序随系统启动&#xff0c;都是直接在操作系统注册表中写入程序的启动参数&#xff0c;这样操作系统在启动的时候就根据启动参数来启动应用程序&#xff0c;而我们要做的就是将程序启动参数写入注册表即可。此…

【3Ds Max】车削命令的简单使用(以制作花瓶为例)

简介 在3ds Max中&#xff0c;"车削"&#xff08;Lathe&#xff09;是一种建模命令&#xff0c;用于创建围绕轴线旋转的几何形状。通过车削命令&#xff0c;您可以将一个闭合的平面或曲线几何形状旋转&#xff0c;从而生成一个立体对象。这种方法常用于创建圆柱体、…

大数据Flink学习圣经:一本书实现大数据Flink自由

学习目标&#xff1a;三栖合一架构师 本文是《大数据Flink学习圣经》 V1版本&#xff0c;是 《尼恩 大数据 面试宝典》姊妹篇。 这里特别说明一下&#xff1a;《尼恩 大数据 面试宝典》5个专题 PDF 自首次发布以来&#xff0c; 已经汇集了 好几百题&#xff0c;大量的大厂面试…

【制作npm包4】api-extractor 学习

制作npm包目录 本文是系列文章&#xff0c; 作者一个橙子pro&#xff0c;本系列文章大纲如下。转载或者商业修改必须注明文章出处 一、申请npm账号、个人包和组织包区别 二、了解 package.json 相关配置 三、 了解 tsconfig.json 相关配置 四、 api-extractor 学习 五、npm包…

Dockerfile自定义镜像

文章目录 Dockerfile自定义镜像镜像结构Dockerfile语法构建java项目 小结 Dockerfile自定义镜像 常见的镜像在DockerHub就能找到&#xff0c;但是我们自己写的项目就必须自己构建镜像了。 而要自定义镜像&#xff0c;就必须先了解镜像的结构才行。 镜像结构 镜像是将应用程序及…

服务器数据库中了360后缀勒索病毒怎么办?360后缀勒索病毒的加密形式

随着信息技术的发展&#xff0c;企业的计算机服务器数据库变得越来越重要。然而&#xff0c;在数字时代&#xff0c;网络上的威胁也日益增多。近期&#xff0c;我们收到很多企业的求助&#xff0c;企业的计算机服务器遭到了360后缀勒索病毒的攻击&#xff0c;导致服务器内的所有…

《TCP IP网络编程》第二十四章

第 24 章 制作 HTTP 服务器端 24.1 HTTP 概要 本章将编写 HTTP&#xff08;HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff09;服务器端&#xff0c;即 Web 服务器端。 理解 Web 服务器端&#xff1a; web服务器端就是要基于 HTTP 协议&#xff0c;将网页对…

easyx图形库基础:3实现弹球小游戏

实现弹球小游戏 一.实现弹球小游戏:1.初始化布&#xff1a;2.初始化一个球的信息&#xff1a;3.球的移动和碰撞反弹4.底边挡板的绘制和移动碰撞重置数据。 二.整体代码&#xff1a; 一.实现弹球小游戏: 1.初始化布&#xff1a; int main() {initgraph(800, 600);setorigin(40…

[论文笔记]Glancing Transformer for Non-Autoregressive Neural Machine Translation

引言 这是论文Glancing Transformer for Non-Autoregressive Neural Machine Translation的笔记。 传统的非自回归文本生成速度较慢,因为需要给定之前的token来预测下一个token。但自回归模型虽然效率高,但性能没那么好。 这篇论文提出了Glancing Transformer,可以只需要一…