Flutter性能监控实践

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 移动端APP作为与用户交互的工具,用户体验是衡量应用优秀与否的重要指标,其中性能尤为重要。在Google推出Flutter跨平台方案后,贝壳也将Flutter接入到多个APP中. 在此之前贝壳已经在原生监控中实现了页面加载、帧率与卡顿等监控。随着Flutter在贝壳各种业务场景的使用, 随之而来的问题就是Flutter性能怎么样,用户体验如何。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 首先可以明确的是在不影响APP性能的情况下,更多的性能数据能够辅助我们改进缺陷,优化以提升APP的使用体验。因此在基于数据采集与性能损耗的可行性初步调研后,我们将Flutter监控功能聚焦在页面加载、帧率、卡顿这三个点上。接下来将从技术调研与论证、架构设计、实现、以及实践效果分别介绍。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"技术调研与论证","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"概念概述","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 通常我们说Flutter中一切皆Widget,描述的是页面模块都是用Widget来表示。Widget是页面的一个不可变的描述,也是Flutter框架的核心要素。其中Navigator(使用堆栈规则来管理一组‘页面’的Widget)通过移动小部件从一个Widget页面可视化地切换到另一个Widget页面。当我们打开一个Widget页面,实际是将Route页面添加到Navigator堆栈中,然后由Navigator调度显示栈顶的Route页面。Route页面是Widget页面的抽象描述(包含基础数据,透明,动画属性等),起到连接Widget与Navigator作用,也避免相互的耦合。下图为Flutter页面显示的时序图:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/73/739f83920f3310d587617062b02fe8fb.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"页面唯一性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 对于页面数据采集,首先要解决的是如何将多类性能数据关联到某一个具体的页面。对于原生来说, 通过页面名称就能做到:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"   iOS(ViewController):在一个可执行文件中,ViewController名称是唯一的;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"   Android(Activity、Fragment):同一个包名下Activity和Fragment类名是唯一的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是在Flutter中没有直接获取页面唯一标识方式(如","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"text":"getPackageName","attrs":{}},{"type":"text","text":"),并且Widget类名是可以重复的,也就无法精确定位页面唯一类。那么我们该如何给一个页面定义唯一标识?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"经过调研我们找到两种方式如下表:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ff/ff8ab1cbad47d2a797a72e778ae755ea.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 结合iOS瘦身的效果与编译时插入包体对比,以及未来对Dart代码瘦身混淆的考量,我们选择了后者。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"页面定义:与Route关联的页面顶级Widget作为我们的统计单元。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"页面唯一标识:使用页面所在的文件","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"importUri(","attrs":{}},{"type":"text","text":"Dart文件的唯一标识","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":") + ClassName","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"页面生命周期","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 对Flutter Widget来说, 本质上没有生命周期这一概念,因为Widget树只是不断重建的过程。Flutter Framework 提供两个大类Widget:","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"stateful","attrs":{}},{"type":"text","text":" 和 ","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"stateless","attrs":{}},{"type":"text","text":" widgets,其创建及调用时机如下图:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/88/88d017817f03dd57e36ec3950065c0ea.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3f/3f0ffdf855b7441f21f34cf67686d6d6.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 我们发现,对比StatelessWidget 和 StatefulWidget可以看出两者存在的差异。StatelessWidget没有可变状态,没有合适的监控点。而如果在StatefulWidget的build、createState、didUpdateWidget或didChangeDependencies等中打点, 会因widget树状态重建频繁导致打点过多,对页面本身性能产生影响。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 那么页面首帧与页面绘制完成应该在什么时机获取呢?结合页面加载过程(","attrs":{}},{"type":"link","attrs":{"href":"https://flutter.dev/docs/resources/architectural-overview#building-widgets","title":"","type":null},"content":[{"type":"text","text":"architectural-overview - building-widgets","attrs":{}}]},{"type":"text","text":")分析,我们最终通过Navigator对Route的管理作为页面生命周期监控hook点。对于页面首帧我们采用在Route buildPage后增加一个PostFrameCallback。而对于页面加载完成,我们给TransitionRoute 的AnimationController设置statusListener, 并监听AnimationStatus.completed状态来确定动画结束时机,以此确定页面加载完成的时间点。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"帧率与卡顿","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"帧率:通常指每秒绘制的帧数(frames per second)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"丢帧:因系统负载导致帧率过低所造成的画面出现停滞现象,也叫跳帧或者掉帧。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"卡顿:一般来说指丢帧的另一个概念,指画面出现停滞现象比丢帧更明显。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Flutter官方有提供一套基于 ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"SchedulerBinding.addTimingsCallback","attrs":{}},{"type":"text","text":" 回调实现的帧率方案。从源码中可以看出,当flutter页面有视图绘制刷新时, 系统吐出一串FrameTiming数据 (与Android dumpsys gfxinfo中的frameStats类似)。并且其对性能的影响也可以忽略不计(官方数据iPhone6s:对60fps的设备每帧增加 0.1ms 的负载,每秒CPU占用0.01%)","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}}],"text":"Flutter spends less than 0.1ms every 1 second to report the timings (measured on iPhone6S). The 0.1ms is about 0.6% of 16ms (frame budget for 60fps), or 0.01% CPU usage per second.","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 我们可以直接使用这种方式获取监控所要采集的FPS数据源。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然有了帧率数据源,我们如何用数据衡量页面性能?如何为业务开发同学提供一个客观的指标来评判性能,以及如何验证优化效果?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 通常来说FPS是衡量页面流畅度的指标,如何计算FPS得出大家都认可的参考标准呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在经过调研与实践后,我们为卡顿阈值找到一个具有说服力的衡量指标。列举如下:","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"卡顿阈值的选取","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 首先我们认为卡顿本身是一个很主观的东西,就好像有人觉得打王者荣耀玩流畅模式(30FPS)好像也还算流畅,有人会觉得不开高帧率(60FPS)就没法玩。那有没有什么较为客观一点的标准?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在网上查阅了查阅了大量研究资料后,我们找到了一篇发表在ICIP上的论文 ","attrs":{}},{"type":"link","attrs":{"href":"https://www.researchgate.net/profile/Zhan-Ma-6/publication/224359119_Modeling_the_impact_of_frame_rate_on_perceptual_quality_of_video/links/00b7d514f3b347250e000000/Modeling-the-impact-of-frame-rate-on-perceptual-quality-of-video.pdf","title":null,"type":null},"content":[{"type":"text","marks":[{"type":"underline","attrs":{}}],"text":"Modeling the impact of frame rate on perceptual quality of video","attrs":{}}]},{"type":"text","text":"他们选择了6类视频,在不同帧率下进行了测试,实验结果如下图所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/58/5880242a40c4a67c19d48870aae70fd7.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 该图反映了帧率和人眼主观感受之间的关系。6 个测试序列分别使用6张图表示。每张图x座标代表帧率,y座标代表人眼主观感受(MOS),红色虚线代表CIF(352×288)分辨率序列的拟合曲线,蓝色虚线代表QCIF(176×144)分辨率序列的拟合曲线。主观感受取值范围 0-100,数值越大代表主观感受越好。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"据此结果,实验设计模型通过标准因子将六种场景的得分标准化,并绘制到一个座标下","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/49/49e217a822d90bbe2773230cd32593c8.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 从该图可以看出,当帧率大于15 帧的时候,人眼的主观感受差别不大,基本上都处于较高的水平。而帧率小于15帧以后,人眼的主观感受会急剧下降,人眼会立刻感受到画面的不连贯性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要达到15FPS,单帧耗时不能超过 16.7ms * 4。最终,我们选择了","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":" 16.7ms * 4 (60Hz设备)","attrs":{}},{"type":"text","text":"作为Flutter页面卡顿的阈值。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"架构设计","attrs":{}}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下图列出前端、后端、原生、Flutter的侧重点:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e3/e311d28e792520fb1a89732066aca494.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 后端的能力在数据处理这块,原生APM监控体系中也比较成熟。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 前端页面展示主要包含如下三个方面:","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 页面通用功能","attrs":{}},{"type":"text","text":":版本的数据, 版本维度数据对比","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 页面加载功能包括:","attrs":{}},{"type":"text","text":"访问数,首次渲染时间,二次渲染时间,页面生命周期","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 页面帧率功能包括:","attrs":{}},{"type":"text","text":"FPS平均值,平均卡顿次数。(FPS最差值,丢帧平均值,丢帧峰值,这三个值作为参考数据来统计)","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" FlutterPlugin在客户端主要聚焦在如下方面:","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 页面唯一性标识获取","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 页面生命周期监控点","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 生命周期与Platform映射规则","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 页面加载采集方案实现","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 页面加载本地计算与统计","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 帧率数据源如何采集","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 帧率如何计算","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 卡顿标准如何确定","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l 上传模块的实现","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"监控SDK实现","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"概览图","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c2/c2bbe127792d84269efbca619211f5cc.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Hook时机点如上图(后面详细介绍),由MonitorService分发事件:","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l pageChange: ","attrs":{}},{"type":"text","text":"页面的起始点可以考虑在Navigator.push调用,但在1.22容器打开的首个页面并不会触发push, 我们在新版本将hook点放到Route install函数中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l buildPage: ","attrs":{}},{"type":"text","text":"页面构建时间耗时= buildPageEnd - buildPageStart","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l firstFrame: ","attrs":{}},{"type":"text","text":"页面第一帧绘制完成时机 (Route buildPageEnd + postFrameCallback)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"user"}}],"text":"l animatorEnd: ","attrs":{}},{"type":"text","text":"页面绘制并且动画完成时间点 (Animation Completed + postFrameCallback)","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"编译时Hook能力(AOP)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 借助","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"beike_aspectd (","attrs":{}},{"type":"link","attrs":{"href":"https://mp.weixin.qq.com/s?__biz=MzIyMTg0OTExOQ%3D%3D&mid=2247486207&idx=2&sn=520474e581fce9df1523e963c42ea709&chksm=e837398fdf40b0998190de240ae81d02b8731ae7bbc157ec3bab98345ea9fae250091deac11c&mpshare=1&scene=1&srcid=1103aTWnR7vfJqJH4hdDUlz5&sharer_sharetime=1635930132689&sharer_shareid=297c951c4cf4d173a55f2f115fabd97a#rd","title":"","type":null},"content":[{"type":"text","text":"文章链接","attrs":{}}]},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":")","attrs":{}},{"type":"text","text":"编译时代码注入能力,将监控库的函数编译时注入到app.dill中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"涉及的内容如下:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"navigator hook点: push和pop的时机","attrs":{}}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/// lib/src/widgets/navigator.dart\nFuture push(Route route) {}\nvoid pop([ T? result ]) {}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在Navigator 2.0 后命令式API变更为声明式API,Navigator initState中push逻辑被移除掉了,转由initialRoute的创建route add触发,因此我们将Page Change的时机调整到Route install()方法中。即TransitionRoute:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/// lib/src/widgets/routes.dart\n/// abstract class TransitionRoute...\nvoid install() {//增加 }","attrs":{}}]},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"buildPage点: 获取Widget的类名,页面唯一性的一部分","attrs":{}}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/// lib/src/material/page.dart\n/// lib/src/cupertino/route.dart\n@override\nWidget buildPage(BuildContext context,Animation animation,\nAnimation secondaryAnimation,\n) {}\n\n@Inject(\"package:flutter/src/material/page.dart\", \"MaterialPageRoute\",\n \"-buildPage\",\n lineNum: 87)\n@pragma(\"vm:entry-point\")\nvoid routeBeforePage() {\n //...\n}\n\n@Inject(\"package:flutter/src/material/page.dart\", \"MaterialPageRoute\",\n \"-buildPage\",\n lineNum: 97)\n@pragma(\"vm:entry-point\")\nvoid routeAfterPage() {\n //...\n}","attrs":{}}]},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"动画结束时机","attrs":{}}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":" @Inject(\"package:flutter/src/widgets/pages.dart\", \"PageRoute\",\n \"-createAnimationController\",\n lineNum: 41)\n @pragma(\"vm:entry-point\")\n void createAnimationController() {\n Object controller; //Aspectd Ignore\n AnimationController animationController = controller;\n // 这里要注意1.12.13和2.x版本差异\n animationController.addStatusListener((state) {\n if (state == AnimationStatus.completed) {\n WidgetsBinding.instance.addPostFrameCallback((duration) {\n Logger.devLog('AnimatorEnd结束时间点');\n // ...\n });\n }\n });\n }","attrs":{}}]},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"Widget注入方法获取importUri","attrs":{}}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/// @Add是beike_aspectd提供的编译时注解\n@Add(\"package:.+\\\\.dart\", \".*\", isRegex: true, superCls: 'Widget')\n@pragma(\"vm:entry-point\")\ndynamic importUri(PointCut pointCut) {\n// 获取 importUri\n \treturn pointCut.sourceInfos[\"importUri\"];\n}","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"页面加载","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 页面加载主要是在对Route加载显示页面的流程。 主要采集内容如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/80/80e3f1ed3cf1db41d8ef7c279b627152.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"帧率与卡顿","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 因为帧率的数据源是动态数据,所以用单帧时间换算FPS的计算原则来统计。以单帧的绘制效率(结合vsync信号时间)评估1秒能够绘制的帧数。我们称之为: ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"单帧FPS","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":" 。","attrs":{}},{"type":"text","text":"以下以60Hz设备举例说明其计算:","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"color","attrs":{"color":"#40A9FF","name":"user"}},{"type":"strong","attrs":{}}],"text":"[单帧FPS] = 1000 / Math.max(单帧时间, 16.7 * Math.ceil(单帧时间 / 16.7))","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主要采集内容如下: ","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6c/6c88b18d6f2fbcd55422e5969b5248ff.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 综上所诉, 我们对一个页面从打开到退出的关键生命周期进行hook,计算对应的首帧耗时、平均 Fps、卡顿次数等数据。在页面退出后,获取到页面的唯一标识(包名+类名),以及对应的性能数据,并将其上传到远端.","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"实践效果","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"线下实时FPS展示面板","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在profile/debug 模式下,FPS展示面板可以直观的评估页面流畅度。可以查看当前设备最近100(可配置)帧的表现情况:(如下图)","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7f/7f768742b6f3a4fa6680046be4b2f565.gif","alt":null,"title":"","style":[{"key":"width","value":"25%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 除了实时的FPS 查看,我们还将性能监控库中的数据进行了本地展示(下图)。以此掌握当前页面在不同设备上的性能表现,进行更精确的优化。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d0/d0f004121981782418d6ca9cec9941ff.gif","alt":null,"title":"","style":[{"key":"width","value":"25%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"线上数据采集","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 下图为线上采集的数据,结合线下FPS工具里采集的数据可以帮助业务方更快看到优化效果。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f4/f48028bcac4595f812b26ba10ee5007c.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/da/da10a9bf9c51a3f98813190018b6e95d.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 实际端上还采集了页面丢帧数据,但没有显示在网页上,因为我们认为这并不能很好的衡量实际使用过程中渲染性能。从目前资料来看影响的因素有2点:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":1,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"帧率不能过低, 并且保持稳定:如持续低于30fps时,动画连贯性受到影响.","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":1,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"帧率稳定: 如fps是60,20,60,30,... 等不均匀速率, 容易产生的视觉上的卡顿.","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于FPS计算,目前腾讯","attrs":{}},{"type":"link","attrs":{"href":"https://perfdog.qq.com/","title":"","type":null},"content":[{"type":"text","text":"PrefDog","attrs":{}}]},{"type":"text","text":"比较高得影响力,其中说到衡量FPS的要点: ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1) Avg(FPS):平均帧率(一段时间内平均FPS)   ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2) Var(FPS):帧率方差(一段时间内FPS方差)  ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3) Drop(FPS):降帧次数(平均每小时相邻两个FPS点下降大于8帧的次数)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以单从FPS平均值数据的说服力不足以佐证上述第一点内容,因此我们将FPS平均值、FPS最差值作为参考值呈现在网页上,待后续完善。目前我们在Flutter帧率上主要采用卡顿指标(即上面的第二个因素帧率是否稳定)来评估页面渲染性能。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"总结","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 本文主要介绍贝壳早期在Flutter 1.12.13 (1.22、2.0已适配)性能监控实践过程中的一些思路和实现的方式。APP监控需要深入挖掘运行机制,更深层次的机制原理之后的文章会详细描述(如详细的流程,渲染原理等)。此外Flutter版本迭代很频繁,一些监控时机很可能在下一个稳定版就不适用,这就需要开发者去找到更合理的点。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 最后,页面加载、页面帧率、页面卡顿等性能数据帮助了我们优化提升了APP的使用体验。如何更精准获取数据、更合理的处理数据, 并驱动改进提升APP的性能也是我们的终极目标。就像","attrs":{}},{"type":"link","attrs":{"href":"https://mp.weixin.qq.com/s?__biz=MzIyMTg0OTExOQ==&mid=2247487737&idx=1&sn=47a1b8196d6d8db5e73a896812e06201&chksm=e8372389df40aa9f5984bcd1f194d3565867ed4fc1f45ddbd9b8ea74256f5f0650dc4052988d&mpshare=1&scene=1&srcid=1108WIiQjOJUX2UWKwfLV6r5&sharer_sharetime=1636355906002&sharer_shareid=297c951c4cf4d173a55f2f115fabd97a&version=3.1.19.90358&platform=mac#rd","title":"","type":null},"content":[{"type":"text","text":"KeFrame流畅度优化组件","attrs":{}}]},{"type":"text","text":"一样能帮助解决实际卡顿问题。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章