Flutter中文社区

本文主要记录了如何一步步学习了解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(container:renderView,debugShortDescription:'[root]',child:rootWidget,).attachToRenderTree(buildOwner!,renderViewElementasRenderObjectToWidgetElement);if(isBootstrapFrame){SchedulerBinding.instance!.ensureVisualUpdate();}}renderView变量是一个RenderObject,它是渲染树的根。renderViewElement变量是renderView对应的Element对象。可见该方法主要完成了根widget到根RenderObject再到根Element的整个关联过程。

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需要重新构建。代码如下:

RenderObjectToWidgetElementattachToRenderTree(BuildOwnerowner,[RenderObjectToWidgetElementelement]){if(element==null){owner.lockState((){element=createElement();assert(element!=null);element.assignOwner(owner);});owner.buildScope(element,(){element.mount(null,null);});}else{element._newWidget=this;element.markNeedsBuild();}returnelement;}2.3**scheduleWarmUpFrame**runApp的实现中,当调用完attachRootWidget后,最后一行会调用WidgetsFlutterBinding实例的scheduleWarmUpFrame()方法,该方法的实现在SchedulerBinding中,它被调用后会立即进行一次绘制(而不是等待"vsync"信号),在此次绘制结束前,该方法会锁定事件分发,也就是说在本次绘制结束完成之前Flutter将不会响应各种事件,这可以保证在绘制过程中不会再触发新的重绘。

下面是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在FlutterJNI.getObservatoryUri()中;

2)iOS在FlutterEngine.observatoryUrl中。

有了URI之后我们就可以使用的服务了,官方有一个帮我们写好的SDK:vm_service

Futureconnect()async{ServiceProtocolInfoinfo=awaitService.getInfo();if(info.serverUri==null){print("serviceprotocolurlisnull,startvmservicefail");return;}service=awaitgetService(info);print('socketconnectedinservice$info');vm=awaitservice.getVM();Listisolates=vm.isolates;main=isolates.firstWhere((ref)=>ref.name.contains('main')==true);main=isolates.first;connected=true;}FuturegetService(info)async{Uriuri=convertToWebSocketUrl(serviceProtocolUrl:info.serverUri);returnawaitvmServiceConnectUri(uri.toString(),log:StdoutLog());}获取frameworkVersion,调用一个VmService实例的callExtensionService,传入'flutterVersion',就能拿到当前的flutterframework和engine信息

获取内存信息,调用一个VmService实例的getMemoryUsage,就能拿到当前的内存信息

获取FlutterAPP的FPS,官方提供了好几个办法来让我们在开发Flutterapp的过程中可以使用查看fps等性能数据,如devtools,具体见文档DebuggingFlutterapps、Flutterperformanceprofiling等。

//需监听fps时注册voidstart(){SchedulerBinding.instance.addTimingsCallback(_onReportTimings);}//不需监听时移除voidstop(){SchedulerBinding.instance.removeTimingsCallback(_onReportTimings);}void_onReportTimings(Listtimings){//TODO}3.2**崩溃日志捕获上报**flutter的崩溃日志收集主要有两个方面:

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)实现动态化机制,目前没有比较好的开源技术可以去借鉴。

