Home Flutter页面绘制梳理
Post
Cancel

Flutter页面绘制梳理

RenderObject

markNeedsCompositingBitsUpdate()

主要变量

  • _needsCompositingBitsUpdate
  • needsCompositing. 若为true, layer需要被创建,在绘制时进行图层合成

作用

将合成状态置为dirty, 也就是,needsCompositing变量需要被重新计算(重新计算时机是PipelineOwner.flushCompositingBits

处理逻辑

除将自身的_needsCompositingBitsUpdate置为true之外,还会通知parent.markNeedsCompositingBitsUpdate(),形成链式调用,直到parentRenderObject子类,或者自身或者parentisRepaintBoundary= true

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void markNeedsCompositingBitsUpdate() {
    if (_needsCompositingBitsUpdate)
      return;
    _needsCompositingBitsUpdate = true;
    if (parent is RenderObject) {
      final RenderObject parent = this.parent;
      if (parent._needsCompositingBitsUpdate)
        return;
      if (!isRepaintBoundary && !parent.isRepaintBoundary) {
        parent.markNeedsCompositingBitsUpdate();
        return;
      }
    }
    assert(() {
      final AbstractNode parent = this.parent;
      if (parent is RenderObject)
        return parent._needsCompositing;
      return true;
    }());
    // parent is fine (or there isn't one), but we are dirty
    if (owner != null)
      owner._nodesNeedingCompositingBitsUpdate.add(this);
  }

在断言中的parent._needsCompositing == true背后的逻辑:

  • 能允许到这里,说明parent.isRepaintBoundary == true

  • RenderObject的构造方法:

    1
    2
    3
    
    RenderObject() {
        _needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
    }
    

所以就有了:parent._needsCompositing == true

最后一行代码:owner._nodesNeedingCompositingBitsUpdate.add(this), 将向上遍历到尽头的RenderObject添加到owner._nodesNeedingCompositingBitsUpdate队列。在下次PipelineOwner.flushCompositingBits时,便会遍历该队列,重新计算RenderObjectneedsCompisiting值。PipelineOwner的部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final List<RenderObject> _nodesNeedingCompositingBitsUpdate = <RenderObject>[];

void flushCompositingBits() {
    if (!kReleaseMode) {
        Timeline.startSync('Compositing bits');
    }
    _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
    for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
        if (node._needsCompositingBitsUpdate && node.owner == this)
            node._updateCompositingBits();
    }
    _nodesNeedingCompositingBitsUpdate.clear();
    if (!kReleaseMode) {
        Timeline.finishSync();
    }
}

接着,会调用RenderObject._updateCompositingBits()方法,去更新RenderObjectneedsCompisiting值,看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void _updateCompositingBits() {
    if (!_needsCompositingBitsUpdate)
      return;
    final bool oldNeedsCompositing = _needsCompositing;
    _needsCompositing = false;
    visitChildren((RenderObject child) {
      child._updateCompositingBits();
      if (child.needsCompositing)
        _needsCompositing = true;
    });
    if (isRepaintBoundary || alwaysNeedsCompositing)
      _needsCompositing = true;
    if (oldNeedsCompositing != _needsCompositing)
      markNeedsPaint();
    _needsCompositingBitsUpdate = false;
}

只有在_needsCompositingBitsUpdate == true时才会计算_needsCompositing, 这与上面的结论是一致的。

该方法会一直向下遍历,只要有一个childneedsCompositing == true, 那自身也会置为true。并且,一旦needsCompositing 有变化,便会调用markNeedsPaint()重绘。

流程图

markNeedsCompositingBitsUpdate() -> parent1 -> parent2… -> parent3

PipelineOwner._nodesNeedingCompositingBitsUpdate add parent3

PipelineOwner.flushCompositingBits()

_updateCompositingBits() -> child2 -> child1 -> … -> child0

markNeedsPaint()

主要变量

  • _needsPaint
  • isRepaintBoundary

作用

标记该RenderObject需要被重新绘制。等待下一次集中绘制。

处理逻辑

