Flutter 探索系列:布局和渲染(二)

上一篇文章中,我们介绍 Flutter Widget 的设计思想、实现原理,并分析了 Widget、Element 和 RenderObject 的源码,这篇文章继续结合源码分析 Flutter 的渲染过程。

实现原理

1,Flutter 渲染流程是怎样的?

image

从这张图上可知,界面显示到屏幕上,Flutter 经过了 Vsync 信号、动画、build、布局、绘制、合成等渲染过程。

显示器垂直同步 Vsync 不断的发出信号,驱动 Flutter Engine 刷新视图。Flutter 提供 60 FPS,也就是一秒钟发出60次信号,触发60次重绘。

运行动画,每个 Vsync 信号都会触发动画状态改变。

Widget 状态改变,触发重建一棵新的 Widget 树。比较新旧 Widget 树的差异,找出有变动的 Widget 节点和对应的 RenderObject 节点。详细过程请参考上一篇文章。

对需要更新 RenderObject 节点,更新界面布局、重新绘制。

根据新的 RenderObject 树,更新合成图层。

输出新的图层树。

2,渲染过程中,Flutter 如何更新界面布局?
经过 build 环节后,找出需要更新的 RenderObject 树,首先进入布局环节。上一篇文章中介绍到,element 被标记为 dirty 时便会重建,同样的,RenderObject 被标记为 dirty 后,放入一个待处理的数组中。在布局环节中,遍历该数组中的元素,按照节点在 RenderObject 树上的深度重新布局。

每个节点 RenderObject 对象,按照部件的逻辑先计算自身所占空间的大小、位置,再计算 child 的,paren 将 size 约束限制传递给 child,child 根据这个约束计算自身的所占空间,然后再传给 child 的 child,如此递归完成整个 RenderObject 树的布局更新。大概过程如下

parent.performLayout() -> child.layout() -> child.performLayout()/child.performResize() -> child.child.layout() -> .....

Flutter 还有一个 RelayoutBoundary,用于确定重绘边界,可以手动指定或自动设置。边界内的对象重新布局,不会影响边界外的对象。

3,渲染过程中,Flutter 如何绘制界面?
Paint 的过程有点类似于 Layout,同样将待重新绘制的 RenderObject 标记为 dirty,放入一个数组中。这个数组也是深度优先的顺序执行,先绘制自身,再绘制 child。

isRepaintBoundary 重绘边界也类似上面的 RelayoutBoundary,重绘边界内的元素及 child,会一起重新绘制,边界外的元素不受影响。

源码分析

我们按照 Flutter 的渲染依次分析 Vsync、build、layout、paint 四个过程 。

Vsync

垂直同步信号 Vsync 到来后,执行一系列动作开始界面的重新渲染,那么在哪里监听 Vsync 信号,收到 Vsync 信号如何通知界面刷新?

上一篇文章介绍了 Widget build 实现过程,其中提到在 Flutter 应用启动时,初始化了一个单例对象 WidgetsFlutterBinding,它是连接 Flutter engine sdk 和 Widget 框架的桥梁,它混合了 SchedulerBinding 和 RendererBinding。SchedulerBinding 提供了 window.onBeginFrame 和 window.onDrawFrame 回调,监听刷新事件。

RendererBinding 在初始化方法 initInstances 中,addPersistentFrameCallback 向 persistentCallbacks 队列添加了一个回调 _handlePersistentFrameCallback。

在收到从 Flutter engine 传来的刷新事件时,调用 _handlePersistentFrameCallback 回调,也就是执行 drawFrame 方法。

// RendererBinding
void initInstances() {
    ...
    addPersistentFrameCallback(_handlePersistentFrameCallback);
}
  
void _handlePersistentFrameCallback(Duration timeStamp) {
    drawFrame();
}

// SchedulerBinding
void addPersistentFrameCallback(FrameCallback callback) {
    _persistentCallbacks.add(callback);
}

那么 persistentCallbacks 队列什么时候被执行?

这里先介绍一个类 Window,它是 Flutter engine 提供的一个图形界面相关的接口,包括了屏幕尺寸、调度接口、输入事件回调、图形绘制接口和其他一些核心服务。Window 有一个绘制的回调方法 _onDrawFrame

当 Flutter engine 调用 _onDrawFrame 时,触发 SchedulerBinding.handleDrawFrame 方法,这个方法里面遍历执行已注册的回调,即前面注册的 drawFrame 方法。

// SchedulerBinding
  void ensureFrameCallbacksRegistered() {
    window.onBeginFrame ??= _handleBeginFrame;
    window.onDrawFrame ??= _handleDrawFrame;
  }
  
void handleDrawFrame() {
    ...
      _schedulerPhase = SchedulerPhase.persistentCallbacks;
      for (FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
    ...
}

Vsync 信号到来,Flutter engine 调用 _onDrawFrame 方法启动渲染流程,开启一系列的动作。

Build

收到刷新事件后,先调用 WidgetsBinding.drawFame 方法。这个方法重建 Widget 树,这一过程上篇文章有详细介绍,这里不多做赘述。

//WidgetsBinding
void drawFrame() {
    ...
    if (renderViewElement != null)
      buildOwner.buildScope(renderViewElement);
    super.drawFrame();
    ...
}

Layout

super.drawFrame() 会进入到 RenderBinding.drawFrame 方法,开始重新布局和绘制。

//RenderBinding
void drawFrame() {
  pipelineOwner.flushLayout(); //布局
  pipelineOwner.flushCompositingBits(); //重绘之前的预处理操作,检查RenderObject是否需要重绘
  pipelineOwner.flushPaint(); // 重绘
  renderView.compositeFrame(); // 将需要绘制的比特数据发给GPU
  pipelineOwner.flushSemantics(); 
}

flushLayout 方法内,遍历 _nodesNeedingLayout 数组,_nodesNeedingLayout 内存放的是被标记为 dirty 的 RenderObject 元素。遍历前先对 _nodesNeedingLayout 数组排序,按照深度优先的顺序重新排列,即先处理上层节点再处理下层节点,然后遍历每个元素重新布局。

void flushLayout() {
    ...
    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();
        }
      }
    ...
}

