Flutter中的布局绘制流程简析(一)

抽象根系
• 阅读 12209

开始

Flutter对比前端流行的框架,除了构建控件树和控件状态管理等,还多了布局和绘制的流程,布局和绘制以往都是前端开发可望而不可及的都被封锁在浏览器渲染引擎的实现里面,而我们只能通过文档或者做一些demo去深入,就像盲人摸象,很多时候都是只知其一不知其二。相对而言,Flutter把这个黑盒打开了,意味着我们可以做更加深入的优化,开发效率也能成倍提高。
接下来就去深入去了解,尽可能把这个过程完整展现给大家。

入口

界面的布局和绘制在每一帧都在发生着,甚至界面没有变化,它也会存在;可以想象每一帧里面,引擎都像流水线的一样重复着几个过程:build(构建控件树),layout(布局), paint(绘制)和 composite(合成),周而复始。那么驱动整个流水线的入口在哪里呢?
直接来到WidgetBinding.drawFrame方法:

 void drawFrame() {
    ...
    try {
      if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement);
      super.drawFrame();
      buildOwner.finalizeTree();
    } finally {
     ...
    }
    ...
  }

这里renderViewElement就是Root了,在第一帧的时候,控件树还没有构建,当然也不存在renderViewElement了;而接下来buildOwner这个对象是干嘛的呢?

BuilderOwner

先看一下从哪里开始会用到builderOwner的方法:
Flutter中的布局绘制流程简析(一)

可以看到我们经常使用setState方法就与BuilderOwner紧密关联了,接着再看BuilderOwner.scheduleBuildFor方法:

void scheduleBuildFor(Element element) {
    ...
    if (element._inDirtyList) {
      ...
      _dirtyElementsNeedsResorting = true;
      return;
    }
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled();
    }
    _dirtyElements.add(element);
    element._inDirtyList = true;
   ...
  }

这里的处理过程:如果_scheduledFlushDirtyElements不为true,就调起onBuildScheduled方法,并把Elment都加入到_dirtyElements中,那么onBuildScheduled又会干些啥尼?
回到WidgetBinding.initInstances方法:

 void initInstances() {
    super.initInstances();
    ...
    buildOwner.onBuildScheduled = _handleBuildScheduled;
    ...
  }

看到真实调用的是WidgetBinding._handleBuildScheduled方法,我们继续完善刚才的调用过程:
Flutter中的布局绘制流程简析(一)
所以这里就可以看到我们调用setState方法最终会触发界面新的一帧绘制。

当触发新的一帧时,我们又回到最初的WidgetBinding.drawFrame方法中,那么builderOwner.buildScope方法究竟会干些工作:

void buildScope(Element context, [VoidCallback callback]) {
    if (callback == null && _dirtyElements.isEmpty)
      return;
    ..l
    Timeline.startSync('Build', arguments: timelineWhitelistArguments);
    try {
      _scheduledFlushDirtyElements = true;
      if (callback != null) {
        
        _dirtyElementsNeedsResorting = false;
        try {
          callback();
        } finally {
         ...
        }
      }
      _dirtyElements.sort(Element._sort);
      _dirtyElementsNeedsResorting = false;
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      while (index < dirtyCount) {
        ...
        try {
          _dirtyElements[index].rebuild();
        } catch (e, stack) {
          ...
        }
        index += 1;
        if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
          _dirtyElements.sort(Element._sort);
          _dirtyElementsNeedsResorting = false;
          dirtyCount = _dirtyElements.length;
          while (index > 0 && _dirtyElements[index - 1].dirty) {
            index -= 1;
          }
        }
      }
        ...
        return true;
      }());
    } finally {
      for (Element element in _dirtyElements) {
        assert(element._inDirtyList);
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
      _scheduledFlushDirtyElements = false;
      _dirtyElementsNeedsResorting = null;
      Timeline.finishSync();
    }
  }

首先把_scheduledFlushDirtyElements标记设为true,表示正在从新构建新的控件树,然后_dirtyElements会做一轮排序,看一下Element._sort的方法如何实现的:

static int _sort(Element a, Element b) {
    if (a.depth < b.depth)
      return -1;
    if (b.depth < a.depth)
      return 1;
    if (b.dirty && !a.dirty)
      return -1;
    if (a.dirty && !b.dirty)
      return 1;
    return 0;
  }

嗯,因为在这里最初排序都是标记为dirty的Element,所以最后的结果是,depth小的Element会排最前,depth大的排最后;也就是说父Element会比子Element更早被rebuild,这样可以防止子Element会重复rebuild。
当在rebuild过程中有可能会加入新的Dirty Element,所以每次rebuild的时候都会重新检查_dirtyElements是否有增加或者检查_dirtyElementsNeedsResorting标记位,接着从新排序一遍,这个时候我们的_dirtyElements列表中就有可能存在之前已经rebuild完,dirty为false的Element了,重新排序后,depth小的和dirty不为true的会排最前,重新把index定位到第一个Dirty Element继续rebuild。
如果在这个过程我们想把已经rebuild过一次的Element想重复加入到_dirtyElements中,形成死循环,会怎样的尼,这个时候Element._inDirtyList还是为true,表明Element已经在_dirtyElements列表中,在开发模式下引擎会报错,给出相应提示;一般情况下是不应该出现的,万一出现就需要思考一下代码是否合理了。