除了标记自身_needsPaint = true外,还会调用parent.markNeedsPaint(),形成链式调用,直到isRepaintBoundary == true或者parent不是RenderObject.

源码

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
void markNeedsPaint() {
    assert(owner == null || !owner.debugDoingPaint);
    if (_needsPaint)
      return;
    _needsPaint = true;
    if (isRepaintBoundary) {
      assert(() {
        if (debugPrintMarkNeedsPaintStacks)
          debugPrintStack(label: 'markNeedsPaint() called for $this');
        return true;
      }());
      // If we always have our own layer, then we can just repaint
      // ourselves without involving any other nodes.
      assert(_layer is OffsetLayer);
      if (owner != null) {
        owner._nodesNeedingPaint.add(this);
        owner.requestVisualUpdate();
      }
    } else if (parent is RenderObject) {
      final RenderObject parent = this.parent;
      parent.markNeedsPaint();
      assert(parent == this.parent);
    } else {
      assert(() {
        if (debugPrintMarkNeedsPaintStacks)
          debugPrintStack(label: 'markNeedsPaint() called for $this (root of render tree)');
        return true;
      }());
      // If we're the root of the render tree (probably a RenderView),
      // 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.
      if (owner != null)
        owner.requestVisualUpdate();
    }
}

如果RenderObjectisRepaintBoundary == true, 表示上访就到此为止了,不能再向上传递了,否则要出大事。接着,将该RenderObject加入到owner._nodesNeedingPaint队列,等待被重绘,并且调用owner.requestVisualUpdate()

如果RenderObject不是边界,表示还能继续上访,那就调用parent.markNeedsPaint()

parent压根就不是RenderObject, 则会调用owner.requestVisualUpdate()

而,这个owner.requestVisualUpdate()就是告诉owner说:我已经准备好被重绘了,你快开始吧。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PipelineOwner {
    PipelineOwner({
        this.onNeedVisualUpdate,
        this.onSemanticsOwnerCreated,
        this.onSemanticsOwnerDisposed,
    });
    
    final VoidCallback onNeedVisualUpdate;
    
    void requestVisualUpdate() {
        if (onNeedVisualUpdate != null)
            onNeedVisualUpdate();
    }
    
}

PipelineOwner在实例化时,onNeedVisualUpdate就被传了进来。那PipelineOwner在哪里被实例化的呢?是RendererBinding.initInstances()时被实例化的,看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
    @override
    void initInstances() {
        super.initInstances();
        _instance = this;
        _pipelineOwner = PipelineOwner(
            onNeedVisualUpdate: ensureVisualUpdate,
            onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
            onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
        );
        window
            ..onMetricsChanged = handleMetricsChanged
            ..onTextScaleFactorChanged = handleTextScaleFactorChanged
            ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
            ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
            ..onSemanticsAction = _handleSemanticsAction;
        initRenderView();
        _handleSemanticsEnabledChanged();
        assert(renderView != null);
        addPersistentFrameCallback(_handlePersistentFrameCallback);
        initMouseTracker();
    }
}

onNeedVisualUpdate被传入的是ensureVisuaUpdate,该方法在SchedulerBinding内部:

1
2
3
4
5
6
7
8
9
10
11
12
void ensureVisualUpdate() {
    switch (schedulerPhase) {
      case SchedulerPhase.idle:
      case SchedulerPhase.postFrameCallbacks:
        scheduleFrame();
        return;
      case SchedulerPhase.transientCallbacks:
      case SchedulerPhase.midFrameMicrotasks:
      case SchedulerPhase.persistentCallbacks:
        return;
    }
}

当我们调用markNeedsPaint之后,会调用到ensureVisualUpdate。而只有schedulerPhase == SchedulerPhase.postFrameCallbacks|idle, 才会去安排下一帧的绘制。其他情况下都不做反应。

还可以在追下去,RendererBinding.initInstances()什么时候被调用的呢?就是我们最熟悉的一个方法:runApp:

1
2
3
4
5
void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}