_layoutWithoutResize 调用 performLayout 方法,每个 RenderObject 子类都有不同的实现,以 RenderView 为例,它读取配置中的 size,然后调用 child 的 layout 方法,并把 size 限制传进去。同时将自身的布局标志 _needsLayout 设置为 false

void _layoutWithoutResize() {
   ...
   performLayout(); 
   markNeedsSemanticsUpdate();
   ...
   _needsLayout = false;
   markNeedsPaint();
}

void performLayout() {
    _size = configuration.size;
    
    if (child != null)
      child.layout(new BoxConstraints.tight(_size));//调用child的layout
}

layout 方法中,传入的两个参数:constraints 表示 parent 对 child 的大小限制,parentUsesSize 表示 child 布局变化是否影响 parent,如果为 true,当 child 布局变化时,parent 会被标记为需要重新布局。

void layout(Constraints constraints, { bool parentUsesSize: false }) {
    ...
    RenderObject relayoutBoundary;
    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      final RenderObject parent = this.parent;
      relayoutBoundary = parent._relayoutBoundary;
    }
    
    if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
      return;
    }
    _constraints = constraints;
    _relayoutBoundary = relayoutBoundary;
    
    if (sizedByParent) {
        performResize(); 
    }
    
    performLayout();
    ...
}

sizedByParent 表示 child size 完成由 parent 决定,所以当 sizedByParent 为 true 时,child size 在 performResize 中确定。当 sizedByParent 为 false 时,执行 performLayout 计算自身 size,并调用自身的 child 布局,最终调用链就变成:

parent.performLayout() -> child.layout() -> child.performLayout()/child.performResize() -> child.child.layout() -> .....

RelayoutBoundary,用于确定重绘边界。边界内的对象重新布局,不会影响边界外的对象。在 RenderObject 的 markNeedsLayout 方法中,从自身开始向 parent 查找到 relayoutBoundary,然后把它添加到待布局 _nodesNeedingLayout 数组中,等下次 Vsnc 信号到来时重新布局。

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

  void markParentNeedsLayout() {
    _needsLayout = true;
    final RenderObject parent = this.parent;
    if (!_doingThisLayoutWithCallback) {
      parent.markNeedsLayout();
    } else {
      assert(parent._debugDoingThisLayout);
    }
  }

Paint

布局完成后开始绘制,绘制的入口是 flushPaint。类似于布局,将需要重新绘制的 RenderObject 标记为 dirty,同样按照深度优先的顺序遍历 _nodesNeedingPaint 数组,每个元素都重新绘制。

void flushPaint() {
  final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
  _nodesNeedingPaint = <RenderObject>[];
  // Sort the dirty nodes in reverse order (deepest first).
  for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
    if (node._needsPaint && node.owner == this) {
      if (node._layer.attached) {
        PaintingContext.repaintCompositedChild(node);
      } else {
        node._skippedPaintingOnLayer();
      }
    }
  }
  ...
}

paint 由具体的 RenderObject 类重写,每个实现都不一样。如果 RenderObject 有 child,执行自身的 paint 后,再执行 paintChild,调用链: paint() -> paintChild() -> paint() ...

static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent: false }) {
    ...
    OffsetLayer childLayer = child._layer;
    if (childLayer == null) {  
      child._layer = childLayer = OffsetLayer();
    } else {
      childLayer.removeAllChildren();
    }
    
    final PaintingContext childContext = PaintingContext(child._layer, child.paintBounds);
    child._paintWithContext(childContext, Offset.zero);
    childContext._stopRecordingIfNeeded();
}

void _paintWithContext(PaintingContext context, Offset offset) {
  ...
  paint(context, offset); 
  ...
}


  void paintChild(RenderObject child, Offset offset) {
    ...
    if (child.isRepaintBoundary) {
      stopRecordingIfNeeded();
      _compositeChild(child, offset);
    } else {
      child._paintWithContext(this, offset);
    }
    ...
 }

isRepaintBoundary 类似于上面布局中的 RepaintBoundary,它决定是否自身是否独立绘制,如果为 true,则独立绘制,否则随 parent 一块绘制。

最后将所有layer组合成Scene,然后通过 ui.window.render 方法,把 scene 提交给Flutter Engine

void compositeFrame() {
  ...
  try {
    final ui.SceneBuilder builder = ui.SceneBuilder();
    final ui.Scene scene = layer.buildScene(builder);
    if (automaticSystemUiAdjustment)
      _updateSystemChrome();
    ui.window.render(scene);
    scene.dispose(); 
  } finally {
    Timeline.finishSync();
  }
}

参考资料

Flutter
Flutter框架分析(四)-- Flutter框架的运行
Flutter运行机制-从启动到显示

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