概述
软件项目的交付是一个复杂且漫长的过程,任何细小的失误都有可能导致交付过程失败。在软件开发过程中,除了代码逻辑的 Bug 和视觉异常这些功能层面的问题之外,移动应用另一类常见的问题是性能问题,比如滑动操作不流畅、页面出现卡顿丢帧现象等。这些问题虽然不至于让移动应用完全不可用,但也很容易引起用户反感,从而对应用质量产生质疑,甚至失去耐心。
那么,对于应用渲染并不流畅,出现了性能问题,我们该如何检测,又该从哪里着手处理呢?和移动开发类似, Flutter 的性能问题主要可以分为 GPU 线程问题和 UI 线程(CPU)问题两类。对于这些问题,有一个通用的套路:首先,都需要先通过性能图层进行初步分析,而一旦确认问题存在,接下来就是利用 Flutter 提供的各类分析工具来进行问题定位。
图层分析
Flutter运行模式
1、Debug
Debug模式可以在真机和模拟器上同时运行,此模式会打开所有的断言,包括debugging信息、debugger aids(比如observatory)和服务扩展。优化了快速develop/run循环,但是没有优化执行速度、二进制大小和部署。命令flutter run就是以这种模式运行的,通过sky/tools/gn --android
或者sky/tools/gn --ios
来构建应用的。
2、Release
Release模式只能在真机上运行,不能在模拟器上运行:会关闭所有断言和debugging信息,关闭所有debugger工具。优化了快速启动、快速执行和减小包体积。禁用所有的debugging aids和服务扩展。这个模式是为了部署给最终的用户使用。命令flutter run --release
就是以这种模式运行的,通过sky/tools/gn --android --runtime-mode=release
或者sky/tools/gn --ios --runtime-mode=release
来构建应用。
3、Profile
Profile模式只能在真机上运行,不能在模拟器上运行,基本和Release模式一致,除了启用了服务扩展和tracing,以及一些为了最低限度支持tracing运行的东西(比如可以连接observatory到进程)。命令flutter run --profile
就是以这种模式运行的,通过sky/tools/gn --android --runtime-mode=profile
或者sky/tools/gn --ios --runtime-mode=profile
来构建应用。
4、test
headless test模式只能在桌面上运行,基本和Debug模式一致,除了是headless的而且你能在桌面运行。命令flutter test
就是以这种模式运行的,通过sky/tools/gn
来build。
在实际开发中,应该用到上面所说的四种模式又各自分为两种:一种是未优化的模式,供开发人员调试使用;一种是优化过的模式,供最终的开发人员使用。默认情况下是未优化模式,如果要开启优化模式,build的时候在命令行后面添加--unoptimized参数。
不管是移动开发还是前端开发,对于性能问题分析的思路都是先分析并定位问题,Flutter也不例外,借助Flutter 提供的度量性能工具,我们可以快速定位代码中的性能问题,而性能图层就是帮助我们确认问题影响范围的利器,它类似Android的图层分析工具。
为了使用性能图层,Flutter提供了分析(Profile)模式,与调试代码可以通过模拟器在调试模式下找到代码逻辑 Bug 不同,性能问题需要在发布模式下使用真机进行检测。相比发布(Release)模式而言,调试模式增加了很多额外的检查(比如断言),这些检查可能会耗费很多资源;更重要的是,调试模式使用 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 线程的情况显示在下面,蓝色垂直的线条表示已执行的正常帧,绿色的线条代表的是当前帧。
同时,为了保持 60Hz 的刷新频率,GPU 线程与 UI 线程中执行每一帧耗费的时间都应该小于 16ms(1/60 秒)。在这其中有一帧处理时间过长,就会导致界面卡顿,图表中就会展示出一个红色竖条,如下图所示。
如果红色竖条出现在 GPU 线程图表,意味着渲染的图形太复杂,导致无法快速渲染;而如果是出现在了 UI 线程图表,则表示 Dart 代码消耗了大量资源,需要优化代码执行时间。
GPU问题定位
GPU渲染问题主要集中在底层渲染耗时上,有时候 Widget 树虽然构造起来容易,但在 GPU 线程下的渲染却很耗时。例如,涉及 Widget 裁剪、蒙层这类多视图叠加渲染,或是由于缺少缓存导致静态图像的反复绘制,都会明显拖慢 GPU 的渲染速度。
接下来,使用性能图层提供的两项参数,即检查多视图叠加的视图渲染开关 checkerboardOffscreenLayers和检查缓存的图像开关checkerboardRasterCacheImages来检查这两种情况。
checkerboardOffscreenLayers
多视图叠加通常会用到 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],
)
)
);
其中,动态模糊的NavigationBar效果如下图所示。
当我们开启checkerboardOffscreenLayers之后,可以看到视图蒙层效果对GPU的渲染压力导致性能视图频繁闪动。如果我们没有对动态模糊效果有特殊需求,则可以使用不带模糊效果的 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 检测图层也不再频繁闪烁了。
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 的调用栈和执行时间,去检查代码中可疑的方法调用。
打开 Android Studio 底部工具栏中的“Open DevTools”按钮之后,系统会自动打开 Dart DevTools 的网页,将顶部的 tab 切换到 Performance 后,我们就可以开始分析代码中的性能问题了。
接下来,我们通过一个在 ListView 中计算 MD5 的例子来演示 Performance 的具体分析过程。考虑到在 build 函数中进行渲染信息的组装是一个常见的操作,为了演示Performance的使用过程,我们故意放大计算 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 的繁忙程度。所以,我们要检测 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,这样视图的初始化时间就减少了。