本文是FlutterFramework渲染流程分析
中的第四篇
- FlutterFramework渲染流程分析(一):开篇
- FlutterFramework渲染流程分析(二):Element
- FlutterFramework渲染流程分析(三):RenderObject
- FlutterFramework渲染流程分析(四):Layer
- FlutterFramework渲染流程分析(五):常见问题分析
上篇我们讲了RenderObject
中layout
和paint
流程。layout
最终计算出来的布局信息是提供给paint
使用的,paint
的flush
阶段会调用到自身的voidpaint(PaintingContextcontext,Offsetoffset)
方法,我们来回顾下这个方法。
///code4.1///[RenderClipOval]中的实现voidpaint(PaintingContextcontext,Offsetoffset){if(child!=null){if(clipBehavior!=Clip.none){_updateClip();layer=context.pushClipPath(needsCompositing,offset,_clip,_getClipPath(_clip!),super.paint,clipBehavior:clipBehavior,oldLayer:layerasClipPathLayer?,);}else{context.paintChild(child!,offset);layer=null;}}else{layer=null;}}
这里引入了两个重要的对象,一个是Layer
,一个是PaintingContext
。_clip
是一个Rect
对象,context.pushClipPath
通过_clip
做layer
的裁剪,得到的新图层重新赋值给layer
。先看一下RenderObject
中关于layer
的定义。
///code4.2///[RenderObejct]中的代码@protectedContainerLayer?getlayer{assert(!isRepaintBoundary||_layerHandle.layer==null||_layerHandle.layerisOffsetLayer);return_layerHandle.layer;}@protectedsetlayer(ContainerLayer?newLayer){assert(!isRepaintBoundary,'Attemptedtosetalayertoarepaintboundaryrenderobject.\n''TheframeworkcreatesandassignsanOffsetLayertoarepaint''boundaryautomatically.',);_layerHandle.layer=newLayer;}finalLayerHandle<ContainerLayer>_layerHandle=LayerHandle<ContainerLayer>();
对layer
的操作都会转到_layerHandle
上来,LayerHandle
的接口比较简单,看下面setlayer(T?layer)
方法的实现,它只是持有layer
并对layer
做引用计数而已,也就是这个layer
被多少个LayerHandle
持有了,当持有数为0时,_layer?_unref()
里就会对layer
做资源释放工作。
///code4.3classLayerHandle<TextendsLayer>{///Createanewlayerhandle,optionallyreferencinga[Layer].LayerHandle([this._layer]){if(_layer!=null){_layer!._refCount=1;}}T?_layer;///The[Layer]whoseresourcesthisobjectkeepsalive.//////Settinganewvalueornullwilldisposethepreviouslyheldlayerif///therearenootheropenhandlestothatlayer.T?getlayer=>_layer;setlayer(T?layer){assert(layer?.debugDisposed!=true,'Attemptedtocreateahandletoanalreadydisposedlayer:$layer.',);if(identical(layer,_layer)){return;}_layer?._unref();_layer=layer;if(_layer!=null){_layer!._refCount=1;}}@overrideStringtoString()=>'LayerHandle(${_layer!=null?_layer.toString():'DISPOSED'})';}
对Layer
先暂时了解这么多,再看PaintingContext
。
///code4.4classPaintingContextextendsClipContext{///Createsapaintingcontext.//////Typicallyonlycalledby[PaintingContext.repaintCompositedChild]///and[pushLayer].@protectedPaintingContext(this._containerLayer,this.estimatedBounds);finalContainerLayer_containerLayer;///Anestimateoftheboundswithinwhichthepaintingcontext's[canvas]///willrecordpaintingcommands.Thiscanbeusefulfordebugging.//////Thecanvaswillallowpaintingoutsidethesebounds.//////The[estimatedBounds]rectangleisinthe[canvas]coordinatesystem.finalRectestimatedBounds;...//一系列layer操作ClipRectLayer?pushClipRect(...)...ClipRRectLayer?pushClipRRect(...)...ClipPathLayer?pushClipPath(...)...ColorFilterLayerpushColorFilter(...)...TransformLayer?pushTransform(...)...OpacityLayerpushOpacity(...)......}
PaintingContext
是绘制上下文,包含一系列layer操作,构造函数接受两个参数,一个是_containerLayer
,一个是estimatedBounds
。estimatedBounds
限制了图层的位置和大小,ContainerLayer
是Layer
的一种,从名字就猜到它是用来组合Layer
的。根据code4.1
中的代码,看一下pushClipPath
的实现
///code4.5ClipPathLayer?pushClipPath(boolneedsCompositing,Offsetoffset,Rectbounds,PathclipPath,PaintingContextCallbackpainter,{ClipclipBehavior=Clip.antiAlias,ClipPathLayer?oldLayer}){if(clipBehavior==Clip.none){painter(this,offset);returnnull;}finalRectoffsetBounds=bounds.shift(offset);finalPathoffsetClipPath=clipPath.shift(offset);//这里我们着重看这条件分支里面的处理if(needsCompositing){finalClipPathLayerlayer=oldLayer??ClipPathLayer();layer..clipPath=offsetClipPath..clipBehavior=clipBehavior;//构造一个新的layer,push到_containerLayer上pushLayer(layer,painter,offset,childPaintBounds:offsetBounds);returnlayer;}else{//通过canvas操作将裁剪操作保存到_currentLayer上clipPathAndPaint(offsetClipPath,clipBehavior,offsetBounds,()=>painter(this,offset));returnnull;}}//super.paint的实现@overridevoidpaint(PaintingContextcontext,Offsetoffset){if(child!=null){context.paintChild(child!,offset);}}
先明确一下,这个方法里面有一个方法传参painter
,根据code4.1中的代码,就是super.paint
,看它的实现调用[PaintingContext].paintChild
的操作,其实就是绘制子节点,无论是pushLayer
,还是clipPathAndPaint
,都将这个painter
操作传进去了。
///code4.6voidpushLayer(ContainerLayerchildLayer,PaintingContextCallbackpainter,Offsetoffset,{Rect?childPaintBounds}){...appendLayer(childLayer);//createContext实现如下面的代码,创建一个新的绘制上下文,此时的childLayer是上面传的ClipPathLayerfinalPaintingContextchildContext=createChildContext(childLayer,childPaintBounds??estimatedBounds);//执行child的绘制painter(childContext,offset);...}@protectedvoidappendLayer(Layerlayer){...//移除layer原先的parent(如果存在)layer.remove();//加到_containerLayer_containerLayer.append(layer);}@protectedPaintingContextcreateChildContext(ContainerLayerchildLayer,Rectbounds){returnPaintingContext(childLayer,bounds);
childLayer
是code4.5
中传进来的ClipPathLayer
,已经限制了裁剪区域。pushLayer
在把childLayer
添加到_containerLayer
之后,同时将它传递到childContext
去绘制,这样就限制了child
绘制和区域,实现裁剪的功能。
///code4.7void_clipAndPaint(voidFunction(booldoAntiAlias)canvasClipCall,ClipclipBehavior,Rectbounds,VoidCallbackpainter){//canvas操作canvas.save();switch(clipBehavior){caseClip.none:break;caseClip.hardEdge:canvasClipCall(false);caseClip.antiAlias:canvasClipCall(true);caseClip.antiAliasWithSaveLayer:canvasClipCall(true);canvas.saveLayer(bounds,Paint());}//painter实际上就是childContext.paintpainter();if(clipBehavior==Clip.antiAliasWithSaveLayer){canvas.restore();}canvas.restore();}voidclipPathAndPaint(Pathpath,ClipclipBehavior,Rectbounds,VoidCallbackpainter){//执行canvas的裁剪操作_clipAndPaint((booldoAntiAlias)=>canvas.clipPath(path,doAntiAlias:doAntiAlias),clipBehavior,bounds,painter);}
根据clipBehavior,canvas先做了一个clipPath
的裁剪操作,然后再绘制child。canvas操作最终是会反映到_currentLayer
上的,看下面这段代码:
///code4.8boolget_isRecording{//开始记录时,_canvas会被初始化finalboolhasCanvas=_canvas!=null;...returnhasCanvas;}//RecordingstatePictureLayer?_currentLayer;ui.PictureRecorder?_recorder;Canvas?_canvas;@overrideCanvasgetcanvas{if(_canvas==null){_startRecording();}assert(_currentLayer!=null);return_canvas!;}void_startRecording(){assert(!_isRecording);_currentLayer=PictureLayer(estimatedBounds);_recorder=ui.PictureRecorder();_canvas=Canvas(_recorder!);_containerLayer.append(_currentLayer!);}@protected@mustCallSupervoidstopRecordingIfNeeded(){if(!_isRecording){return;}..._currentLayer!.picture=_recorder!.endRecording();_currentLayer=null;_recorder=null;_canvas=null;}
先看_startRecording
方法,开始记录绘制时,_canvas
会被初始化,同时会传入_recorder
用来记录任务在_canvas
上执行的操作,最终在stopRecordingIfNeeded
时,通过recorder!.endRecording()
将之前记录的绘制动作输出成一个picture
,然后赋值给_currentLayer!.picture
,注意_currentLayer
是在_startRecording
时就已经append
到_containerLayer
上的。
无论是pushLayer
还是clipPathAndRepaint
,其绘制结果都存在于_containerLayer
上,同时这个_containerLayer
是在PaintingContext实例化时由外部提供。还记得我们code4.5中说到的绘制子节点的方法painter
,它最终会走到[PaintContext].paintChild
这里
///code4.9voidpaintChild(RenderObjectchild,Offsetoffset){...if(child.isRepaintBoundary){stopRecordingIfNeeded();//这里会生成一个新的layer,用来绘制child//_compositeChild方法也会根据_needPaint这个标记去判断是不是要重新生成_compositeChild(child,offset);}elseif(child._wasRepaintBoundary){child._layerHandle.layer=null;child._paintWithContext(this,offset);}else{child._paintWithContext(this,offset);}}
如果是绘制边界,那么新建一个PaintContext
去绘制;否则将其绘制到当前的PaintingContext
上。
从上面的绘制操作可以看出,每一个节点在绘制时,都要绘制子节点;根据isRepaintBoundary
判断要不要生成一个独立的绘制区域,再根据_needsPaint
判断是不是要新建一个PaintingContext
,这样就能保证每次绘制时只绘制脏区域。
另外,PaintContext
里面针对childlayer的处理完成后,最终都会走到code4.6中的appendLayer
方法里,这个方法会走到_containerLayer.append
,形成LayerTree
。
那么顶层layer
是哪里来的?这就回归到RenderObject
中关于layer
的传递了,RenderObject
持有着一个LayerHandle
,而LayerHandle
里layer
属性默认是null
的。上篇我们讲过RenderObject
的继承关系,根RenderObject
是RenderView
,通过追踪一下RenderView
的实例化,就能找到在RenderView._updateMatricesAndCreateNewRootLayer
时生成的rootLayer
。
///code4.10voidprepareInitialFrame(){...scheduleInitialLayout();scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());...}voidscheduleInitialPaint(ContainerLayerrootLayer){...//设置layer_layerHandle.layer=rootLayer;...owner!._nodesNeedingPaint.add(this);}Matrix4?_rootTransform;TransformLayer_updateMatricesAndCreateNewRootLayer(){_rootTransform=configuration.toMatrix();finalTransformLayerrootLayer=TransformLayer(transform:_rootTransform);rootLayer.attach(this);...returnrootLayer;}
rootRenderObject是RenderView
,rootLayer是TransformLayer
。rootLayer.attach
到当前RenderView
上时,也会将RenderView
传递到所有childlayer
上,也就是所有layer
的owner
都会是同一个RenderView
。
现在我们能拿到rootLayer
了,只是rootLayer
怎么去绘制到界面上还不清楚。实际上对于layer
这个对象我们目前还一知半解,只知道它里面可能保存着绘制信息。跟前几篇一样,还是从对象的关键属性和继承关系着手看
///code4.11//1.如code4.3中提到的,主要是用来配合LayerHandle做引用计数和资源释放int_refCount=0;void_unref(){};//2.继承自AbstractNode的接口,构成树形结构。从parent的返回可以断定每一个节点都是或继承自ContainerLayerContainerLayer?getparent=>super.parentasContainerLayer?;voiddropChild(Layerchild){};voidadoptChild(Layerchild){};//3.兄弟节点双向链接结构,在append时会处理这个兄弟节点的关系Layer?getnextSibling=>_nextSibling;Layer?_nextSibling;Layer?getpreviousSibling=>_previousSibling;Layer?_previousSibling;//前面提到的节点的关系的形成就在这个方法里面处理voidappend(Layerchild){};//4.标记这个Layer是否已经更新过,标志的节点会在随后走到addToScene方法bool_needsAddToScene=true;voidmarkNeedsAddToScene(){};boolgetalwaysNeedsAddToScene=>false;//5.addToScene是将当前Layer中绘制信息通过builder推送到Engine上绘制//生成的绘制信息保存到_engineLayer上。voidaddToScene(ui.SceneBuilderbuilder);vid_addToSceneWithRetainedRendering(ui.SceneBuilderbuilder){};//6.如5中提到的,_engineLayer是当前layer在Engine中的映射对象。ui.EngineLayer?getengineLayer=>_engineLayer;setengineLayer(ui.EngineLayer?value){};ui.EngineLayer?_engineLayer;
这段代码比较长,但总的来看1、2、3点都是为了形成LayerTree
的;4、5、6点是跟绘制有关的,_enginelayer
就是这个Layer
的绘制结果,它是Layer在Engine中的映射。在它的子类上,会保存着一些绘制的信息,比如下面这段在ClipRectLayer
中的代码
//code4.11void_addToSceneWithRetainedRendering(ui.SceneBuilderbuilder){...if(!_needsAddToScene&&_engineLayer!=null){//节点有更新时会走addRetained,就是会把上次的_engineLayer推送到Engine中渲染builder.addRetained(_engineLayer!);return;}//否则从新生成_engineLayer,看以下的实现addToScene(builder);..._needsAddToScene=false;}//Layer中的addToScene是空实现,看子类ClipRectLayer的voidaddToScene(ui.SceneBuilderbuilder){...boolenabled=true;...if(enabled){engineLayer=builder.pushClipRect(clipRect!,clipBehavior:clipBehavior,oldLayer:_engineLayerasui.ClipRectEngineLayer?,);}else{engineLayer=null;}//绘制子节点,这里是一个递归处理,会更新底下所有的子孙节点的LayeraddChildrenToScene(builder);if(enabled){builder.pop();}}
对于ClipRectLayer
来说,它的绘制信息就是clipRect
和clipBehavior
,然后通过ui.SceneBuilder
中的pushClipRect
调用Engine
中相应的操作方法生成EngineLayer
并保存下来。ui.SceneBuilder
和ui.EngineLayer
都是Engine
对象。
每一个Layer
的addToScene
都会触发底下所有子孙节点的更新,子孙节点或是命中缓存处理builder.addRetained
,或是走重新添加addToScene
。不管哪种方式,最终都是利用ui.SceneBuilder
去跟Engine
层做交互,ui.SceneBulider
实例化在RenderView
中。
//code4.12//[RenderView]中的方法voidcompositeFrame(){...try{finalui.SceneBuilderbuilder=ui.SceneBuilder();finalui.Scenescene=layer!.buildScene(builder);if(automaticSystemUiAdjustment){_updateSystemChrome();}//_view是ui.FlutterView,用来绘制界面的而已。render也对应着Engine中的方法。_view.render(scene);scene.dispose();...}...}//ContainerLayer中的buildScene方法ui.ScenebuildScene(ui.SceneBuilderbuilder){updateSubtreeNeedsAddToScene();//这里即是更新layertree,builder会保存有layertree的信息addToScene(builder);..._needsAddToScene=false;//生成ui.Scene,返回给_view最终渲染到界面上finalui.Scenescene=builder.build();returnscene;}
ui.SceneBuilder
会根据layertree
的信息生成ui.Scene
,通过ui.FlutterView
最终将ui.Scene
渲染到界面上。渲染方法是_view.render
,它接受一个ui.Scene
对象,而ui.Scene
是通过ui.SceneBuilder
根据layertree
生成的。
本文讲了跟Paint
流程有关的几个重要的概念:Layer:存储着绘制信息的对象,同时持有着Engine层的绘制结果EngineLayer;PaintingContext:绘制上下文,处理Layer的操作,构建LayerTree
,同时最后也讲了关于形成的LayerTree
最终是怎么通知到Engine
中进行绘制的。
到现在,Widget
、Element
、RenderObject
、Layer
,包括Layout
、Paint
流程相关的东西都讲完了,最后一篇文章会根据前面积累的这些知识点,重头梳理一下整个渲染流程,从首帧渲染到我们日常开发中比较关注的部分,做一个完整性补充。