本文主要记录了如何一步步学习了解Flutter视图绘制原理,然后应用到性能监控和性能优化的实践
ydtech
Flutter的架构主要分成三层:Framework,Engine,Embedder。
1.Framework使用dart实现,包括MaterialDesign风格的Widget,Cupertino(针对iOS)风格的Widgets,文本/图片/按钮等基础Widgets,渲染,动画,手势等。此部分的核心代码是:flutter仓库下的flutterpackage,以及sky_engine仓库下的io,async,ui(dart:ui库提供了Flutter框架和引擎之间的接口)等package。
2.Engine使用C++实现,主要包括:Skia,Dart和Text。Skia是开源的二维图形库,提供了适用于多种软硬件平台的通用API。
对于开发者来说,使用最多的还是framework,我就从Flutter的入口函数开始一步步往下走,分析一下Flutter视图绘制的原理。
在Flutter应用中,main()函数最简单的实现如下:
voidmain(){runApp(MyApp());}runApp方法调用了WidgetsFlutterBinding类ensureInitialized、attachRootWidget(app)、scheduleWarmUpFrame()三个方法,代码如下
//参数app是一个widget,是Flutter应用启动后要展示的第一个Widget。voidrunApp(Widgetapp){WidgetsFlutterBinding.ensureInitialized()..scheduleAttachRootWidget(app)..scheduleWarmUpFrame();}2.1**WidgetsFlutterBinding**WidgetsFlutterBinding继承自BindingBase并混入了很多Binding,查看这些Binding的源码可以发现这些Binding中基本都是监听并处理Window对象(包含了当前设备和系统的一些信息以及FlutterEngine的一些回调)的一些事件,然后将这些事件按照Framework的模型包装、抽象然后分发。
WidgetsFlutterBinding正是粘连Flutterengine与上层Framework的“胶水”。
1.GestureBinding:
提供了window.onPointerDataPacket回调,绑定Framework手势子系统,是Framework事件模型与底层事件的绑定入口。
2.ServicesBinding:
提供了window.onPlatformMessage回调,用于绑定平台消息通道(messagechannel),主要处理原生和Flutter通信。
3.SchedulerBinding:
提供了window.onBeginFrame和window.onDrawFrame回调,监听刷新事件,绑定Framework绘制调度子系统。
4.PaintingBinding:
绑定绘制库,主要用于处理图片缓存。
5.SemanticsBinding:
语义化层与Flutterengine的桥梁,主要是辅助功能的底层支持。
6.RendererBinding:
提供了window.onMetricsChanged、window.onTextScaleFactorChanged等回调。它是渲染树与Flutterengine的桥梁。
7.WidgetsBinding:
提供了window.onLocaleChanged、onBuildScheduled等回调。它是Flutterwidget层与engine的桥梁。
WidgetsFlutterBinding.ensureInitialized()负责初始化一个WidgetsBinding的全局单例,代码如下:
classWidgetsFlutterBindingextendsBindingBasewithGestureBinding,ServicesBinding,SchedulerBinding,PaintingBinding,SemanticsBinding,RendererBinding,WidgetsBinding{staticWidgetsBindingensureInitialized(){if(WidgetsBinding.instance==null)WidgetsFlutterBinding();returnWidgetsBinding.instance;}}看到这个混入(with)很多的,下面先看父类:BindingBase
abstractclassBindingBase{...ui.SingletonFlutterWindowgetwindow=>ui.window;//获取window实例@protected@mustCallSupervoidinitInstances(){assert(!_debugInitialized);assert((){_debugInitialized=true;returntrue;}());}}看到有句代码Windowgetwindow=>ui.window链接宿主操作系统的接口,也就是Flutterframework链接宿主操作系统的接口。系统中有一个Window实例,可以从window属性来获取,看看源码:
voidattachRootWidget(WidgetrootWidget){finalboolisBootstrapFrame=renderViewElement==null;_readyToProduceFrames=true;_renderViewElement=RenderObjectToWidgetAdapter
RenderViewgetrenderView=>_pipelineOwner.rootNode!asRenderView;
renderView是RendererBinding中拿到PipelineOwner.rootNode,PipelineOwner在RenderingPipeline中起到重要作用:
随着UI的变化而不断收集『DirtyRenderObjects』随之驱动RenderingPipeline刷新UI。
简单讲,PipelineOwner是『RenderObjectTree』与『RendererBinding』间的桥梁。
最终调用attachRootWidget,执行会调用RenderObjectToWidgetAdapter的attachToRenderTree方法,该方法负责创建根element,即RenderObjectToWidgetElement,并且将element与widget进行关联,即创建出widget树对应的element树。如果element已经创建过了,则将根element中关联的widget设为新的,由此可以看出element只会创建一次,后面会进行复用。BuildOwner是widgetframework的管理类,它跟踪哪些widget需要重新构建。代码如下:
RenderObjectToWidgetElement
下面是scheduleWarmUpFrame()方法的部分实现(省略了无关代码):
voidscheduleWarmUpFrame(){...Timer.run((){handleBeginFrame(null);});Timer.run((){handleDrawFrame();resetEpoch();});//锁定事件lockEvents(()async{awaitendOfFrame;Timeline.finishSync();});...}该方法中主要调用了handleBeginFrame()和handleDrawFrame()两个方法。
查看handleBeginFrame()和handleDrawFrame()两个方法的源码,可以发现前者主要是执行了transientCallbacks队列,而后者执行了persistentCallbacks和postFrameCallbacks队列。
3.postFrameCallbacks:在Frame结束时只会被调用一次,调用后会被系统移除,可由SchedulerBinding.instance.addPostFrameCallback()注册。
注意,不要在此类回调中再触发新的Frame,这可以会导致循环。
真正的渲染和绘制逻辑在RendererBinding中实现,查看其源码,发现在其initInstances()方法中有如下代码:
voidinitInstances(){...//省略无关代码addPersistentFrameCallback(_handlePersistentFrameCallback);}void_handlePersistentFrameCallback(DurationtimeStamp){drawFrame();}voiddrawFrame(){assert(renderView!=null);pipelineOwner.flushLayout();//布局pipelineOwner.flushCompositingBits();//重绘之前的预处理操作,检查RenderObject是否需要重绘pipelineOwner.flushPaint();//重绘renderView.compositeFrame();//将需要绘制的比特数据发给GPUpipelineOwner.flushSemantics();//thisalsosendsthesemanticstotheOS.}需要注意的是:由于RendererBinding只是一个mixin,而with它的是WidgetsBinding,所以需要看看WidgetsBinding中是否重写该方法,查看WidgetsBinding的drawFrame()方法源码:
@overridevoiddrawFrame(){...//省略无关代码try{if(renderViewElement!=null)buildOwner.buildScope(renderViewElement);super.drawFrame();//调用RendererBinding的drawFrame()方法buildOwner.finalizeTree();}}在调用RendererBinding.drawFrame()方法前会调用buildOwner.buildScope()(非首次绘制),该方法会将被标记为“dirty”的element进行rebuild()
我们再来看WidgetsBinding,在initInstances()方法中创建BuildOwner对象,然后执行buildOwner!.onBuildScheduled=_handleBuildScheduled;,这里将_handleBuildScheduled赋值给了buildOwnder的onBuildScheduled属性。
BuildOwner对象,它负责跟踪哪些widgets需要重新构建,并处理应用于widgets树的其他任务,其内部维护了一个_dirtyElements列表,用以保存被标“脏”的elements。
每一个element被新建时,其BuildOwner就被确定了。一个页面只有一个buildOwner对象,负责管理该页面所有的element。
//WidgetsBindingvoidinitInstances(){...buildOwner!.onBuildScheduled=_handleBuildScheduled;...}());}当调用buildOwner.onBuildScheduled()时,便会走下面的流程。
//WidgetsBinding类void_handleBuildScheduled(){ensureVisualUpdate();}//SchedulerBinding类voidensureVisualUpdate(){switch(schedulerPhase){caseSchedulerPhase.idle:caseSchedulerPhase.postFrameCallbacks:scheduleFrame();return;caseSchedulerPhase.transientCallbacks:caseSchedulerPhase.midFrameMicrotasks:caseSchedulerPhase.persistentCallbacks:return;}}当schedulerPhase处于idle状态,会调用scheduleFrame,然后经过window.scheduleFrame()中的performDispatcher.scheduleFrame()去注册一个VSync监听。
voidscheduleFrame(){...window.scheduleFrame();...}2.4**小结**Flutter从启动到显示图像在屏幕主要经过:首先监听处理window对象的事件,将这些事件处理包装为Framework模型进行分发,通过widget创建element树,接着通过scheduleWarmUpFrame进行渲染,接着通过Rendererbinding进行布局,绘制,最后通过调用ui.window.render(scene)Scene信息发给Flutterengine,Flutterengine最后调用渲染API把图像画在屏幕上。
我大致整理了一下Flutter视图绘制的时序图,如下
在对视图绘制有一定的了解后后,思考一个问题,怎么在视图绘制的过程中去把控性能,优化性能,我们先来看一下Flutter官方提供给我们的两个性能监控工具。
1.observatory
observatory:在engine/shell/testings/observatory可以找到它的具体实现,它开启了一个ServiceClient,用于获取dartvm运行状态.flutterapp启动的时候会生成一个当前的observatory服务器的地址
比方说选择了timeline后,可以进行性能分析,如图
2.devTools
打开后的页面
devtools中的timeline就是performance,我们选择之后页面如下,操作体验上好了很多
observatory与devtools都是通过vm_service实现的,网上使用指南比较多,这边就不多赘述了,我这边主要介绍一下DartVMService(后面简称)vm_service,是Dart虚拟机内部提供的一套Web服务,数据传输协议是JSON-RPC2.0。
不过我们并不需要要自己去实现数据请求解析,官方已经写好了一个可用的DartSDK给我们用:vm_service。vm_service在启动的时候会在本地开启一个WebSocket服务,服务URI可以在对应的平台中获得:
1)Android在
2)iOS在FlutterEngine.observatoryUrl中。
有了URI之后我们就可以使用的服务了,官方有一个帮我们写好的SDK:vm_service
Future
获取内存信息,调用一个VmService实例的getMemoryUsage,就能拿到当前的内存信息
获取FlutterAPP的FPS,官方提供了好几个办法来让我们在开发Flutterapp的过程中可以使用查看fps等性能数据,如devtools,具体见文档DebuggingFlutterapps、Flutterperformanceprofiling等。
//需监听fps时注册voidstart(){SchedulerBinding.instance.addTimingsCallback(_onReportTimings);}//不需监听时移除voidstop(){SchedulerBinding.instance.removeTimingsCallback(_onReportTimings);}void_onReportTimings(List
1)flutterdart代码的异常(包含app和framework代码两种情况,一般不会引起闪退,你猜为什么)
2)flutterengine的崩溃日志(一般会闪退)
Dart有一个Zone的概念,有点类似sandbox的意思。不同的Zone代码上下文是不同的互不影响,Zone还可以创建新的子Zone。Zone可以重新定义自己的print、timers、microtasks还有最关键的howuncaughterrorsarehandled未捕获异常的处理
runZoned((){Future.error("asynchronouserror");},onError:(dynamice,StackTracestack){reportError(e,stack);});1.Flutterframework异常捕获
注册FlutterError.onError回调,用于收集Flutterframework外抛的异常。
runZoned((){Future.error("asynchronouserror");},onError:(dynamice,StackTracestack){reportError(e,stack);});2.Flutterengine异常捕获
flutterengine部分的异常,以Android为例,主要为libfutter.so发生的错误。
这部份可以直接交给native崩溃收集sdk来处理,比如firebasecrashlytics、bugly、xCrash等等
我们需要将dart异常及堆栈通过MethodChannel传递给buglysdk即可。
收集到异常之后,需要查符号表(symbols)还原堆栈。
首先需要确认该flutterengine所属版本号,在命令行执行:
flutter--version
输出如下:
其次,在flutterinfra上找到对应cpuabi的symbols.zip并下载,解压后,可以得到带有符号信息的debugso文件——libflutter.so,然后按照平台文档上传进行堆栈还原就可以了,如bugly平台就提供了上传工具
java-jarbuglySymbolAndroid.jar-ixxx
在业务开发中我们要学会用devtools来检测工程性能,这样有助于我们实现健壮性更强的应用,在排查过程中,我发现视频详情页存在渲染耗时的问题,如图
VideoControls控件的build耗时是28.6ms,如图
所以这里我们的优化方案是提高build效率,降低Widgettree遍历的出发点,将setState刷新数据尽量下发到底层节点,所以将VideoControl内触发刷新的子组件抽取成独立的Widget,setState下发到抽取出的Widget内部
优化后为11.0ms,整体的平均帧率也达到了了60fps,如图
接下来分析下paint过程有没有可以优化的部分,我们打开debugProfilePaintsEnabled变量分析可以看到Timeline显示的paint层级,如图
我们发现频繁更新的_buildPositionTitle和其他Widget在同一个layer中,这里我们想到的优化点是利用RepaintBoundary提高paint效率,它为经常发生显示变化的内容提供一个新的隔离layer,新的layerpaint不会影响到其他layer
看下优化后的效果,如图
在Flutter开发过程中,我们用devtools工具排查定位页面渲染问题时,主要有两点:
1.提高build效率,setState刷新数据尽量下发到底层节点。
2.提高paint效率,RepaintBoundry创建单独layer减少重绘区域。
当然Flutter中性能调优远不止这一种情况,build/layout/paint每一个过程其实都有很多能够优化的细节。
1.绘制原理讲解,我们review了一下源码,发现整个渲染过程就是一个闭环,Framework,Engine,Embedder各司其职,简单来说就是Embedder不断拿回Vsync信号,Framework将dart代码交给Engine翻译成跨平台代码,再通过Embedder回调宿主平台;
2.性能监控就是不断得在这个循环中去插入我们的哨兵,观察整个生态,获取异常数据上报;
3.性能优化通过一次项目实践,学习怎么用工具提升我们定位问题的效率。
优点:
我们可以看到Flutter在视图绘制过程中形成了闭环,双端基本保持了一致性,所以我们的开发效率得到了极大的提升,性能监控和性能优化也比较方便。
缺点:
2)实现动态化机制,目前没有比较好的开源技术可以去借鉴。