当前位置:

Flutter Framework 渲染流程四Layer

访客 2024-04-24 565 0

本文是FlutterFramework渲染流程分析中的第四篇

  • FlutterFramework渲染流程分析(一):开篇
  • FlutterFramework渲染流程分析(二):Element
  • FlutterFramework渲染流程分析(三):RenderObject
  • FlutterFramework渲染流程分析(四):Layer
  • FlutterFramework渲染流程分析(五):常见问题分析

上篇我们讲了RenderObjectlayoutpaint流程。layout最终计算出来的布局信息是提供给paint使用的,paintflush阶段会调用到自身的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通过_cliplayer的裁剪,得到的新图层重新赋值给layer。先看一下RenderObject中关于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

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,一个是estimatedBoundsestimatedBounds限制了图层的位置和大小,ContainerLayerLayer的一种,从名字就猜到它是用来组合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操作传进去了。

pushLayer///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);

childLayercode4.5中传进来的ClipPathLayer,已经限制了裁剪区域。pushLayer在把childLayer添加到_containerLayer之后,同时将它传递到childContext去绘制,这样就限制了child绘制和区域,实现裁剪的功能。

clipPathAndRepaint///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,而LayerHandlelayer属性默认是null的。上篇我们讲过RenderObject的继承关系,根RenderObjectRenderView,通过追踪一下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是TransformLayerrootLayer.attach到当前RenderView上时,也会将RenderView传递到所有childlayer上,也就是所有layerowner都会是同一个RenderView

现在我们能拿到rootLayer了,只是rootLayer怎么去绘制到界面上还不清楚。实际上对于layer这个对象我们目前还一知半解,只知道它里面可能保存着绘制信息。跟前几篇一样,还是从对象的关键属性和继承关系着手看

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来说,它的绘制信息就是clipRectclipBehavior,然后通过ui.SceneBuilder中的pushClipRect调用Engine中相应的操作方法生成EngineLayer并保存下来。ui.SceneBuilderui.EngineLayer都是Engine对象。

每一个LayeraddToScene都会触发底下所有子孙节点的更新,子孙节点或是命中缓存处理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中进行绘制的。

到现在,WidgetElementRenderObjectLayer,包括LayoutPaint流程相关的东西都讲完了,最后一篇文章会根据前面积累的这些知识点,重头梳理一下整个渲染流程,从首帧渲染到我们日常开发中比较关注的部分,做一个完整性补充。

发表评论

  • 评论列表
还没有人评论,快来抢沙发吧~