透过源码理解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:

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

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

@override
  StatefulElement createElement() => StatefulElement(this);

@protected
  @factory
  State 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);

  
  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }

 
  @protected
  void didUnmountRenderObject(covariant RenderObject renderObject) { }

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

3.1 ColoredBox

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

final Color color;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _RenderColoredBox(color: color);
  }

@override
  void 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();
  }

  @override
  void 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.重新构建widget
  super.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);

  @override
  Widget 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。这里比较的是hashCode
      if (child.widget == newWidget) {
        if (child.slot != newSlot) {
          updateSlotForChild(child, newSlot);
        }
        newChild = child;
      } else if (Widget.canUpdate(child.widget, newWidget)) {
        //两个widget不同,但是可以复用。位置不同则先更新位置。然后用新widget更新element
        if (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:

 @override
  void update(StatelessWidget newWidget) {
    //直接更换widget
    super.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和通知:

@override
  void update(ProxyWidget newWidget) {
    final ProxyWidget oldWidget = widget as ProxyWidget;
    //使用新的widget更新持有的widget
    super.update(newWidget);
    //通知其他关联widget自己发生了变化
    updated(oldWidget);
    //标记dirty
    rebuild(force: true);
  }

  @protected
  void 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获取对应的旧Element
          oldChild = 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 later
              oldKeyedChildren.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的Element
    if (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,重新创建了Element
      final 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:

@override
  void 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:

@override
  void _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 {
  @override
  Widget 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;

  @override
  void initState() {
    super.initState();
    widgets = [
      StatelessColorfulTile(),
      StatelessColorfulTile()
    ];
  }

  @override
  Widget 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();

  @override
  Widget 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);

  @override
  StatefulColorfulTileState createState() => StatefulColorfulTileState();
}

class StatefulColorfulTileState extends State<StatefulColorfulTile> {
  final Color _color = Utils.randomColor();

  @override
  Widget 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.重新构建widget
  super.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博客

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

8.3.tensorRT高级(3)封装系列-tensor封装,索引计算,内存标记及自动复制

目录 前言1. Tensor封装总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记。 本次课程学习 tensorRT 高级-tensor封装&#xff0c;索引计算&a…

【Spring系列篇--关于IOC的详解】

目录 面试经典题目&#xff1a; 1. 什么是spring&#xff1f;你对Spring的理解&#xff1f;简单来说&#xff0c;Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。 2.什么是IoC&#xff1f;你对IoC的理解&#xff1f;IoC的重要性?将实例化对象的权利从程序员…

echart 3d立体颜色渐变柱状图

如果可以实现记得点赞分享&#xff0c;谢谢老铁&#xff5e; 1.需求描述 根据业务需求将不同的法律法规&#xff0c;展示不同的3d立体渐变柱状图。 2.先看下效果图 3. 确定三面的颜色&#xff0c;这里我是自定义的颜色 // 右面生成颜色const rightColorArr ref(["#79D…

自动驾驶仿真:基于Carsim开发的加速度请求模型

文章目录 前言一、加速度输出变量问题澄清二、配置Carsim动力学模型三、配置Carsim驾驶员模型四、添加VS Command代码五、Run Control联合仿真六、加速度模型效果验证 前言 1、自动驾驶行业中&#xff0c;算法端对于纵向控制的功能预留接口基本都是加速度&#xff0c;我们需要…

git环境超详细配置说明

一&#xff0c;简介 在git工具安装完成之后&#xff0c;需要设置一下常用的配置&#xff0c;如邮箱&#xff0c;缩写&#xff0c;以及git commit模板等等。本文就来详细介绍些各个配置如何操作&#xff0c;供参考。 二&#xff0c;配置步骤 2.1 查看当前git的配置 git conf…

解决ios隔空播放音频到macos没有声音的问题

解决ios隔空播放音频到macos没有声音的问题 一、检查隔空播放支持设备和系统要求二、打开隔空播放接收器三、重置MAC控制中心进程END 一、检查隔空播放支持设备和系统要求 Mac、iPhone、iPad 和 Apple Watch 上“连续互通”的系统要求 二、打开隔空播放接收器 ps;我设备是同一…

基于web的停车场收费管理系统/基于springboot的停车场管理系统

摘 要 随着汽车工业的迅猛发展&#xff0c;我国汽车拥有量急剧增加。停车场作为交通设施的组成部分,随着交通运输的繁忙和不断发展&#xff0c;人们对其管理的要求也不断提高&#xff0c;都希望管理能够达到方便、快捷以及安全的效果。停车场的规模各不相同,对其进行管理的模…

剪枝基础与实战(3): 模型剪枝和稀疏化训练流程

Model Pruning 相关论文:Learning Efficient Convolutional Networks through Network Slimming (ICCV 2017) 考虑一个问题,深度学习模型里面的卷积层出来之后的特征有非常多,这里面会不会存在一些没有价值的特征及其相关的连接?又如何去判断一个特征及其连接是否有价值? …

redis实战-缓存数据解决缓存与数据库数据一致性

缓存的定义 缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码。防止过高的数据访问猛冲系统,导致其操作线程无法及时处理信息而瘫痪&#xff0c;这在实际开发中对企业讲,对产品口碑,用户评价都是致命的;所以企业非常重视缓存…

【汇编语言】使用DS和[address]实现字的传送

文章目录 要解决的问题&#xff1a;CPU从内存单元中读取数据字的传送 要解决的问题&#xff1a;CPU从内存单元中读取数据 1、要求&#xff1a;CPU要读取一个内存单元时&#xff0c;必须先给出这个内存单元的地址&#xff1b; 2、原理&#xff1a;8086设备中&#xff0c;内存地…

LeetCode--HOT100题(35)

目录 题目描述&#xff1a;23. 合并 K 个升序链表&#xff08;困难&#xff09;题目接口解题思路1代码解题思路2代码 PS: 题目描述&#xff1a;23. 合并 K 个升序链表&#xff08;困难&#xff09; 给你一个链表数组&#xff0c;每个链表都已经按升序排列。 请你将所有链表合…

git压缩/合并多次commit提交为1次commit提交

git压缩/合并N次commit提交为1次commit提交 假设有最近3次提交&#xff1a; commit_id1 commit_id2 commit_id3目标是把以上3次commit合并成1个commit&#xff0c;注意&#xff0c;最新的commit提交在最上面。 在git bash里面的操作步骤&#xff1a; &#xff08;1&#xff0…

【周末闲谈】关于“数据库”你又知道多少?

个人主页&#xff1a;【&#x1f60a;个人主页】 系列专栏&#xff1a;【❤️周末闲谈】 系列目录 ✨第一周 二进制VS三进制 ✨第二周 文心一言&#xff0c;模仿还是超越&#xff1f; ✨第二周 畅想AR 文章目录 系列目录前言数据库数据库的五大特点数据库介绍数据库管理系统&a…

Oracle故障案例之-19C时区补丁DSTV38更新

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&#x1f61…

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

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

2023国赛数学建模E题思路模型代码 高教社杯

本次比赛我们将会全程更新思路模型及代码&#xff0c;大家查看文末名片获取 之前国赛相关的资料和助攻可以查看 2022数学建模国赛C题思路分析_2022国赛c题matlab_UST数模社_的博客-CSDN博客 2022国赛数学建模A题B题C题D题资料思路汇总 高教社杯_2022国赛c题matlab_UST数模社…

RK3588平台开发系列讲解(AI 篇)RKNN-Toolkit2 API 介绍

文章目录 一、RKNN 初始化及对象释放二、RKNN 模型配置沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇章主要讲解 RKNN-Toolkit2 API 详细说明。 一、RKNN 初始化及对象释放 在使用 RKNN Toolkit2 的所有 API 接口时,都需要先调用 RKNN()方法初始化 RKNN 对象,…

图数据库_Neo4j和SpringBoot整合使用_实战创建明星关系图谱---Neo4j图数据库工作笔记0010

然后我们再来看一下这个明星关系图谱 可以看到这里 这个是原来的startRelation 我们可以写CQL去查询对应的关系 可以看到,首先查询出来以后,然后就可以去创建 我们可以把写的创建明星关系的CQL,拿到 springboot中去执行 可以看到,这里我们先写一个StarRelationRepository,然…

工作6年了日期时间格式化还在写YYYY疯狂给队友埋雷

前言 哈喽小伙伴们好久不见&#xff0c;今天来个有意思的雷&#xff0c;看你有没有埋过。 正文 不多说废话&#xff0c;公司最近来了个外地回来的小伙伴&#xff0c;在广州工作过6年&#xff0c;也是一名挺有经验的开发。 他提交的代码被小组长发现有问题&#xff0c;给打回了&…

【C语言练习】数组OJ题

目录 一.消失的数字思路1&#xff1a;思路2&#xff1a; 二.移除元素三.轮转数组四.删除有序数组中的重复项五.合并两个有序数组 一.消失的数字 题目&#xff1a; 思路1&#xff1a; 数组是从0加到N&#xff0c;所以把0到N的数加起来减去数组中的值&#xff0c;结果就是消失…