-
Notifications
You must be signed in to change notification settings - Fork 1
检测并优化FlutterApp的整体性能表现
移动应用一类常见的问题是性能问题,比如滑动操作不流畅、页面出现卡顿丢帧现象等。这些问题虽然不至于让移动应用完全不可用,但也很容易引起用户反感,从而对应用质量产生质疑,甚至失去耐心。
那么,如果应用渲染并不流畅,出现了性能问题,我们该如何检测,又该从哪里着手处理呢?
在Flutter中,性能问题可以分为GPU线程问题和UI线程(CPU)问题两类。这些问题的确认都需要先通过性能图层进行初步分析,而一旦确认问题存在,接下来就需要利用Flutter提供的各类分析工具来定位问题了。
所以在今天这篇文章中,我会与你一起学习分析Flutter应用性能问题的基本思路和工具,以及常见的优化办法。
要解决问题,我们首先得了解如何去度量问题,性能分析也不例外。Flutter提供了度量性能问题的工具和手段,来帮助我们快速定位代码中的性能问题,而性能图层就是帮助我们确认问题影响范围的利器。
为了使用性能图层,我们首先需要以分析(Profile)模式启动应用。 与调试代码可以通过模拟器在调试模式下找到代码逻辑Bug不同,性能问题需要在发布模式下使用真机进行检测。
这是因为,相比发布模式而言,调试模式增加了很多额外的检查(比如断言),这些检查可能会耗费很多资源;更重要的是,调试模式使用JIT模式运行应用,代码执行效率较低。这就使得调试模式运行的应用,无法真实反映出它的性能问题。
而另一方面,模拟器使用的指令集为x86,而真机使用的指令集是ARM。这两种方式的二进制代码执行行为完全不同,因此模拟器与真机的性能差异较大:一些x86指令集擅长的操作模拟器会比真机快,而另一些操作则会比真机慢。这也使得我们无法使用模拟器来评估真机才能出现的性能问题。
为了调试性能问题,我们需要在发布模式的基础之上,为分析工具提供少量必要的应用追踪信息,这就是分析模式。除了一些调试性能问题必须的追踪方法之外,Flutter应用的分析模式和发布模式的编译和运行是类似的,只是启动参数变成了profile而已:我们既可以在Android Studio中通过菜单栏点击Run->Profile ‘main.dart’ 选项启动应用,也可以通过命令行参数flutter run --profile运行Flutter应用。
在完成了应用启动之后,接下来我们就可以利用Flutter提供的渲染问题分析工具,即性能图层(Performance Overlay),来分析渲染问题了。
性能图层会在当前应用的最上层,以Flutter引擎自绘的方式展示GPU与UI线程的执行图表,而其中每一张图表都代表当前线程最近 300帧的表现,如果UI产生了卡顿(跳帧),这些图表可以帮助我们分析并找到原因。
下图演示了性能图层的展现样式。其中,GPU线程的性能情况在上面,UI线程的情况显示在下面,蓝色垂直的线条表示已执行的正常帧,绿色的线条代表的是当前帧:
图1 性能图层
为了保持60Hz的刷新频率,GPU线程与UI线程中执行每一帧耗费的时间都应该小于16ms(1/60秒)。在这其中有一帧处理时间过长,就会导致界面卡顿,图表中就会展示出一个红色竖条。下图演示了应用出现渲染和绘制耗时的情况下,性能图层的展示样式:
图2 渲染和绘制耗时异常
如果红色竖条出现在GPU线程图表,意味着渲染的图形太复杂,导致无法快速渲染;而如果是出现在了UI线程图表,则表示Dart代码消耗了大量资源,需要优化代码执行时间。
接下来,我们就先看看GPU问题定位吧。
GPU问题主要集中在底层渲染耗时上。有时候Widget树虽然构造起来容易,但在GPU线程下的渲染却很耗时。涉及Widget裁剪、蒙层这类多视图叠加渲染,或是由于缺少缓存导致静态图像的反复绘制,都会明显拖慢GPU的渲染速度。
我们可以使用性能图层提供的两项参数,即检查多视图叠加的视图渲染开关checkerboardOffscreenLayers,和检查缓存的图像开关checkerboardRasterCacheImages,来检查这两种情况。
多视图叠加通常会用到Canvas里的savaLayer方法,这个方法在实现一些特定的效果(比如半透明)时非常有用,但由于其底层实现会在GPU渲染上涉及多图层的反复绘制,因此会带来较大的性能问题。
对于saveLayer方法使用情况的检查,我们只要在MaterialApp的初始化方法中,将checkerboardOffscreenLayers开关设置为true,分析工具就会自动帮我们检测多视图叠加的情况了:使用了saveLayer的Widget会自动显示为棋盘格式,并随着页面刷新而闪烁。
不过,saveLayer是一个较为底层的绘制方法,因此我们一般不会直接使用它,而是会通过一些功能性Widget,在涉及需要剪切或半透明蒙层的场景中间接地使用。所以一旦遇到这种情况,我们需要思考一下是否一定要这么做,能不能通过其他方式来实现呢。
比如下面的例子中,我们使用CupertinoPageScaffold与CupertinoNavigationBar实现了一个动态模糊的效果。
CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),//动态模糊导航栏
child: ListView.builder(
itemCount: 100,
//为列表创建100个不同颜色的RowItem
itemBuilder: (context, index)=>TabRowItem(
index: index,
lastItem: index == 100 - 1,
color: colorItems[index],//设置不同的颜色
colorName: colorNameItems[index],
)
)
);
图3 动态模糊效果
由于视图滚动过程中频繁涉及视图蒙层效果的更新,因此checkerboardOffscreenLayers检测图层也感受到了对GPU的渲染压力,频繁的刷新闪烁。
图4 检测saveLayer使用
如果我们没有对动态模糊效果的特殊需求,则可以使用不带模糊效果的Scaffold和白色的AppBar实现同样的产品功能,来解决这个性能问题:
Scaffold(
//使用普通的白色AppBar
appBar: AppBar(title: Text('Home', style: TextStyle(color:Colors.black),),backgroundColor: Colors.white),
body: ListView.builder(
itemCount: 100,
//为列表创建100个不同颜色的RowItem
itemBuilder: (context, index)=>TabRowItem(
index: index,
lastItem: index == 100 - 1,
color: colorItems[index],//设置不同的颜色
colorName: colorNameItems[index],
)
),
);
运行一下代码,可以看到,在去掉了动态模糊效果之后,GPU的渲染压力得到了缓解,checkerboardOffscreenLayers检测图层也不再频繁闪烁了。
图5 去掉动态模糊效果
从资源的角度看,另一类非常消耗性能的操作是,渲染图像。这是因为图像的渲染涉及I/O、GPU存储,以及不同通道的数据格式转换,因此渲染过程的构建需要消耗大量资源。为了缓解GPU的压力,Flutter提供了多层次的缓存快照,这样Widget重建时就无需重新绘制静态图像了。
与检查多视图叠加渲染的checkerboardOffscreenLayers参数类似,Flutter也提供了检查缓存图像的开关checkerboardRasterCacheImages,来检测在界面重绘时频繁闪烁的图像(即没有静态缓存)。
我们可以把需要静态缓存的图像加到RepaintBoundary中,RepaintBoundary可以确定Widget树的重绘边界,如果图像足够复杂,Flutter引擎会自动将其缓存,避免重复刷新。当然,因为缓存资源有限,如果引擎认为图像不够复杂,也可能会忽略RepaintBoundary。
如下代码展示了通过RepaintBoundary,将一个静态复合Widget加入缓存的具体用法。可以看到,RepaintBoundary在使用上与普通Widget并无区别:
RepaintBoundary(//设置静态缓存图像
child: Center(
child: Container(
color: Colors.black,
height: 10.0,
width: 10.0,
),
));
如果说GPU线程问题定位的是渲染引擎底层渲染异常,那么UI线程问题发现的则是应用的性能瓶颈。比如在视图构建时,在build方法中使用了一些复杂的运算,或是在主Isolate中进行了同步的I/O操作。这些问题,都会明显增加CPU的处理时间,拖慢应用的响应速度。
这时,我们可以使用Flutter提供的Performance工具,来记录应用的执行轨迹。Performance是一个强大的性能分析工具,能够以时间轴的方式展示CPU的调用栈和执行时间,去检查代码中可疑的方法调用。
在点击了Android Studio底部工具栏中的“Open DevTools”按钮之后,系统会自动打开Dart DevTools的网页,将顶部的tab切换到Performance后,我们就可以开始分析代码中的性能问题了。
图6 打开Performance工具
图7 Performance主界面
接下来,我们通过一个ListView中计算MD5的例子,来演示Performance的具体分析过程。
考虑到在build函数中进行渲染信息的组装是一个常见的操作,为了演示这个知识点,我们故意放大了计算MD5的耗时,循环迭代计算了1万次:
class MyHomePage extends StatelessWidget {
MyHomePage({Key key}) : super(key: key);
String generateMd5(String data) {
//MD5固定算法
var content = new Utf8Encoder().convert(data);
var digest = md5.convert(content);
return hex.encode(digest.bytes);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('demo')),
body: ListView.builder(
itemCount: 30,// 列表元素个数
itemBuilder: (context, index) {
//反复迭代计算MD5
String str = '1234567890abcdefghijklmnopqrstuvwxyz';
for(int i = 0;i<10000;i++) {
str = generateMd5(str);
}
return ListTile(title: Text("Index : $index"), subtitle: Text(str));
}// 列表项创建方法
),
);
}
}
与性能图层能够自动记录应用执行情况不同,使用Performance来分析代码执行轨迹,我们需要手动点击“Record”按钮去主动触发,在完成信息的抽样采集后,点击“Stop”按钮结束录制。这时,我们就可以得到在这期间应用的执行情况了。
Performance记录的应用执行情况叫做CPU帧图,又被称为火焰图。火焰图是基于记录代码执行结果所产生的图片,用来展示CPU的调用栈,表示的是CPU 的繁忙程度。
其中,y轴表示调用栈,其每一层都是一个函数。调用栈越深,火焰就越高,底部就是正在执行的函数,上方都是它的父函数;x轴表示单位时间,一个函数在x轴占据的宽度越宽,就表示它被采样到的次数越多,即执行时间越长。
所以,我们要检测CPU耗时问题,皆可以查看火焰图底部的哪个函数占据的宽度最大。只要有“平顶”,就表示该函数可能存在性能问题。比如,我们这个案例的火焰图如下所示:
图8 CPU帧图/火焰图
可以看到,_MyHomePage.generateMd5函数的执行时间最长,几乎占满了整个火焰图的宽,而这也与代码中存在的问题是一致的。
在找到了问题之后,我们就可以使用Isolate(或compute)将这些耗时的操作挪到并发主Isolate之外去完成了。
在Flutter中,性能分析过程可以分为GPU线程问题定位和UI线程(CPU)问题定位,而它们都需要在真机上以分析模式(Profile)启动应用,并通过性能图层分析大致的渲染问题范围。一旦确认问题存在,接下来就需要利用Flutter所提供的分析工具来定位问题原因了。
关于GPU线程渲染问题,我们可以重点检查应用中是否存在多视图叠加渲染,或是静态图像反复刷新的现象。而UI线程渲染问题,我们则是通过Performance工具记录的火焰图(CPU帧图),分析代码耗时,找出应用执行瓶颈。
通常来说,由于Flutter采用基于声明式的UI设计理念,以数据驱动渲染,并采用Widget->Element->RenderObject三层结构,屏蔽了无谓的界面刷新,能够保证绝大多数情况下我们构建的应用都是高性能的,所以在使用分析工具检测出性能问题之后,通常我们并不需要做太多的细节优化工作,只需要在改造过程中避开一些常见的坑,就可以获得优异的性能。比如:
- 控制build方法耗时,将Widget拆小,避免直接返回一个巨大的Widget,这样Widget会享有更细粒度的重建和复用;
- 尽量不要为Widget设置半透明效果,而是考虑用图片的形式代替,这样被遮挡的Widget部分区域就不需要绘制了;
- 对列表采用懒加载而不是直接一次性创建所有的子Widget,这样视图的初始化时间就减少了。