Flutter构建、布局、绘制三部曲

学习Fullter也有些时间了,写过不少demo,对一些常用的widget使用也比较熟练,但是总觉得对Flutter的框架没有一个大致的了解,碰到有些细节的地方又没有文档可以查询,例如在写UI时总不知道为什么container添加了child就变小了;widget中key的作用,虽然官方有解释但是凭空来讲的话有点难理解。所以觉得深入一点的了解Flutter框架还是很有必要的。

构建

初次构建

flutter的入口main方法直接调用了runApp(Widget app)方法,app参数就是我们的根视图的Widget,我们直接跟进runApp方法

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()//此方法是对flutter的框架做一些必要的初始化
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}

runApp方法先调用了WidgetsFlutterBinding.ensureInitialized()方法,这个方法是做一些必要的初始化

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
      static WidgetsBinding ensureInitialized() {
        if (WidgetsBinding.instance == null)
          WidgetsFlutterBinding();
        return WidgetsBinding.instance;
      }
}

WidgetsFlutterBinding混入了不少的其他的Binding

  • BindingBase 那些单一服务的混入类的基类
  • GestureBinding framework手势子系统的绑定,处理用户输入事件
  • ServicesBinding 接受平台的消息将他们转换成二进制消息,用于平台与flutter的通信
  • SchedulerBinding 调度系统,用于调用Transient callbacks(Window.onBeginFrame的回调)、Persistent callbacks(Window.onDrawFrame的回调)、Post-frame callbacks(在Frame结束时只会被调用一次,调用后会被系统移除,在Persistent callbacks后Window.onDrawFrame回调返回之前执行)
  • PaintingBinding 绘制库的绑定,主要处理图片缓存
  • SemanticsBinding 语义化层与Flutter engine的桥梁,主要是辅助功能的底层支持
  • RendererBinding 渲染树与Flutter engine的桥梁
  • WidgetsBinding Widget层与Flutter engine的桥梁

以上是这些Binding的主要作用,在此不做过多赘述,WidgetsFlutterBinding.ensureInitialized()返回的是WidgetsBinding对象,然后马上调用了WidgetsBinding的attachRootWidget(app)方法,将我们的根视图的Widget对象穿进去,我们继续看attachRootWidget方法

void attachRootWidget(Widget rootWidget) {
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget
    ).attachToRenderTree(buildOwner, renderViewElement);
}

创建了一个RenderObjectToWidgetAdapter,让后直接调用它的attachToRenderTree方法,BuildOwner是Widget framework的管理类

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
    if (element == null) {
      owner.lockState(() {
        element = createElement();
        assert(element != null);
        element.assignOwner(owner);
      });
      owner.buildScope(element, () {
        element.mount(null, null);
      });
    } else {
      element._newWidget = this;
      element.markNeedsBuild();
    }
    return element;
}

element为空,owner先锁定状态,然后调用了RenderObjectToWidgetAdapter的createElement()返回了RenderObjectToWidgetElement对象,让后将owner赋值给element(assignOwner方法),让后就是owner调用buildScope方法

void buildScope(Element context, [VoidCallback callback]) {
    if (callback == null && _dirtyElements.isEmpty)
      return;
    Timeline.startSync('Build', arguments: timelineWhitelistArguments);
    try {
      _scheduledFlushDirtyElements = true;
      if (callback != null) {
        _dirtyElementsNeedsResorting = false;
        try {
          callback();
        } finally {}
      }
      ...
  }

省略了部分以及后续代码,可以看到buildScope方法首先就调用了callback(就是element.mount(null, null)方法),回到RenderObjectToWidgetElement的mount方法

@override
void mount(Element parent, dynamic newSlot) {
  assert(parent == null);
  super.mount(parent, newSlot);
  _rebuild();
}

首先super.mount(parent, newSlot)调用了RootRenderObjectElement的mount方法(只是判定parent和newSlot都为null),让后又继续向上调用了RenderObjectElement中的mount方法

@override
void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _renderObject = widget.createRenderObject(this);
  attachRenderObject(newSlot);
  _dirty = false;
}

