Flutter 性能优化 Tips

12,007 阅读11分钟

本文目的

  • 介绍应用流畅性的检测和优化策略
  • 介绍内存的检测和优化策略
  • 介绍性能优化证明的意义和流程
  • 介绍性能检测工具 Observatory 的基础使用

目录结构

  • 流畅性
  • 内存优化
  • 优化证明
  • 性能检测利器 Observatory 基础使用
  • 总结

流畅性

App 流畅性的关键指标有 UI帧率,GPU帧率,我们期望它能达到 60fps,也就是16ms每帧。

以 profile / release 模式运行

为了获取最接近生产环境的数据,我们应该选择一台尽可能低端的真机,并且以 profile 模式或者 release 模式下运行app。

  1. 因为 debug 模式会有一些额外的检查工作,比如assert()
  2. 为了加速开发效率,debug 模式是以 JIT(Just in time)模式编译 dart 代码的,而 profile 和 release 是提前编译为机器码 AOT(Ahead Of Time),所以 debug 会慢很多
  1. 在 Android Studio and IntelliJ 中, 在菜单栏中点击 Run > Flutter Run main.dart in Profile Mode

  2. VS Code:打开 launch.json 文件并设置flutterMode 为 profile:

"configurations": [
	{
		"name": "Flutter",
		"request": "launch",
		"type": "dart",
		"flutterMode": "profile" # 测试完后记得把它改回去!
	}
]
  1. 用命令行启动:
$ flutter run --profile

检测帧率

那么检测帧率有哪些方法呢?Flutter 给我们提供了 Performance Overlay,如下图,绿色代表当前渲染帧。

performance-overlay-green

我们有三种开启方式

  1. 在Android Studio 和 IntelliJ IDEA中: 选中 View > Tool Windows > Flutter Inspector. 点击下面这个按钮。

  1. 在 VS Code中 选中 View > Command Palette… 会显示一个 command 面板. 在命令面板中输入 performance 并选择 Toggle Performance Overlay 如果命令显示为不可用,需要检查 app 是否正在运行.

  2. 从命令行中运行 键盘输入P

  3. 代码中打开 在MaterialApp 或者 WidgetsApp的构造函数中设置showPerformanceOverlay 属性为 true :

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showPerformanceOverlay: true, // 开启
      title: 'My Awesome App',
      home: MyHomePage(title: 'My Awesome App'),
    );
  }
}

然后就是动手操作 app,并观察图表上是否出现红色线条。绿色代表当前帧,当页面有变动,图表会不断绘制。蒙版上有2个图表,每个图表上有三横格,每个横格代表16ms。如果大多数帧都在第一格,说明达到了期望的帧率。

performance overlay jank

图表分别体现了 UI帧率 和 GPU帧率。如果出现了红色,说明对应的线程有太多work要做。那先来了解一下 Flutter 中的4个主要线程分别承担了什么职责。

  • Platform线程:插件代码运行的线程;即Android/iOS的主线程,
  • UI线程:在Dart虚拟机中执行Dart代码。作用是创建视图树,然后将它发送给GPU。注意不要阻塞此线程!
  • GPU线程:把上面提到的视图树渲染出来,虽然我们在flutter中不能直接访问GPU线程和数据,但是Dart代码可能导致此线程变慢
  • I/O线程:执行比较耗时的任务

在运行app的过程中,观察爆红的地方和触发场景,进行分析。

分析思路

  • 如果是UI报红:那么可能是执行了某个较耗时的函数?或者函数调用过多?算法复杂度高?
  • 如果只是 GPU 报红:那么可能是要绘制的图形过于复杂?或者执行了过多GPU操作?
    • 比如要实现一个混合图层的半透明效果:如果把透明度设置在顶层控件上,CPU会把每个子控件图层渲染出来,再执行saveLayer操作保存为一个图层,最后给这个图层设置透明度。而saveLayer开销很大,这里官方给出了一个建议:首先确认这些效果是否真的有必要;如果有必要,我们可以把透明度设置到每个子控件上,而不是父控件。裁剪操作也是类似。
    • 还有一个拖慢GPU渲染速度的是没有给静态图像做缓存,导致每次build都会重新绘制。我们可以把静态图形加到RepaintBoundry控件中,引擎会自动判断图像是否复杂到需要用repaint boundary,不需要的话也会忽略。
    • 开启saveLayer和图形缓存的检查
    MaterialApp(
        showPerformanceOverlay: true,
        checkerboardOffscreenLayers: true, // 使用了saveLayer的图形会显示为棋盘格式并随着页面刷新而闪烁
        checkerboardRasterCacheImages: true, // 做了缓存的静态图片在刷新页面时不会改变棋盘格的颜色;如果棋盘格颜色变了说明被重新缓存了,这是我们要避免的
        ...
    );
    

