Flutter渲染优化之RepaintBoundary
前言
了解Flutter的同学应该都或多或少知道Flutter中的三棵树(Widget,Element,RenderObject),其中RenderObject负责绘制逻辑,RenderObject中的paint方法类似于Android中View的Draw方法
Flutter状态更新
Flutter中按有无状态更新可分为两类:StatelessWidget(无状态)和StatefulWidget(有状态),StatefulWidget中创建一个State,在State内部调用setState,等到下一次Vsycn信号过来就会重建更新状态了。
Flutter的渲染流程
在Flutter三棵树中Widget和Element的节点是一一对应,而RenderObject是少于或等于Widget的数量的。当Widget是RenderObjectWidget的派生类的时候才有对应的RenderObject。Element和RenderObject在某些条件下是可以复用的,
Flutter渲染流程
渲染的耗时包括两部分
构建流水线任务
绘制逻辑
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7yQpGFH1-1640945089424)(/Users/liuwaiping/Library/Application Support/typora-user-images/image-20211230180048490.png)]
从上图可以看出Flutter绘制一帧的任务会先构建三棵树然后再去绘制,因为Element复用的原因所以页面刷新的时候Widget和Element的生命方法并不会重复调用(解决构建耗时性能问题),但是在不使用RepaintBoundary的情况下RenderObject中的paint方法会被频繁调用,接下来我们学习一下Flutter是怎么提升绘制性能的。
RepaintBoundary
RepaintBoundary是集继承 SingleChildRenderObjectWidget,也属于RenderObjectWidget的派生类,所以RepaintBoundary也会有对应的RenderObject。
class RepaintBoundary extends SingleChildRenderObjectWidget { const RepaintBoundary({ Key? key, Widget? child }) : super(key: key, child: child); factory RepaintBoundary.wrap(Widget child, int childIndex) { final Key key = child.key != null ? ValueKey<Key>(child.key!) : ValueKey<int>(childIndex); return RepaintBoundary(key: key, child: child); } static List<RepaintBoundary> wrapAll(List<Widget> widgets) => <RepaintBoundary>[ for (int i = 0; i < widgets.length; ++i) RepaintBoundary.wrap(widgets[i], i), ]; @override RenderRepaintBoundary createRenderObject(BuildContext context) => RenderRepaintBoundary(); }复制代码
RepaintBoundary中创建的RenderObject是RenderRepaintBoundary,下面是RenderRepaintBoundary的代码
class RenderRepaintBoundary extends RenderProxyBox { RenderRepaintBoundary({ RenderBox? child }) : super(child); //isRepaintBoundary默认是返回false,RenderRepaintBoundary中返回的是true @override bool get isRepaintBoundary => true; //,,,省略无关代码 }复制代码
isRepaintBoundary在RenderObject中默认是返回false,RenderRepaintBoundary中返回的是true
RenderObject中isRepaintBoundary的作用
当RenderObject中isRepaintBoundary返回时true时当前节点的RenderObject(以及子节点)的绘制会在新创建Layer完成,这样就和其他Layer做了隔离,因为Layer是可以复用的,这样帧刷新的时候就不需要把每个RenderObject的paint方法都执行一遍。关于Layer的介绍可参考 初识Flutter中的Layer,下面我们是看看isRepaintBoundary返回true时是怎么创建Layer的。
核心代码如下:
void paintChild(RenderObject child, Offset offset) { //1,isRepaintBoundary = true if (child.isRepaintBoundary) { //2,结束当前layer的绘制 stopRecordingIfNeeded(); //3, _compositeChild(child, offset); } else { child._paintWithContext(this, offset); } } //3,合成child void _compositeChild(RenderObject child, Offset offset) { // Create a layer for our child, and paint the child into it. if (child._needsPaint) { //4,如果child需要被绘制(_needsPaint=true代表当前节点或者当前节点子孩子被PipelineOwer标记出需要被重绘) repaintCompositedChild(child, debugAlsoPaintedParent: true); } else { } final OffsetLayer childOffsetLayer = child._layer! as OffsetLayer; childOffsetLayer.offset = offset; appendLayer(child._layer!); } //4, static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) { //重新绘制 _repaintCompositedChild( child, debugAlsoPaintedParent: debugAlsoPaintedParent, ); } static void _repaintCompositedChild( RenderObject child, {bool debugAlsoPaintedParent = false, PaintingContext? childContext,}) { OffsetLayer? childLayer = child._layer as OffsetLayer?; if (childLayer == null) { child._layer = childLayer = OffsetLayer(); } else { childLayer.removeAllChildren(); } //创建新的PaintingContext,新的PaintingContext会创建新的PictureLayer childContext ??= PaintingContext(child._layer!, child.paintBounds); child._paintWithContext(childContext, Offset.zero); childContext.stopRecordingIfNeeded(); } 复制代码
流程图如下:
从上述流程可以看出当isRepaintBoundary=false时,就会触发paint的方法,我们假设下图所有RenderObject的isRepaintBoundary=false且其中RenderObject4被标记需要刷新
RenderObject4会自下而上寻找自己的父亲节点,直到找到父节点为isRepaintBoundary=true为止,然后把父节点依次标记需要刷新(_needsPaint = true),如下图所示:
找到最顶层的父节点,然后执行paint方法,最终的结果就是遍历执行了所有RenderObject的paint方法,如下图所示:
如果RenderObject4的上一级父节点就是isRepaintBoundary=true,那么流程就如下
寻找父节点isRepaintBoundary=true
如果RenderObject4被标记需要刷新,RenderObject1和RenderObject4需要执行paint方法:
假如是RenderObject2,RenderObject3,RenderObject5,RenderObject6,RenderObject7中有一个需要刷新,右边标颜色的节点会执行paint方法
综上流程分析,假设场景是RenderObject4的绘制很耗但是是刷新不频繁,RenderObject5,RenderObject6,RenderObject7的刷新很频繁,我们使用RepaintBoundary对RenderObject4对应的Widget包一层这样可以缩短渲染时绘制阶段的耗时从而降低卡顿问题。
使用到RepaintBoundary的地方
在Flutter framework中的有些Widget就使用到RepaintBoundary了
Flowl
流式布局每个child都是独立的layer渲染
Flow({ Key? key, required this.delegate, List<Widget> children = const <Widget>[], this.clipBehavior = Clip.hardEdge, }) : assert(delegate != null), assert(clipBehavior != null), super(key: key, children: RepaintBoundary.wrapAll(children));复制代码
RepaintBoundary中的源码:
factory RepaintBoundary.wrap(Widget child, int childIndex) { assert(child != null); final Key key = child.key != null ? ValueKey<Key>(child.key!) : ValueKey<int>(childIndex); return RepaintBoundary(key: key, child: child); } /// Wraps each of the given children in [RepaintBoundary]s. /// /// The key for each [RepaintBoundary] is derived either from the wrapped /// child's key (if the wrapped child has a non-null key) or from the wrapped /// child's index in the list. static List<RepaintBoundary> wrapAll(List<Widget> widgets) => <RepaintBoundary>[ for (int i = 0; i < widgets.length; ++i) RepaintBoundary.wrap(widgets[i], i), ];复制代码
SliverChildBuilderDelegate
SliverChildBuilderDelegate这个是ListView.builder的时候内部会创建SliverChildBuilderDelegate,列表大量item彼此之间独立layer渲染
@override Widget? build(BuildContext context, int index) { assert(builder != null); if (addRepaintBoundaries) child = RepaintBoundary(child: child); if (addSemanticIndexes) { final int? semanticIndex = semanticIndexCallback(child, index); if (semanticIndex != null) child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child); } if (addAutomaticKeepAlives) child = AutomaticKeepAlive(child: child); return KeyedSubtree(child: child, key: key); }复制代码
总结
以上是对RepaintBoundary的作用分析,实则是对RenderObject的原理讲解。希望通过此篇文章帮助到大家提升的对Flutter渲染机制的认识。
作者:二两陈皮
链接:https://juejin.cn/post/7047809275409891359