RenderObjectElement中的mount方法又调用了Element的mount方法

@mustCallSuper
void mount(Element parent, dynamic newSlot) {
  _parent = parent;
  _slot = newSlot;
  _depth = _parent != null ? _parent.depth + 1 : 1;
  _active = true;
  if (parent != null) // Only assign ownership if the parent is non-null
    _owner = parent.owner;
  if (widget.key is GlobalKey) {
    final GlobalKey key = widget.key;
    key._register(this);
  }
  _updateInheritance();
}

Element的mount方法其实就是进行了一些赋值,以确认当前Element在整个树种的位置,让后回到RenderObjectElement中的mount方法,调用了widget.createRenderObject(this)方法,widget是RenderObjectToWidgetAdapter的实例,它返回的是RenderObjectWithChildMixin对象,让后调用attachRenderObject方法

@override
void attachRenderObject(dynamic newSlot) {
  assert(_ancestorRenderObjectElement == null);
  _slot = newSlot;
  _ancestorRenderObjectElement = _findAncestorRenderObjectElement();//获取此RenderObjectElement最顶端的RenderObjectElement对象
  _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);//将renderObject插入RenderObjectElement中
  final ParentDataElement<RenderObjectWidget> parentDataElement = _findAncestorParentDataElement();
  if (parentDataElement != null)
    _updateParentData(parentDataElement.widget);
}

///RenderObjectToWidgetElement中的insertChildRenderObject方法,简单将子RenderObject赋值给父RenderObject的child字段
@override
void insertChildRenderObject(RenderObject child, dynamic slot) {
  assert(slot == _rootChildSlot);
  assert(renderObject.debugValidateChild(child));
  renderObject.child = child;
}

Element的mount方法确定当前Element在整个树种的位置并插入,RenderObjectElement中的mount方法来创建RenderObject对象并将其插入到渲染树中,让后再回到RenderObjectToWidgetElement方法,mount之后调用_rebuild()方法,_rebuild()方法中主要是调用了Element的updateChild方法

@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  if (newWidget == null) {//当子Widget没有的时候,直接将child deactivate掉
    if (child != null)
      deactivateChild(child);
    return null;
  }
  if (child != null) {//有子Element的时候
    if (child.widget == newWidget) {//Widget没有改变
      if (child.slot != newSlot)//再判断slot有没有改变,没有则不更新slot
        updateSlotForChild(child, newSlot);//更新child的slot
      return child;//返回child
    }
    if (Widget.canUpdate(child.widget, newWidget)) {//Widget没有改变,再判断Widget能否update,如果能还是重复上面的步骤
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      child.update(newWidget);
      return child;
    }
    deactivateChild(child);//如果不能更新的话,直接将child deactivate掉,然后在inflateWidget(newWidget, newSlot)创建新的Element
  }
  return inflateWidget(newWidget, newSlot);//根据Widget对象以及slot创建新的Element
}

由于我们是第一次构建,child是null,所以就直接走到inflateWidget方法创建新的Element对象,跟进inflateWidget方法

@protected
Element inflatinflateWidgeteWidget(Widget newWidget, dynamic newSlot) {
  final Key key = newWidget.key;
  if (key is GlobalKey) {//newWidget的key是GlobalKey
    final Element newChild = _retakeInactiveElement(key, newWidget);//复用Inactive状态的Element
    if (newChild != null) {
      newChild._activateWithParent(this, newSlot);//activate 此Element(将newChild出入到Element树)
      final Element updatedChild = updateChild(newChild, newWidget, newSlot);//直接将newChild更新
      return updatedChild;//返回更新后的Element
    }
  }
  final Element newChild = newWidget.createElement();//调用createElement()进行创建
  newChild.mount(this, newSlot);//继续调用newChild Element的mount方法(如此就行一直递归下去,当递归完成,整个构建过程也就结束了)
  return newChild;//返回子Element
}