接着先跳过super.drawFrame方法,来到builderOwner.finalizeTree方法:

void finalizeTree() {
    Timeline.startSync('Finalize tree', arguments: timelineWhitelistArguments);
    try {
      lockState(() {
        _inactiveElements._unmountAll(); // this unregisters the GlobalKeys
      });
     ...
    } catch (e, stack) {
      _debugReportException('while finalizing the widget tree', e, stack);
    } finally {
      Timeline.finishSync();
    }
  }

主要把_inactiveElements都进行一次清理,所以使用GlobalKey的控件,如果想起到重用控件的效果,必须在同一帧里面完成“借用”,否则就会被清理了。

简单总结一下BuilderOwner的功能就是:管理控件rebuild过程,让控件有序的进行rebuild。

PipelineOwner

终于来到super.drawFrame方法,这个方法实际上调起的是RenderBinding.drawFrame方法:

void drawFrame() {
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    renderView.compositeFrame(); // this sends the bits to the GPU
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
  }

我们又见到一个跟BuilderOwner名称很相似的PipelineOwner,那PipelineOwner又起到什么样的功能尼?直接深入
pipelineOwner.flushLayout方法:

void flushLayout() {
    Timeline.startSync('Layout', arguments: timelineWhitelistArguments);
    _debugDoingLayout = true;
    try {
      while (_nodesNeedingLayout.isNotEmpty) {
        final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
        _nodesNeedingLayout = <RenderObject>[];
        for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
          if (node._needsLayout && node.owner == this)
            node._layoutWithoutResize();
        }
      }
    } finally {
      _debugDoingLayout = false;
      Timeline.finishSync();
    }
  }

跟builderOwner处理相似,先进行一次排序,depth小的排最前优先处理,然后调起RenderObject._layoutWithoutResize方法。

暂时先整理一下,这个时候我们出现三个名词:Widget,Element,RenderObject;它们的关系究竟是咋样的尼,假设你熟悉前端的Vue或者React框架,它们的关系等同于下面这张图:

Flutter中的布局绘制流程简析(一)

也就是说RenderObject负责着界面的布局绘制和事件处理等;而Element则是进行virtual dom diff,并且负责创建RenderObject;Widget则是我们控件业务逻辑组织的地方, 负责创建Element。

大概可以想到PipelineOwner的主要功能:负责管理那些dirty render object,让它们进行布局和绘制。

接着RenderObject._layoutWithoutResize方法:

void _layoutWithoutResize() {
    ...
    try {
      performLayout();
      markNeedsSemanticsUpdate();
    } catch (e, stack) {
      _debugReportException('performLayout', e, stack);
    }
    ...
    _needsLayout = false;
    markNeedsPaint();
  }

可以看到其实直接调用了RenderObject.performLayout方法,而这个方法则是应由开发者自己实现的布局逻辑,接着会调起RenderObject.markNeedsPaint方法,也就是说每次重新layout都会触发一次paint。

void markNeedsPaint() {
    if (_needsPaint)
      return;
    _needsPaint = true;
    if (isRepaintBoundary) {
      if (owner != null) {
        owner._nodesNeedingPaint.add(this);
        owner.requestVisualUpdate();
      }
    } else if (parent is RenderObject) {
      final RenderObject parent = this.parent;
      parent.markNeedsPaint();
    } else {
      if (owner != null)
        owner.requestVisualUpdate();
    }
  }

这里的逻辑,主要判断当前的RenderObject.isRepaintBoundary是否为true,如果是则把当前RenderObject加入到PipelineOwner对应的列表中等待接下来的flushPaint处理,并触发下一帧的绘制;当isRepaintBoundary不为true的时候,则会一直往上查找直到找到isRepaintBoundary为true的RenderObject,也就是有可能会找到根节点RenderView,然后加入到_nodesNeedingPaint列表中:

class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> {
    ...
    bool get isRepaintBoundary => true;
    ...
}

这样的话我们就得注意了,如果经常需要重绘区域,最好把isRepaintBoundary标记true,这样就尽量避免触发全局重绘,提高性能,对应的flutter就已经提供了一个RepaintBoundary控件,自动把isRepaintBoundary标记为true,非常方便我们去做优化。

既然有markNeedsPaint方法,当然也有markNeedsLayout方法:

void markNeedsLayout() {
    if (_needsLayout) {
      return;
    }
    if (_relayoutBoundary != this) {
      markParentNeedsLayout();
    } else {
      _needsLayout = true;
      if (owner != null) {
        ...
        owner._nodesNeedingLayout.add(this);
        owner.requestVisualUpdate();
      }
    }
  }

处理逻辑基本上跟markNeedsPaint差不多,_relayoutBoundary也可以减少全局重新布局,可以把布局范围缩小,提高性能,但是_relayoutBoundary的设置是有点不一样的,等会再去讨论。

简单整理一下

