vlambda博客
学习文章列表

Flutter渲染原理学习

        写Flutter已有好几个月的时间了,最开始总会有一点点不适应,但是写一段时间后还是觉得蛮顺手的,而且支持热更,不需要等整个项目编译,提升了不少效率。但是在开发过程中,总是会遇到因为Widget嵌套得不好而出现一些错误,于是一直想学习一下它的相关原理看看是为什么。所以通过学习和参考网上大神分享的的一些资料,整理出了今天要分享的文章。

正文

        Flutter有三棵重要的树,分别是Widget树、Element树、RenderObject树,它们各司其职,分成了几个相关联但清晰的结构。Widget树与我们日常开发接触最多,其它的两棵树比较少接触到。

这三棵树的关联的大致流程:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,再完成布局排列和绘制。最后合并层级,通过Skia引擎渲染为GPU数据,然后GPU接着将数据交给显示器显示。

它们三者的联系如下图:

      如果最开始有人告诉你写Flutter就像在写配置,你会不会觉得不太可能。那么一起通过下面的内容看看这句话到底是不是真的。


Widget

     Widget只是UI元素中的配置数据,并且一个Widget可以对应多个Element。真正的UI渲染树是由Element构成的。


日常开发中,常见结构如下图:

Flutter渲染原理学习

重要成员:
  • Key:跟Widget的runtimeType一起决定此Widget是否复用
  • createElement方法:创建对应的Element
  • canUpdate方法:对比runtimeType和Key,相等的话表示就会用新的Widget去更新Element,否则的话会重新创建Element对象。

static bool canUpdate(Widget oldWidget, Widget newWidget) {
 return oldWidget.runtimeType == newWidget.runtimeType
     && oldWidget.key == newWidget.key;
}

StatelessWidget:

  • 用于不需要维护状态的Widget

  • createElement方法返回的是StatelessElement对象

StatefulWdiget:

  • createElement方法返回的是StatefulElement对象

  • createState方法返回一个State对象,维护状态信息

State:

  1. 在第一次插入到树中被创建,它的widget成员可能会被更新,在mount的时候,会调用firstBuild方法,firstBuild方法会调用rebuild方法

  2. 调用setState方法后,会调用对应Element的markNeedsBuild:被标记为dirty,并且会调用owner.scheduleBuildFor(this),然后会触发rebuild。


Element

    mount方法中调用Widget去创建一个RenderObject,然后与Element相关联;然后插入到渲染树中。(RenderObjectElement等等)


void mount(Element parent, dynamic newSlot) {
 super.mount(parent, newSlot);

 _renderObject = widget.createRenderObject(this);
//省略
 attachRenderObject(newSlot);
 _dirty = false;
}

1. 创建一个RenderObject

2. 加入到渲染树

    当配置改变,Widget会根据runtimeType和Key去比较,判断是否可复用Elemen,可复用的话,则更新Element的配置,否则新建一个(Widget中的canUpdate方法决定)

    rebuild方法会调用performRebuild方法

void performRebuild() {

 Widget built;
 try {
     built = build();
} catch (e, stack) {
   
} finally {
   // We delay marking the element as clean until after calling build() so
   // that attempts to markNeedsBuild() during build() will be ignored.
   _dirty = false;
}
 try {
   _child = updateChild(_child, built, slot);
} catch (e, stack) {
   _child = updateChild(null, built, slot);
}

}
  1. build返回新的Widget
  2. updatechild通过新的配置,返回一个element
    • child为null,newWidget不为null,需新建一个Element
    • child不为null、newWidget不为null,根据 新的Widget类型和对比新旧的Key是否一致来决定是否新建一个Element
    • child不为null,newWidget为null,移除掉Element
    • 两者都为null,什么都不用做

移除:

void deactivateChild(Element child) {
 child._parent = null;
 child.detachRenderObject();
 owner._inactiveElements.add(child); // this eventually calls child.deactivate()
}

创建/重新插入到Element树的其他位置:

先移除,然后调用inflateWidget方法,如果key为GlobalKey的话,则会复用之前的Element,然后重新active、并且将其renderObject加入到渲染树中。

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;
}

留意:BuildContext是Widget对应的Element

RenderObject

RenderObject的主要职责是布局、绘制、合成。