inflateWidget中其实就是通过Widget得到Element对象,让后继续调用子Element的mount的方将进行递归。

不同的Element,mount的实现会有所不同,我们看一下比较常用的StatelessElement、StatefulElement,他们的mount方法实现在ComponentElement中

@override
void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _firstBuild();
}

void _firstBuild() {
  rebuild();//调用了Element的rebuild()方法
}

//Element的rebuild方法,通常被三处地方调用
//1.当BuildOwner.scheduleBuildFor被调用标记此Element为dirty时
//2.当Element第一次构建由mount方法去调用
//3.当Widget改变时,被update方法调用
void rebuild() {
  if (!_active || !_dirty)
    return;
  performRebuild();//调用performRebuild方法(抽象方法)
}

//ComponentElement的performRebuild实现
@override
void performRebuild() {
  Widget built;
  try {
    built = build();//构建Widget(StatelessElement直接调用build方法,StatefulElement直接调用state.build方法)
  } catch (e, stack) {
    built = ErrorWidget.builder(_debugReportException('building $this', e, stack));//有错误的化就创建一个ErrorWidget
  } finally {
    _dirty = false;
  }
  try {
    _child = updateChild(_child, built, slot);//让后还是根据Wdiget来更新子Element
  } catch (e, stack) {
    built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
    _child = updateChild(null, built, slot);
  }
}

再看一看MultiChildRenderObjectElement的mount方法

@override
void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _children = List<Element>(widget.children.length);
  Element previousChild;
  for (int i = 0; i < _children.length; i += 1) {
    final Element newChild = inflateWidget(widget.children[i], previousChild);//遍历children直接inflate根据Widget创建新的Element
    _children[i] = newChild;
    previousChild = newChild;
  }
}

可以看到不同的Element构建方式会有些不同,Element(第一层Element)的mount方法主要是确定当前Element在整个树种的位置并插入;ComponentElement(第二层)的mount方法先构建Widget树,让后再递归更新(包括重用,更新,直接创建inflate)其Element树;RenderObjectElement(第二层)中的mount方法来创建RenderObject对象并将其插入到渲染树中。MultiChildRenderObjectElement(RenderObjectElement的子类)在RenderObjectElement还要继续创建children Element。

总结:首先是由WidgetBinding创建RenderObjectToWidgetAdapter然后调用它的attachToRenderTree方法,创建了RenderObjectToWidgetElement对象,让后将它mount(调用mount方法),mount方法中调用的_rebuild,继而调用updateChild方法,updateChild会进行递归的更新Element树,若child没有则需要重新创建新的Element,让后将其mount进Element树中(如果是RenderobjectElement的化,mount的过程中会去创建RenderObject对象,并插入到RenderTree)。

通过setState触发构建

通常我们在应用中要更新状态都是通过State中的setState方法来触发界面重绘,setState方法就是先调用了callback让后调用该State的Element对象的markNeedsBuild方法,markNeedsBuild中将Element标记为dirty并通过BuildOwner将其添加到dirty列表中并调用onBuildScheduled回调(在WidgetsBinding初始化时设置的,它回去调用window.scheduleFrame方法),让后window的onBeginFrame,onDrawFrame回调(在SchedulerBinding初始化时设置的,这两个回调会执行一些callback)会被调用,SchedulerBinding通过persisterCallbacks来调用到BuildOwner中buildScope方法。上面我们只看了buildScope的一部分,当通过setState方法来触发界面重绘时,buildScope的callBack为null