当我们用调起setState改变某些状态,例如:控件的高度;先回到BuilderOwner.buildScope,继续dirty element的rebuild方法:

void rebuild() {
    if (!_active || !_dirty)
      return;
    performRebuild();
  }

接着执行performRebuild方法:

  void performRebuild() {
    Widget built;
    try {
      built = build();
      debugWidgetBuilderValue(widget, built);
    } catch (e, stack) {
      _debugReportException('building $this', e, stack);
      built = new ErrorWidget(e);
    } finally {
      // We delay marking the element as clean until after calling build() so
      // that attempts to markNeedsBuild() during build() will be ignored.
      _dirty = false;
      assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
    }
    try {
      _child = updateChild(_child, built, slot);
      assert(_child != null);
    } catch (e, stack) {
      _debugReportException('building $this', e, stack);
      built = new ErrorWidget(e);
      _child = updateChild(null, built, slot);
    }
  }

控件会重新build出子控件树,然后调起updateChild方法:

 Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    if (child != null) {
      if (child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
      }
      if (Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        return child;
      }
      deactivateChild(child);
    }
    return inflateWidget(newWidget, newSlot);
  }
  1. 如果newWidget为null但是child不为null,也就是删除原来的控件,就会调起deactivateChild方法,会把当前的Element加入到BuilderOwner._inactiveElements列表中(最后可能会被清除也可能会被重用)。

  2. 如果newWidget和child都不为null,也就是更新原来的控件,先调起Widget.canUpdate方法判断是否能够更新(一般都是根据Widget运行时类型是否相同来判断),如果相同调起update方法,继续更新的逻辑,如果不一样,就要deactivate原来的控件,并且创建新的控件。

  3. 如果child为null而Widegt不为null,也就是要创建新的控件。

接下来会分别分析更新的逻辑和创建的逻辑:

  • 更新

直接来到StatefulElement.update方法:

void update(StatefulWidget newWidget) {
    super.update(newWidget);
    final StatefulWidget oldWidget = _state._widget;
    _dirty = true;
    _state._widget = widget;
    try {
      _state.didUpdateWidget(oldWidget);
    } finally {
    }
    rebuild();
  }

这里首先会调起一个控件很重要的生命回调didUpdateWidget,综合上述可以知道,这里是当新的子控件和旧的子控件类型一致时才会调起;接着就是子控件的rebuild过程,然后不停重复下去。

  • 创建

直接来到Element.inflateWidget方法:

Element inflateWidget(Widget newWidget, dynamic 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;
      }
    }
    final Element newChild = newWidget.createElement();
    newChild.mount(this, newSlot);
    return newChild;
  }

这里判断key是否为GlobalKey,如果是会调起_retakeInactiveElement方法,目的是从Globalkey上重用控件,并把控件从BuilderOwner._inactiveElements列表上移除,防止它被unmount,接着就是从新跑一次updateChild流程;如果不是就在新的子控件上创建新的Element,并且mount上去。

但是如果多个child的时候是怎么更新的尼?
来到MultiChildRenderObjectElement.update方法:

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

框架里面好像只规定跟RenderObject相关的控件才可以支持多个child,而updateChildren就是一个flutter版本的virtual dom diff算法的实现。

刚才假设我们需要修改控件的高度,既然跟显示有关,必然跟RenderObejct相关,直接来到RenderObjectElement.update方法:

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

最后调起的是RenderObjectWidget.updateRenderObject方法,在这里我们可以得到新创建的RenderObject,我们在这里把新的RenderObject的属性赋值给旧的RenderObject,而在RenderObject相关属性的setter方法中会调起markNeedsLayout方法,这样在下一帧布局绘制的时候就会生效。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
浩浩 浩浩
4年前
【Flutter实战】线性布局(Row、Column)
4.2线性布局(Row和Column)所谓线性布局,即指沿水平或垂直方向排布子组件。Flutter中通过Row和Column来实现线性布局,类似于Android中的LinearLayout控件。Row和Column都继承自Flex,我们将在弹性布局一节中详细介绍Flex。主轴和纵轴对于线性布局,有主轴和纵轴之分,如果
浩浩 浩浩
4年前
【Flutter实战】布局类组件简介
4.1布局类组件简介布局类组件都会包含一个或多个子组件,不同的布局类组件对子组件排版(layout)方式不同。我们在前面说过Element树才是最终的绘制树,Element树是通过Widget树来创建的(通过Widget.createElement()),Widget其实就是Element的配置数据。在Flutter中,根据Widget是否
Stella981 Stella981
3年前
Flutter获取Build完成状态监听 及每一帧绘制完成的监听
Flutter在Build完成后的监听和每一帧绘制完成后的监听这个是我们监听要用的重要的类WidgetsBinding官方是这么描述它的ThegluebetweenthewidgetslayerandtheFlutterengine.中文的意思是 控件层和Flutter引擎之间的粘合剂。就
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Stella981 Stella981
3年前
Flutter 布局控件完结篇
本文对Flutter的29种布局控件进行了总结分类,讲解一些布局上的优化策略,以及面对具体的布局时,如何去选择控件。1\.系列文章1.Flutter布局详解(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fgithub.com%2Fyang72296
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这