THE END
1.Python现在可以在线编程了!提供交互式编程环境,特别适合数据分析和科学计算。用户可以直接在浏览器中编写和运行代码,生成可视化结果。 3 在线编程访问入口 访问入口:https://ai-jupyter.com 网站还包括哪些板块: 鉴于上面的情况,建议铁铁们多学习新技术,因为我是AI相关的博主,给出普通人学AI的一个靠谱方法。 https://blog.csdn.net/xo3ylAF9kGs/article/details/140057699
2.在线编程语言深度研究探索其应用优势与挑战这个标题涵盖了编程兼容性:在线编程平台需要支持不同浏览器和设备,确保用户在不同环境下都能顺利使用。 用户体验:在线编程平台需要提供良好的用户体验,包括界面设计、操作流畅度等方面。 四、结论 在线编程语言作为一种新兴的编程模式,具有广泛的应用前景。随着技术的不断发展,在线编程语言将会克服现有的挑战,为编程领域带来更多的创新和进https://my.oschina.net/emacs_7610500/blog/14071106
3.当程序员无聊的时候,可以上的10个技术编程网站刘哥聊技术网站类型:技术编程,综合其他,网络科技,博客网站 网站简介:博客园是面向程序员的高品质IT技术学习社区,是程序员学习成长的地方。博客园致力于为程序员打造一个优秀的互联网平台,帮助程序 员学好IT技术,更好地用技术改变世界。 2.CSDN 网站名称: CSDN 网站地址:www.csdn.net https://www.cnblogs.com/liuhongfeng/p/4135267.html
4.科德放映室科德放映室(www.mycodes.net) - 提供最新免费电影电视剧在线观看下载!https://www.mycodes.net/
5.编程中国[杭州] 编程技术工程师 50k-100k [成都] 脚本协议逆向开发 8k-15k [重庆] 后端工程师 8k-30k [广州] 网络技术人才 80k-100k [武汉] 微服务工程师 8k-15k [杭州] 安卓开发工程师 10k-25k [常州] 嵌入式软件工程师 8k-15k [常州] 嵌入式软件工程师 https://www.bccn.net/
6.HTML在线编辑器菜鸟教程HTML 在线编辑器器是一款可以在线编写HTML,Javascript,CSS代码的编辑器,在编辑器中你可以添加额外的js库,在编写完成后你可以在线运行查看效果并下载代码。 编辑器使用简单,你只需要在指定的输入框中输入指定类型的代码,即可实时查看效果。 官网 HTML 在线编辑器https://www.runoob.com/w3cnote/html-online-editor.html
7.PLC编程实用指南(第3版)第1章PLC编程技术基础在线免费阅读PLC编程技术是有关编写、调试PLC(用户)程序的技术。掌握好这个技术,才能正确与有效地使用PLC。 1.1 PLC程序概念 PLC编程标准对程序的定义是:“所有编程语言元素和结构的一个逻辑集合”。传统或低档PLC编程语言元素和结构只是指令(包含操作数),它的程序简单地说,就是PLC指令的一个有序(逻辑)集合。所以,它的编程使用https://fanqienovel.com/reader/7110101073536224288
8.VIPCODE在线少儿编程,凭借专业技术赢得口碑!品牌动态目前国内少儿编程市场还处于刚起步的阶段市场的前景还是十分广阔的,选择现在这个时间加盟品牌是最合适不多的了。随着我国人们现在生活水平的不断提高,家长们对孩子素质教育也是越来越重视。尤其是在一些发达国家更是尤为重视。VIPCODE在线少儿编程,凭借专业技术赢得口碑! http://www.1637.com/news/RV0XV70FF8BY231014.html
9.API接口编程技术免费在线视频教程API接口编程技术,通过PHP,提前做好需要的接口,APP和小程序通过调用接口,获取到JSON数据。很多大公司都做了第三方接口,供程序员使用。中级 API PHP JSON30127人学习 10课时 01小时28分钟36秒课程总时长 2022-04-13更新 欧阳克 金牌讲师 共有32课程 学员44000人 讲师评分5. 2 温故而知新,可以为师矣。博客:https://www.php.cn/course/1174.html
10.大漠插件编程技术网大漠插件是一款编程图片文字识别插件 大漠 综合 插件 (dm.dll)采用vc6.0编写,识别速度超级快! 采用COM接口编写,适用于所有语言调用. 【注册VIP】 使用插件前。请在代码里用注册码连接大漠插件服务器注册VIP。一般在程序启动后reg命令注册。如果不注册VIP无法使用绑定窗口等高级命令bindwindow程序会崩溃。查看完整注册原理https://www.52hsxx.com/
11.c在线编程菜鸟教程计算机编程c语言入门c在线编程菜鸟教程-计算机编程c语言入门 序言 许多学过C语言的小伙伴们都说早已学完后C语言新手入门,但实践工作能力还滞留在很低的水准。大部分,她们设定了好多个for循环来暴力行为处理排列与组合难题。很多人大部分没法单独撰写一个微信小程序。今日给各位讲一个我很早以前做的简易的吃蛇实例。https://www.dkewl.com/course/detail6986.html
12.工业机器人的特点基本组成应用前景人工智能、机器人技术是国家战略性新兴产业,工业机器人技术只会发展得越来越好,而不会被时代所淘汰,从事工业机器人技术相关行业的人才,今后的职业寿命会非常长。 工业机器人如何操作 工业机器人怎么编程 1、在线编程 在线编程也叫示教编程,是指操作人员通过人工手动的方式利用示教器操作机器人。 https://www.cnpp.cn/focus/19982.html
13.94CTO在线学编程人人都能成为CTO94CTO在线学编程专注提供人工智能,Python,JAVA,大数据,互联网架构,C语言,PHP,区块链,前端开发,中小学编程各类课程,上94CTO在线学编程,人人都能成为CTO。https://www.94cto.com/
14.汇智网汇智网是一个学习前沿编程技术的平台,提供了mongodb,node.js,javascript,jquery等相关的课程。汇智网互动式的学习和实时在线的练习,能让你迅速进入状态,快速完成课程学习。http://www.hubwiz.com/
15.在线编程python运行菜鸟教程在线编辑器python菜鸟工具 在线编程python运行 菜鸟教程在线编辑器python 1. 基础语法 1)2.7版本执行 print 'hell\'o' 与 print "hell'o" 执行结果都为 hell'o → 单引号双引号都可以,差别在双引号内可以直接加单引号,否则需要转义; 三引号:“所见即所得” 可以由多行组成。https://blog.51cto.com/u_14555/7062386
16.w3cschool官网w3cschool启用中文品牌名--编程狮,是一个专业的W3C前端开发及编程入门学习平台,提供包括HTML,CSS,Javascript,jQuery,C,PHP,Java,Python,Sql,Mysql等编程语言和开源技术的在线教程及使用手册,是类国外w3schools的W3C学习社区及菜鸟编程平台。https://www.w3cschool.cn/
17.VIPCODE在线少儿编程Python源码编程,适合10~12岁本阶段课程让孩子掌握人工智能程序开发的基本步骤,开发初阶AI智能语音机器人和AI智能无人车。AI智能语音机器人项目以科大讯飞语音识别技术为依托,掌握人工智能语音识别的关键技术:语音处理、语音合成、关键词唤醒、声纹识别、智能对话、上下文关联等。AI智能无人车项目以百度机器视觉技术为依托http://www.soxsok.com/7394/course/36143/