void buildScope(Element context, [VoidCallback callback]) {
  if (callback == null && _dirtyElements.isEmpty)
    return;
  Timeline.startSync('Build', arguments: timelineWhitelistArguments);
  try {
    _scheduledFlushDirtyElements = true;
    if (callback != null) {
      Element debugPreviousBuildTarget;
      _dirtyElementsNeedsResorting = false;
      try {
        callback();//调用callback
      } finally {}
    }
    _dirtyElements.sort(Element._sort);
    _dirtyElementsNeedsResorting = false;
    int dirtyCount = _dirtyElements.length;
    int index = 0;
    while (index < dirtyCount) {
      try {
        _dirtyElements[index].rebuild();//遍历dirtyElements并执行他们的rebuild方法来使这些Element进行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;
        }
      }
    }
  } finally {
    for (Element element in _dirtyElements) {//最后解除Element的dirty标记,以及清空dirtyElements
      assert(element._inDirtyList);
      element._inDirtyList = false;
    }
    _dirtyElements.clear();
    _scheduledFlushDirtyElements = false;
    _dirtyElementsNeedsResorting = null;
    Timeline.finishSync();
  }
}

很明显就是对dirtyElements中的元素进行遍历并且对他们进行rebuild。

布局

window通过scheduleFrame方法会让SchedulerBinding来执行handleBeginFrame方法(执行transientCallbacks)和handleDrawFrame方法(执行persistentCallbacks,postFrameCallbacks),在RendererBinding初始化时添加了_handlePersistentFrameCallback,它调用了核心的绘制方法drawFrame。

@protected
void drawFrame() {
  assert(renderView != null);
  pipelineOwner.flushLayout();//布局
  pipelineOwner.flushCompositingBits();//刷新dirty的renderobject的数据
  pipelineOwner.flushPaint();//绘制
  renderView.compositeFrame(); // 将二进制数据发送给GPU
  pipelineOwner.flushSemantics(); // 将语义发送给系统
}

flushLayout触发布局,将RenderObject树的dirty节点通过调用performLayout方法进行逐一布局,我们先看一下RenderPadding中的实现

@override
void performLayout() {
  _resolve();//解析padding参数
  if (child == null) {//如果没有child,直接将constraints与padding综合计算得出自己的size
    size = constraints.constrain(Size(
      _resolvedPadding.left + _resolvedPadding.right,
      _resolvedPadding.top + _resolvedPadding.bottom
    ));
    return;
  }
  final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding);//将padding减去,生成新的约束innerConstraints
  child.layout(innerConstraints, parentUsesSize: true);//用新的约束去布局child
  final BoxParentData childParentData = child.parentData;
  childParentData.offset = Offset(_resolvedPadding.left, _resolvedPadding.top);//设置childParentData的offset值
  size = constraints.constrain(Size(//将constraints与padding以及child的sieze综合计算得出自己的size
    _resolvedPadding.left + child.size.width + _resolvedPadding.right,
    _resolvedPadding.top + child.size.height + _resolvedPadding.bottom
  ));
}

可以看到RenderPadding中的布局分两种情况。如果没有child,那么就直接拿parent传过来的约束以及padding来确定自己的大小;否则就先去布局child,让后再拿parent传过来的约束和padding以及child的size来确定自己的大小。RenderPadding是典型的单child的RenderBox,我们看一下多个child的RenderBox。例如RenderFlow

@override
void performLayout() {
  size = _getSize(constraints);//直接先确定自己的size
  int i = 0;
  _randomAccessChildren.clear();
  RenderBox child = firstChild;
  while (child != null) {//遍历孩子
    _randomAccessChildren.add(child);
    final BoxConstraints innerConstraints = _delegate.getConstraintsForChild(i, constraints);//获取child的约束,此方法为抽象
    child.layout(innerConstraints, parentUsesSize: true);//布局孩子
    final FlowParentData childParentData = child.parentData;
    childParentData.offset = Offset.zero;
    child = childParentData.nextSibling;
    i += 1;
  }
}