布局:

父控件将布局约束传递给子控件

void layout(Constraints constraints, { bool parentUsesSize = false }) {
 RenderObject relayoutBoundary;
 if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
   relayoutBoundary = this;
} else {
   relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
}
 
_relayoutBoundary = relayoutBoundary;
 
 if (sizedByParent) {
   performResize();
}

 try {
   performLayout();
} catch (e, stack) { }
 _needsLayout = false;
 markNeedsPaint();
}
  1. layout:参数有constraints和parentUsesSize,是否依靠父节点点父节点对子节点对限制。

  2. 确定relayoutBoundary,需要重新布局的区域。

  3. parentData:子节点在父节点中的偏移等信息

  4. performResize:如果是需要依靠父节点确定大小,则大小通过parent传递给它的constraints确定

  5. performLayout:对自身、child进行布局

例如:RenderPadding的performLayout方法


@override
void performLayout() {
 final BoxConstraints constraints = this.constraints;
 _resolve();
 if (child == null) {
   size = constraints.constrain(Size(
     _resolvedPadding.left + _resolvedPadding.right,
     _resolvedPadding.top + _resolvedPadding.bottom,
  ));
   return;
}
 final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding);
   //子组件的layout
 child.layout(innerConstraints, parentUsesSize: true);
 final BoxParentData childParentData = child.parentData as BoxParentData;
   //设置子组件parentData的数据
 childParentData.offset = Offset(_resolvedPadding.left, _resolvedPadding.top);
 size = constraints.constrain(Size(
   _resolvedPadding.left + child.size.width + _resolvedPadding.right,
   _resolvedPadding.top + child.size.height + _resolvedPadding.bottom,
));
}

Flutter渲染原理学习

markNeedsLayout方法:

会根据relayoutBoundary来确定需要布局到哪个节点(如果不是本身,那么就一直往上找)

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

绘制:

void paint(PaintingContext context, Offset offset) { }

有的RenderObject会先判断是否溢出,如果溢出则绘制溢出的提示。正常的话会走绘制流程;绘制child的话,会根据child的parentData来计算绘制位置,然后进行绘制(例如RenderView)。

 void paint(PaintingContext context, Offset offset) {
   if (child != null)
     context.paintChild(child, offset);
}
void paintChild(RenderObject child, Offset offset) {
 if (child.isRepaintBoundary) {
   stopRecordingIfNeeded();
   _compositeChild(child, offset);
} else {
   child._paintWithContext(this, offset);
}
}
void _compositeChild(RenderObject child, Offset offset) {
 if (child._needsPaint) {
   repaintCompositedChild(child, debugAlsoPaintedParent: true);
} else {
}
 child._layer.offset = offset;
 appendLayer(child._layer);
}
  1. _compositeChild:创建Layer、并在layer上绘制,然后合成

  2. _paintWithContext:在父控件上绘制

  3. 如果是独立绘制,那么会创建一个层,将组件在上面绘制。

isRepaintBoundary:是否独立于父元素绘制。如果一个RenderObject绘制频繁时,可以通过制定为true,来提高性能。以下代码体现了它的作用:

void markNeedsPaint() {
 if (_needsPaint)
   return;
 _needsPaint = true;
 if (isRepaintBoundary) {
 
   // If we always have our own layer, then we can just repaint
   // ourselves without involving any other nodes.
   if (owner != null) {
     owner._nodesNeedingPaint.add(this);
     owner.requestVisualUpdate();
  }
} else if (parent is RenderObject) {
   final RenderObject parent = this.parent as RenderObject;
   parent.markNeedsPaint();
} else {
 
   // 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();
}
}

1. markNeedsPaint方法:一直向上父节点查找,直到找到一个isRepaintBoundary为true的RenderObject,才会开始重绘。如果找到了根节点,则直接绘制

2. requestVisualUpdate方法最后会调用到window的scheduleFrame(刷新UI)。

结语

         本文主要分享了Flutter中Widget、Element和RenderObject之间的联系,并且分析了布局与绘制的相关原理。当然Flutter的渲染原理远不止这些,以上只是分析在Framework层面的,下面还有引擎的其他操作。希望通过此文对你有帮助,以上分享如果有什么不妥还望指出纠正。