提高流畅性的策略

  • 代码调用时机是否可以延后?如底部导航栏式的页面,没有必要第一次进入就把每个子Page都创建出来
  • 尽量做到局部刷新
  • 把耗时的计算放到独立的isolate去执行
  • 检查不必要的 saveLayer
  • 检查静态图片是否添加缓存
  • relayout boundary:参考
  • repaint boundary:参考

内存优化

在内存优化方面,我们的目标是希望减少应用内存占用,减少被系统杀死的概率,同时尽可能的避免内存泄露,减少内存碎片化。

内存优化策略

  • 加载对象过大?如图片质量和尺寸不做限制就加载
  • 加载对象过多?如加载长列表;在调用频率很高的方法中创建对象
    • 合理设置缓存大小/长度
    • 在内存不足时或离开页面时清空缓存数据
    • 使用ListView.build()来复用子控件
    • 自定义绘图中避免在onDraw中做创建对象操作,或者相同的参数设置
    • 复用系统提供的资源,比如字符串、图片、动画、样式、颜色、简单布局,在应用中直接引用
  • 内存泄露的问题?比如dispose需要销毁的listener等
  • 不可见的视图是否也在build?
  • 页面离开后的网络请求是否取消?

如何获取内存状态

Dart 提供了一个性能检测工具Observatory,我在最后一部分会进行详细介绍

优化证明

优化证明的意义

性能优化不像其它的开发需求只要完成功能即可,它需要通过统计和数据来证明优化的效果。比如帧率有了多少提高?CPU占用率降低了多少?内存占用减少了多少?对比其它优化策略,哪个优化效果好?

优化证明的流程

profile prove

举个例子

以检查流畅性为例

1. 在profile模式下运行并开启Performance Overlay,整体测试app

2. 找到帧率报红色的模块

3. 把页面孤立出来,并多次测量,并得到baseline(参照)帧率数据。比如长列表页面出现了卡顿,我们可以用TestDriver写一个ListView滑动的性能测试(更多参考Flutter gallery)

scroll_pref.dart

void main() {
  enableFlutterDriverExtension();
  runApp(const GalleryApp(testMode: true));
}

scroll_perf_test.dart

void main() {
  group('scrolling performance test', () {
    FlutterDriver driver;

    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    tearDownAll(() async {
      if (driver != null)
        driver.close();
    });

    test('measure', () async {
      final Timeline timeline = await driver.traceAction(() async {
        await driver.tap(find.text('Material'));

        final SerializableFinder demoList = find.byValueKey('GalleryDemoList');

        for (int i = 0; i < 5; i++) {
          await driver.scroll(demoList, 0.0, -300.0, const Duration(milliseconds: 300));
          await Future<void>.delayed(const Duration(milliseconds: 500));
        }

        // Scroll up
        for (int i = 0; i < 5; i++) {
          await driver.scroll(demoList, 0.0, 300.0, const Duration(milliseconds: 300));
          await Future<void>.delayed(const Duration(milliseconds: 500));
        }
      });

      TimelineSummary.summarize(timeline)
        ..writeSummaryToFile('home_scroll_perf', pretty: true)
        ..writeTimelineToFile('home_scroll_perf', pretty: true);
    });
  });
}

在命令行下执行以下命令

flutter driver --target=test_driver/scroll_perf.dart 

这个命令会:

  • build 目标 app,并把它安装到设备上
  • 运行位于test_driver/目录下的scroll_perf_test.dart的测试( flutter drive 能帮你找到带 _test后缀的同名文件)

Test Driver 将会安装 app 到设备上,再跳转到 Material-GalleryDemoList 页面,做5次滑动列表的操作。执行完成后会借助 TimelineSummary ,在build目录下生成两个json文件:home_scroll_perf.timeline.jsonhome_scroll_perf.timeline_summary.json。这里我们看一下timeline_summary.json文件的内容

{
  "average_frame_build_time_millis": 5.6319655172413805, # 平均每帧 build 时间
  "90th_percentile_frame_build_time_millis": 10.216, 
  "99th_percentile_frame_build_time_millis": 17.168,
  "worst_frame_build_time_millis": 20.415, # 最长帧 build 时间
  "missed_frame_build_budget_count": 21, # build 期丢帧数
  "average_frame_rasterizer_time_millis": 14.234294964028772, # 平均每帧光栅化时间
  "90th_percentile_frame_rasterizer_time_millis": 22.338,
  "99th_percentile_frame_rasterizer_time_millis": 42.661,
  "worst_frame_rasterizer_time_millis": 43.161,
  "missed_frame_rasterizer_budget_count": 112,
  "frame_count": 116,
  "frame_build_times": [
      ... 
  ],# 所有帧的 build 时间
  "frame_rasterizer_times": [
      ...
  ] # 所有帧的光栅化时间
}