可以看到RenderFlow的size直接就根据约束来确定了,并没去有先布局孩子,所以RenderFlow的size不依赖与孩子,后面依旧是对每一个child依次进行布局。

还有一种比较典型的树尖类型的RenderBox,LeafRenderObjectWidget子类创建的RenderObject对象都是,他们没有孩子,他们才是最终需要渲染的对象,例如

@override
void performLayout() {
  size = _sizeForConstraints(constraints);
}

非常简单就通过约束确定自己的大小就结束了。所以performLayout过程就是两点,确定自己的大小以及布局孩子。我们上面提到的都是RenderBox的子类,这些RenderObject约束都是通过BoxConstraints来完成,但是RenderSliver的子类的约束是通过SliverConstraints来完成,虽然他们对child的约束方式不同,但他们在布局过程需要执行的操作都是一致的。

绘制

布局完成了,PipelineOwner就通过flushPaint来进行绘制

void flushPaint() {
  try {
    final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
    _nodesNeedingPaint = <RenderObject>[];
    // 对dirty nodes列表进行排序,最深的在第一位
    for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
      assert(node._layer != null);
      if (node._needsPaint && node.owner == this) {
        if (node._layer.attached) {
          PaintingContext.repaintCompositedChild(node);
        } else {
          node._skippedPaintingOnLayer();
        }
      }
    }
  } finally {}
}

PaintingContext.repaintCompositedChild(node)会调用到child._paintWithContext(childContext, Offset.zero)方法,进而调用到child的paint方法,我们来看一下第一次绘制的情况,dirty的node就应该是RenderView,跟进RenderView的paint方法

@override
void paint(PaintingContext context, Offset offset) {
  if (child != null)
    context.paintChild(child, offset);//直接绘制child
}

自己没有什么绘制的内容,直接绘制child,再看一下RenderShiftedBox

@override
void paint(PaintingContext context, Offset offset) {
  if (child != null) {
    final BoxParentData childParentData = child.parentData;
    context.paintChild(child, childParentData.offset + offset);//直接绘制child
  }
}

好像没有绘制内容就直接递归的进行绘制child,那找一个有绘制内容的吧,我们看看RenderDecoratedBox

@override
void paint(PaintingContext context, Offset offset) {
  _painter ??= _decoration.createBoxPainter(markNeedsPaint);//获取painter画笔
  final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
  if (position == DecorationPosition.background) {//画背景
    _painter.paint(context.canvas, offset, filledConfiguration);//绘制过程,具体细节再painter中
    if (decoration.isComplex)
      context.setIsComplexHint();
  }
  super.paint(context, offset);//画child,里面直接调用了paintChild
  if (position == DecorationPosition.foreground) {//画前景
    _painter.paint(context.canvas, offset, filledConfiguration);
    if (decoration.isComplex)
      context.setIsComplexHint();
  }
}

如果自己有绘制内容,paint方法中的实现就应该包括绘制自己以及绘制child,如果没有孩子就只绘制自己的内容,看一下RenderImage

@override
void paint(PaintingContext context, Offset offset) {
  if (_image == null)
    return;
  _resolve();
  paintImage(//直接绘制Image,具体细节再此方法中
    canvas: context.canvas,
    rect: offset & size,
    image: _image,
    scale: _scale,
    colorFilter: _colorFilter,
    fit: _fit,
    alignment: _resolvedAlignment,
    centerSlice: _centerSlice,
    repeat: _repeat,
    flipHorizontally: _flipHorizontally,
    invertColors: invertColors,
    filterQuality: _filterQuality
  );
}

所以基本上绘制需要完成的流程就是,如果自己有绘制内容,paint方法中的实现就应该包括绘制自己以及绘制child,如果没有孩子就只绘制自己的内容,流程比较简单。

以上是自己学习的一些总结,如有错误之处请指出,大家共同探讨,觉得不错的话,点个赞呗!

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

推荐阅读更多精彩内容