Flutter性能分析:GPU与UI线程问题定位与优化

Viewed 0

在上一篇文章中,我们分享了调试Flutter代码的三种基本方式:输出日志、断点调试与布局调试。通过可定制打印行为的debugPrint函数,可以实现生产环境与开发环境不同的日志输出行为;借助IDE提供的断点调试选项,可以调整代码执行步长和暂停条件;而通过Debug Painting和Flutter Inspector,可以定位视觉布局问题。

除了代码逻辑和视觉异常,移动应用常见的另一类问题是性能问题,如滑动不流畅、页面卡顿丢帧等。这些问题容易引起用户反感,影响应用质量。那么,当应用出现性能问题时,我们该如何检测和处理呢?

在Flutter中,性能问题主要分为GPU线程问题和UI线程(CPU)问题。确认这些问题需要先通过性能图层进行初步分析,然后利用Flutter提供的工具定位问题。本文将介绍分析Flutter应用性能问题的基本思路、工具和常见优化方法。

如何使用性能图层?

要解决性能问题,首先需要度量问题。Flutter提供了性能图层作为工具,帮助确认问题影响范围。为了使用性能图层,需要以分析模式启动应用。与调试模式不同,性能问题需要在发布模式下使用真机检测。调试模式增加了额外检查且使用JIT模式,代码执行效率较低,无法真实反映性能问题;模拟器与真机指令集不同,性能差异较大,因此必须使用真机。

分析模式在发布模式基础上提供少量必要的追踪信息,编译和运行类似发布模式,启动参数为profile。可以通过Android Studio菜单栏点击Run->Profile ‘main.dart’启动,或通过命令行flutter run --profile运行。

分析渲染问题

启动应用后,可以利用性能图层分析渲染问题。性能图层在当前应用最上层以Flutter引擎自绘方式展示GPU与UI线程的执行图表,每张图表代表线程最近300帧的表现。如果UI卡顿,图表帮助分析原因。

性能图层中,GPU线程情况在上面,UI线程在下面,蓝色垂直线条表示已执行的正常帧,绿色线条代表当前帧。为保持60Hz刷新频率,GPU线程与UI线程每帧耗时都应小于16ms。如果有一帧处理时间过长导致卡顿,图表中会展示红色竖条。

如果红色竖条出现在GPU线程图表,意味着图形渲染太复杂;如果出现在UI线程图表,表示Dart代码消耗资源过多,需要优化代码执行时间。

GPU问题定位

GPU问题主要集中在底层渲染耗时上。Widget树构造容易但渲染耗时,涉及Widget裁剪、蒙层等多视图叠加渲染,或缺少缓存导致静态图像反复绘制,都会拖慢GPU渲染速度。可以使用性能图层提供的两项参数检查:checkerboardOffscreenLayers用于检查多视图叠加,checkerboardRasterCacheImages用于检查缓存图像。

checkerboardOffscreenLayers

多视图叠加通常使用Canvas的saveLayer方法,实现半透明等效果时有用,但底层涉及多图层反复绘制,带来较大性能问题。检查saveLayer使用情况,只需在MaterialApp初始化中将checkerboardOffscreenLayers设置为true,分析工具自动检测:使用了saveLayer的Widget显示为棋盘格式并闪烁。

saveLayer是底层绘制方法,一般间接通过功能性Widget使用,如涉及剪切或半透明蒙层场景。遇到这种情况,需考虑是否必要或通过其他方式实现。例如,使用CupertinoPageScaffold与CupertinoNavigationBar实现动态模糊效果,视图滚动中频繁更新蒙层效果,checkerboardOffscreenLayers检测图层频繁闪烁,显示GPU渲染压力。如果没有特殊需求,可以使用不带模糊效果的Scaffold和白色AppBar替代,缓解GPU压力。

checkerboardRasterCacheImages

另一类消耗性能的操作是渲染图像,涉及I/O、GPU存储和数据格式转换。为缓解压力,Flutter提供多层次缓存快照,Widget重建时无需重绘静态图像。检查缓存图像的开关checkerboardRasterCacheImages检测界面重绘时频繁闪烁的图像(即没有静态缓存)。

可以将需要静态缓存的图像加到RepaintBoundary中,RepaintBoundary确定Widget树重绘边界,如果图像足够复杂,Flutter引擎自动缓存,避免重复刷新。缓存资源有限,如果引擎认为图像不够复杂,可能忽略RepaintBoundary。使用RepaintBoundary与普通Widget类似,例如:

RepaintBoundary(
  child: Center(
    child: Container(
      color: Colors.black,
      height: 10.0,
      width: 10.0,
    ),
  ),
);

UI线程问题定位

UI线程问题发现应用性能瓶颈,如在build方法中使用复杂运算,或在主Isolate中进行同步I/O操作,增加CPU处理时间,拖慢应用响应速度。可以使用Flutter的Performance工具记录应用执行轨迹,以时间轴方式展示CPU调用栈和执行时间,检查可疑方法调用。

点击Android Studio底部工具栏的“Open DevTools”按钮,打开Dart DevTools网页,切换到Performance标签页,开始分析性能问题。

通过一个ListView中计算MD5的例子演示分析过程。在build函数中渲染信息组装是常见操作,为演示故意放大计算MD5耗时,循环迭代计算1万次:

class MyHomePage extends StatelessWidget {
  MyHomePage({Key key}) : super(key: key);

  String generateMd5(String data) {
    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) {
            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调用栈和繁忙程度。y轴表示调用栈,每层是一个函数;x轴表示单位时间,函数占据宽度越宽,表示采样次数越多,执行时间越长。检测CPU耗时问题,查看火焰图底部哪个函数占据宽度最大,有“平顶”表示可能存在性能问题。在本例火焰图中,_MyHomePage.generateMd5函数执行时间最长,占满宽度,与代码问题一致。找到问题后,可以使用Isolate或compute将耗时操作挪到并发主Isolate外完成。

总结

在Flutter中,性能分析过程分为GPU线程问题定位和UI线程问题定位,都需要在真机上以分析模式启动应用,并通过性能图层分析大致渲染问题范围。确认问题后,利用Flutter工具定位原因。

对于GPU线程渲染问题,重点检查应用中是否存在多视图叠加渲染或静态图像反复刷新现象。对于UI线程渲染问题,通过Performance工具记录的火焰图分析代码耗时,找出应用执行瓶颈。

由于Flutter采用声明式UI设计,数据驱动渲染,并采用Widget->Element->RenderObject三层结构,屏蔽无谓界面刷新,保证大多数应用高性能。使用分析工具检测出性能问题后,通常只需避开常见坑即可优化,如:

  • 控制build方法耗时,将Widget拆小,避免返回巨大Widget,享受更细粒度重建和复用;
  • 尽量不要为Widget设置半透明效果,考虑用图片代替,减少被遮挡区域绘制;
  • 对列表采用懒加载而不是一次性创建所有子Widget,减少视图初始化时间。

思考题

改造ListView计算MD5的示例,在保证原有功能情况下,使用并发Isolate或compute完成MD5计算。提示:计算过程可以使用CircularProgressIndicator展示加载动画。

0 Answers