WidgetsFlutterBinding的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      WidgetsFlutterBinding();
    return WidgetsBinding.instance;
  }
}

abstract class BindingBase {
  
  BindingBase() {
    ...
    initInstances();
    ...
  }
    ...
}

总结一下,runApp运行后,会调用WidgetsFlutterBinding.ensureInitialized(), 该方法会实例化WidgetsFlutterBinding对象,而在实例化过程中,会调用initInstances方法,此时,RendererBinding.initInstances方法被调用了。

流程图

markNeedsPaint -> parent1->parent2->….->parent3( repaintBoundary or not RenderObject )

PipelineOwner._nodesNeedingPaint add parent3(if repaintBoundary)

owner.requestVisualUpdate()

SchedulerBinding.ensureVisualUpdate() -> SchedulerBinding.scheduleFrame()

RendererBinding.initInstances()

WidgetsFlutterBinding() 对象实例化

WidgetsFlutterBinding.ensureInitialized()

runApp()

markNeedsLayout()

主要变量

  • _needsLayout
  • _relayoutBoundary

作用

标记该节点需要被重新layout

处理逻辑

如果该节点是布局边界了,那直接将本节点加到pipelineOwner._nodesNeedingLayout中,等待下一帧的集中处理。

如果该节点不是布局边界,调用markParentNeedsLayout,通知父节点需要被重新layout

markParentNeedsLayout()

调用parent.markNeedsLayout

markNeedsLayoutForSizedByParentChange()

sizedByParent值变化后,自身以及父节点都需要被重新layout,所以直接调用:

  • markNeedsLayout
  • markParentNeedsLayout

layout

主要变量

  • parentUsesSize. 是否依赖子节点,在调用child.layout时作为入参传入。
  • sizedByParent. 是否只受父节点自身约束的影响。如果是,在performlayout之前需要先计算performSize
  • constraints. 在调用child.layout时作为入参传入, 代表当前节点的约束。
  • _relayoutBoundary. 是否是布局的边界. 四个条件,满足一个就可:!parentUsesSize / sizedByParent / constraints.isTight / 自身不是RenderObject
  • _needsLayout. 标记是否需要layout

作用

render object开始layout的入口。父节点都是通过调用child.layout来传递的。

处理逻辑

首先,设置本render objectlayout boundary, 若boundary有变化,通知子节点清空之前的boundary信息,直到遇到是boundary的子节点为止。

接着,sizedByParenttrue的话,先通过performResize方法计算出render object的大小。

然后,再调用performLayout, 该方法也需要调用所有子节点的layout方法,触发子节点开始布局。

最后,调用markNeedsPaint,要求重新绘制。

performLayout

performLayout不光需要layout自身,还需要调用所有child.layout

performResize

如果sizedByParent == true, 说明本render object只受制于parent的约束,在layout时,需要先performResize出自身的大小,再根据这个大小去performLayout.

parentData

父节点通过setupParentData来给当前节点初始化parentData。一般在mount时被调用。

ParentDataWidget 通过applyParentData来主动更新某个childparentData内容。

RenderObjectElement.mount->attachRenderObject中,会向上找到ParentDataWidget的父类,并通过applyParentData来设置当前节点的parentData值。

作用

父节点在performLayout时,会根据child.parentData的值算出这个childconstraints, 然后调用child.layout并传入该contraints

实例

Stack-Positioned

因为Positioned继承自ParentDataWidget, 它不是RenderObjectElement, 没有RenderObject,不会挂载在RenderObject树上。所以,在RenderStackperformLayout方法中,所有相关的child均是RenderObject. (RenderObject在挂载时,会自动向上找最近的一个RenderObjectElement.renderObject去挂载。)

所以,通过Positionedleft/right/top/bottom信息设置到它的child上之后,Stack根据child.parentData的信息,计算出该childconstraints, 再调用child.layout进行布局。

Widget

StatefulWidget

TODO

绘制流程

runApp时的绘制流程