4. 优化

5. 用步骤3的方法再次测量,对比baseline得出确切的优化效果

Flutter 提供的性能调试 API

更多可以参考官方文档

性能检测利器 Observatory

Observatory 是用于分析和调试Dart应用程序的工具。Observatory允许您根据需要查看正在运行的Dart虚拟机(VM),并提供实时,即时的数据报告。您可以使用它来浏览应用程序的很多状态。

打开Observatory

有2种方式:

  1. 在 androidStudio 中打开Flutter Inspector面板,点击小闹钟图标,如下图
    open observatory from AS
  2. 再命令行中运行flutter run,应用启动成功后,命令行中会输出一个 url,把 url copy 到浏览器即可。
    open observatory from command line

打开Observatory面板,要先选择isolate,表示当前应用。

entey screen

主要页面

下面是性能优化常关注的几个页面。

main screen

1. CPU Profile

app的时间都花在哪了?

进入这个页面后要一般需加载个几秒钟,so be patient。图表的下部按cpu占用比例做了一个列表,反映的是函数的调用次数和执行时间(划重点)。一般排在前面的函数(这些函数是?有待学习)都不是我们写的dart代码。如果你发现自己的某个函数调用占比反常,那么可能存在问题。

注:flutter程序的cpu profile和官方文档上的数据展示不太一样,没有VM tags,所以对于百分比的具体含义有待研究。

cpu profile

采样过程:它每隔一定时间对isolate做采样,采样的数据存储在一个环形缓冲区(叫做profile),它能存放约2分钟的数据,一旦缓冲区满了,它会用最新的sample替换掉最旧的。

  • Profile contains:采样时长和对应的采样数
  • Sampling:采样频率,默认1000Hz,即每毫秒采样一次

2. Allocation Profile

内存都被谁吃了?

allocation profile

Heap 堆,动态分配的Dart对象所在的内存空间

  • New generation: 新创建的对象,一般来说对象比较小,生命周期短,如local 变量。在这里GC活动频繁
  • Old generation:从GC中存活下来的New generation将会提拔到老生代Old generation,它比新生代空间大,更适合大的对象和生命周期长的对象

通过这个面板你能看到新生代/老生代的内存大小和占比;每个类型所占用的内存大小。

为了debug的方便,我们可以获取到某段时间的内存分配情况:点击Reset Accumulator按钮,把数据清零,执行一下要测试的程序,点击刷新。

为了检查内存泄露,我们可以点击GC按钮,手动执行GC。

Accumulator Size:自点击Reset Accumulator以来,累加对象占用内存大小 Accumulator Instances:自点击Reset Accumulator以来,累加实例个数 Current Size:当前对象占用内存大小 Current Instances:当前对象数量

3. Heap Map

是否出现内存碎片化

heap map 面板能查看old generation中的内存状态

它以颜色显示内存块。 每个内存页面(page of memory)为256 KB,每页由水平黑线分隔。 像素的颜色表示对象的类ID - 例如,蓝色表示字符串,绿色表示双精度表。 可用空间为白色,指令(代码)为紫色。 如果启动垃圾收集(使用“分配配置文件”屏幕中的GC按钮),堆映射中将显示更多空白区域(可用空间)。 将光标悬停在上面时,顶部的状态栏显示有关光标下像素所代表的对象的信息。 显示的信息包括该对象的类型,大小和地址。 当你看到白色区域中有很多分散的其它颜色,说明存在内存碎片化,可能是内存泄露导致的。

其它

1. Code Coverage

知道哪些代码执行了,哪些没有执行

code coverage

  • 绿色:已执行的代码
  • 红色:未执行的代码
  • 没有颜色:不可执行的代码

应用场景:写某个类的单元测试,跑完测试后,可以查看哪些代码没有覆盖到,进而补全

2. Class/Instance 信息

查看某个实例的状态,比如我们的项目中使用了Flutter_redux,页面的展示来源与状态树,当页面出现了非预期的效果,我们可以通过Observatory查看状态树

watch state

举个例子

Observatory 帮我找到循环调用的真凶

总结

性能优化涉及了应用的方法面面,很难一言以蔽之。本文我们主要讨论了性能优化的两大主题 —— 流畅性和内存优化,并分别介绍了他们的检测方法和优化策略。另外,我们在优化的同时也要加强优化的证明,用数据说话。最后,我强烈推荐大家尝试一下 Observatory 这个工具,开发中如果遇到了奇怪的问题,没准它能帮你找到答案。

参考

本文版权属于再惠研发团队,欢迎转载,转载请保留出处。@akindone