当前位置: 首页 > 科技观察

Flutter应用性能检测与优化

时间:2023-03-14 18:52:29 科技观察

概述软件项目的交付是一个复杂而漫长的过程,任何一个小的失误都可能导致交付过程的失败。在软件开发过程中,除了代码逻辑Bug、视觉异常等功能性问题外,移动应用中另一个普遍存在的问题就是性能问题,如滑动操作不流畅、页面卡顿、丢帧等。这些问题虽然不会让手机应用完全无法使用,但很容易引起用户的反感,质疑应用的质量,甚至失去耐心。那么,如果应用渲染不流畅,出现性能问题,我们应该如何检测,从哪里入手处理呢?与移动开发类似,Flutter的性能问题主要可以分为GPU线程问题和UI线程(CPU)问题。对于这些问题,有一个大致的套路:首先需要通过性能层进行初步分析,确定问题后,接下来就是使用Flutter提供的各种分析工具来定位问题。层层分析Flutter运行模式1.DebugDebug模式既可以在真机上运行,??也可以在模拟器上运行。此模式将打开所有断言,包括调试信息、调试器辅助工具(例如观察台)和服务扩展。针对快速开发/运行循环进行了优化,但未针对执行速度、二进制文件大小和部署进行优化。flutterrun命令就是在这种模式下运行的,通过sky/tools/gn--android或者sky/tools/gn--ios构建应用。2.ReleaseRelease模式只能在真机上运行,??不能在模拟器上运行:会关闭所有断言和调试信息,关闭所有调试器工具。针对快速启动、快速执行和减小包大小进行了优化。禁用所有调试辅助工具和服务扩展。此模式旨在部署到最终用户。命令flutterrun--release在此模式下运行,通过sky/tools/gn--android--runtime-mode=release或sky/tools/gn--ios--runtime-mode=release构建应用程序。3.ProfileProfile模式只能在真机上运行,??不能在模拟器上运行。它与Release模式基本相同,只是启用了服务扩展和跟踪,以及一些至少支持跟踪的东西(例如将可观察对象连接到进程)。命令flutterrun--profile运行在该模式下,通过sky/tools/gn--android--runtime-mode=profile或sky/tools/gn--ios--runtime-mode=profile构建应用。4、testheadless测试模式只能在桌面上运行,和Debug模式基本一样,只是它是headless,可以在桌面上运行。命令fluttertest运行在这种模式下,通过sky/tools/gn构建。在实际开发中,应该使用上面提到的四种模式,分为两种:一种是未优化模式,供开发者调试使用;另一种是供最终开发者使用的优化模式。默认情况下,它是未优化模式。如果要开启优化模式,构建时在命令行后面加上--unoptimized参数即可。无论是移动开发还是前端开发,分析性能问题的思路都是先分析定位问题,Flutter也不例外。借助Flutter提供的性能测量工具,我们可以快速定位代码中的性能问题,而性能层是帮助我们确认问题范围的有力工具,它类似于Android的层分析工具。为了使用性能层,Flutter提供了分析(Profile)模式。不同于在debug模式下通过模拟器调试代码可以发现代码逻辑bug,release模式下需要使用真机检测性能问题。与发布(Release)模式相比,调试模式增加了很多额外的检查(比如断言),可能会消耗很多资源;更重要的是,调试模式使用JIT(即时编译)模式来运行应用程序,代码执行效率较低。这使得运行在调试模式下的应用程序无法真实反映其性能问题。另一方面,模拟器使用的指令集是x86,而真机使用的指令集是ARM。由于这两种方式的二进制代码执行行为完全不同,所以模拟器和真机的性能差异还是比较大的。x86指令集擅长的一些操作会比真机快,而另一些操作会比真机慢,这也使得我们无法使用模拟器来评估只有在真机上才会出现的性能问题.为了调试性能问题,我们需要在发布模式的基础上,为分析工具提供少量必要的应用跟踪信息,也就是分析模式。Flutter应用的分析模式和发布模式除了一些调试性能问题所必需的跟踪方式外,编译运行类似,只是启动参数变成了profile。我们可以点击菜单栏中的【运行】-【配置文件】'main.dart'选项,在AndroidStudio中启动应用,或者通过命令行参数flutterrun--profile来运行Flutter应用。渲染问题分析在应用启动完成后,我们可以使用Flutter提供的渲染问题分析工具,即性能层(PerformanceOverlay)来分析渲染问题。性能层会在当前应用的顶层,以Flutter引擎自己绘制的方式显示GPU和UI线程的执行图表,每张图表代表当前线程在最近300帧的性能.如果UI卡顿(Frameskipping),这些图表可以帮助我们分析并找到原因,如下图所示。上图演示了性能层的展现方式。其中GPU线程的表现在最上面,UI线程的情况显示在最下面。蓝色竖线代表已经执行的正常帧,绿线代表当前帧。同时,为了保持60Hz的刷新率,GPU线程和UI线程中每一帧的执行时间要小于16ms(1/60秒)。其中,一帧的处理时间过长,会导致界面卡顿,图表中会出现红色竖条,如下图。如果GPU线程图表上出现红色竖条,说明渲染的图形过于复杂,无法快速渲染;如果出现在UI线程图表上,说明Dart代码消耗资源较多,需要优化代码执行时间。GPU问题定位GPU渲染问题主要集中在耗时的底层渲染上。有时候widget树很容易构建,但是在GPU线程下渲染就很耗时了。例如,由于缺少缓存而涉及小部件剪辑、屏蔽或重复绘制静态图像的多视图叠加渲染会显着降低GPU渲染速度。接下来,使用性能层提供的两个参数,用于检查多视图叠加的视图渲染开关checkerboardOffscreenLayers和用于检查缓存图像的checkerboardRasterCacheImages开关来检查这两种情况。checkerboardOffscreenLayers多视图叠加通常使用Canvas中的savaLayer方法,在实现一些特定效果(比如半透明)时非常有用,但是由于其底层实现涉及到在GPU渲染上重复绘制多个图层,所以会造成较大的性能问题。查看saveLayer方法的使用,我们只需要在MaterialApp的初始化方法中将checkerboardOffscreenLayers开关设置为true,分析工具就会自动为我们检测多视图叠加。使用saveLayer的小部件会自动以棋盘格格式显示,并在页面刷新时闪烁。但是saveLayer是一个比较底层的绘制方法,所以我们一般不会直接使用,而是在需要裁剪或者半透明遮罩的场景中,通过一些功能Widget来间接使用。那么一旦遇到这种情况,我们就需要思考是否一定要这样做,是否可以通过其他方式来实现?比如下面的例子,我们使用CupertinoPageScaffold和CupertinoNavigationBar实现动态模糊效果,代码如下:CupertinoPageScaffold(navigationBar:CupertinoNavigationBar(),//动态模糊导航栏child:ListView.builder(itemCount:100,//是一个列表创建100个不同颜色的RowItemitemBuilder:(context,index)=>TabRowItem(index:index,lastItem:index==100-1,color:colorItems[index],//设置不同的颜色colorName:colorNameItems[指数],)));其中模糊的NavigationBar效果如下图所示。当我们启用checkerboardOffscreenLayers时,可以看到viewmask效果对GPU的渲染压力导致performanceview频繁闪烁。如果我们对运动模糊效果没有特殊要求,可以使用不带模糊效果的Scaffold和白色的AppBar实现同样的产品功能来解决这个性能问题。Scaffold(//使用纯白色AppBarappBar:AppBar(title:Text('Home',style:TextStyle(color:Colors.black),),backgroundColor:Colors.white),body:ListView.builder(itemCount:100,//为列表创建100个不同颜色的RowItemitemBuilder:(context,index)=>TabRowItem(index:index,lastItem:index==100-1,color:colorItems[index],//设置不同的颜色colorName:colorNameItems[index],)));运行代码,可以看到去除模糊效果后,GPU的渲染压力得到缓解,checkerboardOffscreenLayers检测层不再频繁闪烁。checkerboardRasterCacheImages从资源的角度来看,还有一类非常耗性能的操作就是渲染图像,因为图像渲染涉及I/O、GPU存储、不同通道的数据格式转换,所以构建渲染过程会消耗大量资源.为了减轻GPU的压力,Flutter提供了多级缓存快照,这样重建Widget时就不需要重新绘制静态图像了。与检查多视图叠加渲染的checkerboardOffscreenLayers参数类似,Flutter提供了检查缓存图像的开关checkerboardRasterCacheImages来检测界面重绘时频繁闪烁的图像。为了提高静态图片的显示性能,我们可以将需要静态缓存的图片添加到RepaintBoundary中。RepaintBoundary可以确定Widget树的重绘边界。如果图片足够复杂,Flutter引擎会自动缓存,避免重复刷新。当然,由于缓存资源有限,如果引擎认为图像不够复杂,RepaintBoundary也可能会被忽略。下面代码展示了通过RepaintBoundary添加一个静态复合Widget到缓存的具体用法,如下图。RepaintBoundary(//设置静态缓存图片child:Center(child:Container(color:Colors.black,height:10.0,width:10.0,),));UI线程问题定位如果GPU线程问题定位在底层渲染引擎渲染异常,那么发现的UI线程问题就是应用的性能瓶颈。比如在构建视图时,在build方法中使用了一些复杂的操作,或者在主Isolate中进行了同步I/O操作。这些问题会显着增加CPU的处理时间,减慢应用程序的响应速度。对于这类问题,我们可以使用Flutter提供的Performance工具来记录应用程序的执行轨迹。Performance是一个强大的性能分析工具,可以将CPU的调用栈和执行时间以时间轴的形式显示出来,以检查代码中可疑的方法调用。打开AndroidStudio底部工具栏中的“打开DevTools”按钮后,系统会自动打开DartDevTools网页。将顶部选项卡切换到Performance后,我们就可以开始分析代码中的性能问题了。下面我们通过一个在ListView中计算MD5的例子来演示Performance的具体分析过程。考虑到build函数中渲染信息的组装是一个常用的操作,为了演示Performance的使用,我们特意放大了MD5的耗时计算,比如10000次循环迭代。classMyHomePageextendsStatelessWidget{MyHomePage({Keykey}):super(key:key);StringgenerateMd5(Stringdata){//MD5固定算法varcontent=newUtf8Encoder().convert(data);vardigest=md5.convert(content);returnhex.encode(digest.bytes);}@overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(title:Text('demo')),body:ListView.builder(itemCount:30,//列表元素个数itemBuilder:(context,index){//迭代计算MD5Stringstr='1234567890abcdefghijklmnopqrstuvwxyz';for(inti=0;i<10000;i++){str=generateMd5(str);}returnListTile(title:Text("Index:$index"),subtitle:Text(str));}//Listitem的创建方法),);}}不同于性能层,可以自动记录应用程序的执行情况。使用Performance分析代码执行轨迹,需要手动点击【记录】按钮主动触发,完成信息的采样和采集后,点击【停止】按钮结束记录,即可得到在此期间应用程序的执行情况。Performance记录的应用程序执行情况称为CPU帧图,也称为火焰图。火焰图是基于记录代码执行结果生成的图像。用于显示CPU的调用栈,表示CPU的繁忙程度。因此,如果我们要检测CPU耗时问题,可以查看火焰图底部哪个函数占用的宽度最大。只要出现“平顶”,就说明该函数可能存在性能问题,如下图所示。可以看出_MyHomePage.generateMd5函数的执行时间最长,几乎占满了火焰图的整个宽度,这也与代码中的问题一致。找到问题后,我们可以使用Isolate(或者compute)将这些耗时的操作移出并发的mainIsolate来完成。综上所述,在Flutter中,性能分析过程分为GPU线程问题定位和UI线程(CPU)问题定位,都需要在真机上以分析模式(Profile)启动应用,进行分析通过性能层范围的一般渲染问题。确认问题后,需要使用Flutter提供的分析工具来定位问题的原因。关于GPU线程渲染的问题,我们可以重点检查应用中是否存在多视图叠加渲染,或者静态图片重复刷新的现象。对于UI线程渲染问题,我们通过Performance工具记录的火焰图(CPU帧图)分析代码耗时,找出应用执行的瓶颈。总的来说,由于Flutter采用了声明式的UI设计理念,数据驱动渲染,Widget->Element->RenderObject的三层结构,屏蔽了不必要的界面刷新,所以可以保证我们构建的大部分情况所有的应用都是高性能的,所以在使用分析工具检测出性能问题后,通常我们不需要做太多细节优化工作,只需要在改造过程中避免一些常见的陷阱,就可以获得优秀的性能。同时,为了避免出现性能问题,还应该从以下几个方面入手:控制构建方法的耗时,拆解Widget,避免直接返回一个巨大的Widget,这样Widget才能享受到更好的体验-粒度重建和重用;不给Widget设置半透明效果,可以考虑用图片代替,这样Widget的一些被遮挡的区域就不需要绘制了;对列表使用延迟加载,而不是直接一次创建所有子Widget,这样就减少了视图的初始化时间。