flutter绘制流程——rebuild

rebuild是Element的方法,有两种场景下会被调用:

  1. element第一次构建mount的时候
  2. widget发生变化的时候
void rebuild() {
  if (_lifecycleState != _ElementLifecycle.active || !_dirty)
    return;
  performRebuild();
}

主要逻辑分为2步

  1. 判断状态是否是active,_dirty是否为true
  2. 执行performRebuild(),这是个抽象方法,所以具体rebuild的逻辑由element子类去实现

下面重点来看performRebuild()

performRebuild.png

1 performRebuild()

顾名思义真正执行重新build的地方,因此每个实现类会有所不同,下面看下不同类型Element的实现

1.1 RenderObjectElement

更新renderObject,当然还有一些RenderObjectElement的继承类可能还做了其他逻辑

@override
void performRebuild() {
  widget.updateRenderObject(this, renderObject);
  _dirty = false;
}

1.1.1 SliverMultiBoxAdaptorElement

对于SliverGrid,SliverList,ListView都会用到它,这里逻辑比较多,今天这篇不去细讲,大概了解有关键逻辑 _build(index)和updateChild`,

@override
void performRebuild() {
  super.performRebuild();
  final SplayTreeMap<int, Element?> newChildren = SplayTreeMap<int, Element?>();
  void processElement(int index) {
    //省略...
    final Element? newChild = updateChild(newChildren[index], _build(index), index);
    //省略...
  }
  //省略...
  newChildren.keys.forEach(processElement);
  //省略...
}

1.2 ComponentElement

void performRebuild() {
  Widget? built;
  try {
    //widget重建 如:StatelessElement
    built = build();
  } catch (e, stack) {
  } finally {
    _dirty = false;
  }
  try {
    _child = updateChild(_child, built, slot);
  } catch (e, stack) {
    //..省略
  }
}
  1. 执行build(),作为新的newWidget
  2. _child = updateChild(_child, built, slot);

1.2.1 StatelessElement

未覆写,逻辑同ComponentElement的performRebuild()

1.2.2 StatefulElement

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

在build之前判断需要didChangeDependencies

总结:performRebuild()实现分两类,ComponentElement和RenderObjectElement

  • RenderObjectElement会updateRenderObject,对于有child的继承类会进行_build(index)和updateChild
  • ComponentElement的performRebuild主要分为两步。1:build(); 2:updateChild。下面依次展开

2 build()

2.1 ComponentElement

/// Subclasses should override this function to actually call the appropriate
/// `build` function (e.g., [StatelessWidget.build] or [State.build]) for
/// their widget.
@protected
Widget build();

用来build返回widget,这个我们就很熟悉了,写UI代码主要围绕在这一块

  1. StatelessElement调用widget.build(this);
  2. StatefulElement 调用state.build(this);
  3. ProxyElement 直接返回widget.child

2.2 SliverMultiBoxAdaptorElement

在1.1.1中的performRebuild()执行的是_build

Widget? _build(int index) {
  return widget.delegate.build(this, index);
}

看到SliverChildDelegate中的定义,和ComponentElement的build意思差不多,只不是事根据传入index来返回widget

/// Returns the child with the given index.
///
/// Should return null if asked to build a widget with a greater
/// index than exists. If this returns null, [estimatedChildCount]
/// must subsequently return a precise non-null value (which is then
/// used to implement [RenderSliverBoxChildManager.childCount]).
///
/// Subclasses typically override this function and wrap their children in
/// [AutomaticKeepAlive], [IndexedSemantics], and [RepaintBoundary] widgets.
///
/// The values returned by this method are cached. To indicate that the
/// widgets have changed, a new delegate must be provided, and the new
/// delegate's [shouldRebuild] method must return true.
Widget? build(BuildContext context, int index);

总结:不管是ComponentElement中的build,还是SliverMultiBoxAdaptorElement中的_build,最终都是用来构建一个Widget。

下面看到performRebuild的下一步updateChild。

3 updateChild

3.1 Element/ComponentElement

Element? updateChild(Element? child, Widget? newWidget, Object? newSlot),用来更新子element

Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
  //如果newWidget是null,并且old child非null,直接deactivateChild
  if (newWidget == null) {
    if (child != null)
      deactivateChild(child);
    return null;
  }
  final Element newChild;
  if (child != null) {
    //新旧widget相同的情况
    if (child.widget == newWidget) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      newChild = child;
    } else if (Widget.canUpdate(child.widget, newWidget)) {
      //可以update的情况,也就是runtimetype和key相同
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      child.update(newWidget);
      newChild = child;
    } else {
      //其他情况,移除旧的,重新inflateWidget新的widget,会创建element
      deactivateChild(child);
      newChild = inflateWidget(newWidget, newSlot);
    }
  } else {
    //old child是null,这里直接inflate新的widget,会创建element
    newChild = inflateWidget(newWidget, newSlot);
  }
  return newChild;
}

总共分以下几种情况

  1. newWidget是null,则清理掉old child(如果old child不为null),返回null
  2. 新旧widget相同,说明数据没有变化,直接返回旧的child
  3. widget可以update,child.update(newWidget); 直接更新old child即可
  4. 其他情况,重新inflateWidget,这里会createElement,清理掉old child(如果old child不为null)

3.2 SliverMultiBoxAdaptorElement

RenderObjectElement的实现类SliverMultiBoxAdaptorElement额外处理的就是更新child的renderobject的parentData

@override
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
  final SliverMultiBoxAdaptorParentData? oldParentData = child?.renderObject?.parentData as SliverMultiBoxAdaptorParentData?;
  final Element? newChild = super.updateChild(child, newWidget, newSlot);
  final SliverMultiBoxAdaptorParentData? newParentData = newChild?.renderObject?.parentData as SliverMultiBoxAdaptorParentData?;

  // Preserve the old layoutOffset if the renderObject was swapped out.
  if (oldParentData != newParentData && oldParentData != null && newParentData != null) {
    newParentData.layoutOffset = oldParentData.layoutOffset;
  }
  return newChild;
}

下面看到update的逻辑

4 update

Element中定义,更新widget

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

4.1 StatelessElement

void update(StatelessWidget newWidget) {
  super.update(newWidget);
  _dirty = true;
  rebuild();
}

4.2 StatefulElement

void update(StatefulWidget newWidget) {
  super.update(newWidget);
  final StatefulWidget oldWidget = state._widget!;
  _dirty = true;
  state._widget = widget as StatefulWidget;
  state.didUpdateWidget(oldWidget) as dynamic;
  rebuild();
}

4.3 RenderObjectElement

void update(covariant RenderObjectWidget newWidget) {
  super.update(newWidget);
  widget.updateRenderObject(this, renderObject);
  _dirty = false;
}

下面看几个典型的实现类

4.3.1 SingleChildRenderObjectElement

@override
void update(SingleChildRenderObjectWidget newWidget) {
  super.update(newWidget);
  assert(widget == newWidget);
  _child = updateChild(_child, widget.child, null);
}

4.3.2 MutliChildRenderObjectElement

@override
void update(MultiChildRenderObjectWidget newWidget) {
  super.update(newWidget);
  _children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren);
  _forgottenChildren.clear();
}

总结:对于update方法首先一定会做的就是更新_widget。然后对于ComponentElement和RenderObjectElement的逻辑有所不同。

  • ComponentElement主要会进行rebuild();这样又回到最初的rebuild,只是到了子节点
  • RenderObjectElement则会更新自己的renderObject,然后根据拥有child是否是多个逻辑有所不同,如:
    • SingleChildRenderObjectElement只有一个child,执行updateChild 这样也回到了前面的步骤3
    • MutliChildRenderObjectElement可能有多个child,执行updateChildren

下面重点开看updateChildren

5 updateChildren

RenderObjectElement中定义,针对可能有多个child的element的更新逻辑

5.1 定义新旧children的开始和结束位置,用于后面遍历

//定义old和new的首尾位置
int newChildrenTop = 0;
int oldChildrenTop = 0;
int newChildrenBottom = newWidgets.length - 1;
int oldChildrenBottom = oldChildren.length - 1;
//根据old和new的长度判断,如果相同则newChildren直接使用oldChildren,如果不同,则创建一个长度为newWidgets.length的list,使用_NullElement.instance来填充
final List<Element> newChildren = oldChildren.length == newWidgets.length ?
oldChildren : List<Element>.filled(newWidgets.length, _NullElement.instance, growable: false);

5.2 从开始位置遍历children,主要处理可以直接updateChild的情况,碰到不能update则直接跳出,newChildrenTop和oldChildrenTop会指向不能update的child位置

// Update the top of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
  //判断oldChild是否被移除
  final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
  final Widget newWidget = newWidgets[newChildrenTop];
  //oldChild是空或者newWidget不能直接更新,就跳出
  if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
    break;
  //更新child
  final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
  //设置到newChild中
  newChildren[newChildrenTop] = newChild;
  //设置上一个child
  previousChild = newChild;
  //移动到下一个位置
  newChildrenTop += 1;
  oldChildrenTop += 1;
}

5.3 从底部开始遍历判断canUpdate,知道返回false,跳出,这样oldChildrenBottom和newChildrenBottom指向末尾不能update的child

// Scan the bottom of the list.
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;
  //注意这里和step2的区别是没有去设置previousChild了,并且没有updateChild
  oldChildrenBottom -= 1;
  newChildrenBottom -= 1;
}

5.4 从oldChildrenTop开始遍历oldChildren,取出widget.key不为null的child,存入oldKeyedChildren,后面可能取出进行复用,这里oldChildrenTop应该等于oldChildrenBottom+1

// Scan the old children in the middle of the list.
// 根据top和bottom位置判断是否还存在中间的元素没有处理
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
//用于存放有key的old child
Map<Key, Element>? oldKeyedChildren;
if (haveOldChildren) {
  oldKeyedChildren = <Key, Element>{};
  //从顶部开始遍历oldChildren
  while (oldChildrenTop <= oldChildrenBottom) {
    final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
    if (oldChild != null) {
      //如果有key,则存入oldKeyedChildren
      if (oldChild.widget.key != null)
        oldKeyedChildren[oldChild.widget.key!] = oldChild;
      else
        //没有直接废弃oldChild
        deactivateChild(oldChild);
    }
    //注意:这里是old child的位置移动
    oldChildrenTop += 1;
  }
}

5.5 从newChildrenTop开始遍历newWidgets,根据key从oldKeyedChildren取出old child,然后判断是否可以直接update,如果可以则在updateChild的作为oldChild参数传入,否则传null。到这里newChildrenTop应该等于newChildrenBottom+1

// Update the middle of the list.
//从顶部更新newChildren
while (newChildrenTop <= newChildrenBottom) {
  Element? oldChild;
  final Widget newWidget = newWidgets[newChildrenTop];
  if (haveOldChildren) {
    final Key? key = newWidget.key;
    //判断new child是否有key
    if (key != null) {
      //获取old child有相同key的child
      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;
}

5.6 在4.3中只是做了newChildrenBottom和oldChildrenBottom的标记,并没有真正的updateChild,所以。下面重置newChildrenBottom和oldChildrenBottom。继续从oldChildrenTop开始遍历,然后updateChild

// We've scanned the whole list.
//重置bottom位置
newChildrenBottom = newWidgets.length - 1;
oldChildrenBottom = oldChildren.length - 1;

// Update the bottom of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
  final Element oldChild = oldChildren[oldChildrenTop];
  final Widget newWidget = newWidgets[newChildrenTop];
  //更新剩余的child
  final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
  newChildren[newChildrenTop] = newChild;
  previousChild = newChild;
  newChildrenTop += 1;
  oldChildrenTop += 1;
}

5.7 清理没有复用成功的child

  // Clean up any of the remaining middle nodes from the old list.
  if (haveOldChildren && oldKeyedChildren!.isNotEmpty) {
    for (final Element oldChild in oldKeyedChildren.values) {
      //将剩下带有key的old child,同时又没能复用的child进行clean
      if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
        deactivateChild(oldChild);
    }
  }
  return newChildren;
}

总结:这么长的流程和逻辑主要是为了对多个children的情况要进行判断是否可以复用,对于不能复用的child进行清理,最终针对child还是会执行到updateChild这样又回到了3

6 inflateWidget

Element中定义,在第3节中 updateChild如果old child是空或者无法update就需要inflateWidget

写android的朋友应该很熟悉了,android里有LayoutInflater.from().inflate(),从xml来解析获取到View;同样在这里通过widget来解析返回Element。关于GlobalKey的逻辑,我们先忽略,后面再介绍。下面的逻辑就简单了,创建一个element,然后mount到当前element

Element inflateWidget(Widget newWidget, Object? newSlot) {
  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!;
    }
  }
  //创建子Element
  final Element newChild = newWidget.createElement();
  //将child挂载到当前element
  newChild.mount(this, newSlot);
  //返回child element
  return newChild;
}

7 总结

走完上面整个rebuild流程,第一感受就是在于ComponentElement和RenderObjectElement在流程上有明显的区别,这也回到这两类Element的设计,RenderObjectElement不一定包含子child,但它包括renderObject用于渲染,而ComponentElement是一种组成的Element,它并不包含RenderObject,但它会有子 Element。因此在rebuild时ComponentElement只需要关心child的update,而RenderObjectElement还需要关注RenderObject的更新。另外在多child的情况如:第5节,diff的逻辑会稍微复杂一点。

对于整个流程中的关于方法我们也要熟悉,如:update,inflateWidget,updateChild。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容