runApp -> RenderObjectToWidgetAdapter( root widget ) -> attachToRenderTree -> RenderObjectToWidgetElement( root element ) -> set root element owner -> root-element-owner.buildScope -> renderObjectToWidgetElement.mount -> container( root render object ) / parent = null / slot = null / depth = 1 -> renderObjectToWidgetElement._rebuild

updateChild -> canUpdate -> update …> (StatulElement) …> state.didUpdateWidget -> state.build

widget.createElement

renderObjectElement.mount

parent / slot / owner = parent.owner / depth = parent.depth + 1 (render object element 形成一颗树)

widget.createRenderObject

renderObjectElement.attachRenderObject

ancestorRenderObjectElement.insertChildRenderObject (render object 形成一颗树)->adoptChild(->setupParentData)/dropChild -> attach/detach

回到上面的updateChild( RenderObjectElement的子类,在mount方法中会调用该方法 )

SchedulerBinding.instance.ensureVisualUpdate()

页面刷新流程

SchedulerBinding.scheduleFrame()

set window.onBeginFrame / window.onDrawFrame

Window.scheduleFrame。 native方法,应该是注册同步信号监听。当有同步信号过来后,会调用上面的两个回调

SchedulerBinding.handleDrawFrame()中,所有的_persistentCallbacks都会被回调。而在RendererBinding.initInstances中,就添加了一个回调监听:_handlePersistentFrameCallback

RendererBinding._handlePersistentFrameCallback()

WidgetsBinding.drawFrame().

仔细看源码的话,会发现这里好像不对,应该调用RendererBinding.drawFrame()。但其实是WidgetsBinding.drawFrame()方法重写了RendererBinding.drawFrame()方法。WidgetsBinding.super.drawFrame()就是指RendererBinding.drawFrame()方法。

buildOwner.buildScope(renderViewElement)。跟第一次的绘制流程调的一个方法。

RendererBinding.drawFrame(). 包括layout/compositingBits/paint/将layer-tree合并送给GPU绘制等。

一帧的所有阶段

  • 动画阶段。

    Window.onBeginFrame时会调用handleBeginFrame, 而handleBeginFrame会遍历瞬态帧注册回调队列_transientCallbacks

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    void handleBeginFrame(Duration rawTimeStamp) {
        Timeline.startSync('Frame', arguments: timelineWhitelistArguments);
          
        ...
      
        assert(schedulerPhase == SchedulerPhase.idle);
        _hasScheduledFrame = false;
        try {
          // TRANSIENT FRAME CALLBACKS
          Timeline.startSync('Animate', arguments: timelineWhitelistArguments);
          _schedulerPhase = SchedulerPhase.transientCallbacks;
          final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
          _transientCallbacks = <int, _FrameCallbackEntry>{};
          callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
            if (!_removedIds.contains(id))
              _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
          });
          _removedIds.clear();
        } finally {
          _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
        }
      }
    

    我们在写动画时,都会minix一个Ticker, 来提供同步信号,应该就是这里的回调。Ticker中的部分源码:

    1
    2
    3
    4
    5
    6
    7
    8
    
    class Ticker {
        ...
        void scheduleTick({ bool rescheduling = false }) {
            assert(!scheduled);
            assert(shouldScheduleTick);
            _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, 				rescheduling: rescheduling);
        }
    }
    

    Ticker会帮助向SchedulerBinding中注册帧回调,在window.onBeginFrame时会告诉Ticker:时机到了,可以更新动画的值了。这时,AnimationController会计算出当前的值,并且通知widget更新。

  • 微任务阶段。等待在第一步中回调之后产生的新的微任务完成。

  • build阶段。

  • layout阶段。

  • 合成位阶段。

  • paint阶段。

  • 合成阶段。将layer-tree合并并送给GPU。

  • 语意阶段(供盲人使用的)。

  • The finalization phase in the widgets layer。部分Widget在绘制中被移除之后,调用state.dispose方法。

  • The finalization phase in the scheduler layer。handleDrawFrame中回调通过addPostFrameCallback注册的监听帧完成回调。

This post is licensed under CC BY 4.0 by the author.