前言
App 流畅性的关键指标有 UI帧率和 GPU 帧率,我们期望它能达到 60fps,也就是16ms每帧。为了获取最接近生产环境的数据,我们应该选择一台尽可能低端的真机,并且以 profile 模式或者 release 模式下运行app。
这是因为 debug 模式会有一些额外的检查工作,比如 assert() 等,而且为了加速开发效率,debug 模式是以 JIT(Just in time)模式编译 dart 代码的,而 profile 和 release 是提前编译为机器码 AOT(Ahead Of Time),所以 debug 会慢很多。因此,在查看性能时不要用debug 模式,否则可能无法准确评估性能。
Flutter运行模式
Flutter 主要有以下几种运行模式:
-
Debug模式:调试页面开发时使用,可以在真机和模拟器上同时运行。此模式会打开所有的断言,包括debugging信息、debugger aids(比如observatory)和服务扩展,优化了快速开发/运行循环,但是没有优化执行速度、二进制大小和部署。命令
flutter run就是以这种模式运行的。 -
Release模式:部署发包时使用,只能在真机上运行,不能在模拟器上运行。此模式会关闭所有断言和debugging信息,关闭所有debugger工具,优化了快速启动、快速执行和减小包体积,禁用所有的debugging aids和服务扩展。命令
flutter run --release就是以这种模式运行的。 -
Profile模式:调试性能时使用,只能在真机上运行,不能在模拟器上运行。基本和Release模式一致,除了启用了服务扩展和tracing,以及一些为了最低限度支持tracing运行的东西(比如可以连接observatory到进程)。命令
flutter run --profile就是以这种模式运行的。 -
Test模式:headless test模式只能在桌面上运行,基本和Debug模式一致,除了是headless的而且能在桌面运行。命令
flutter test就是以这种模式运行的。
怎么使用profile模式呢?
为了调试性能问题,我们需要在发布模式的基础之上,为分析工具提供少量必要的应用追踪信息,这就是分析模式。除了一些调试性能问题必须的追踪方法之外,Flutter 应用的分析模式和发布模式的编译和运行是类似的,只是启动参数变成了 profile 而已。
我们可以在 Android Studio 中通过菜单栏点击 Run => Profile => main.dart 选项启动应用,也可以通过命令行参数 flutter run --profile 运行 Flutter 应用。在 VSCode 中,可以打开 launch.json 文件并设置 flutterMode 为 profile。
检测帧率
在 Android Studio 中,可以通过 File=>Settings 中搜索 flutter 找到配置,打开 Open Flutter Inspector view on app launch,然后选中 View > Tool Windows > Flutter Performance。性能工具会显示一些按钮,用于在应用中显示性能图层或减速以便查看帧率。
在 VS Code 中,选中 View > Command Palette… 会显示一个命令面板,输入 performance 并选择 Toggle Performance Overlay。如果命令显示为不可用,需要检查 app 是否正在运行。
在代码中,可以在 MaterialApp 或者 WidgetsApp 的构造函数中设置 showPerformanceOverlay 属性为 true 来开启性能图层。
怎么看帧率
性能图层展示了 GPU 线程和 UI 线程的性能情况。其中,GPU 线程的性能情况在上面,UI 线程的情况显示在下面,蓝色垂直的线条表示已执行的正常帧,绿色的线条代表的是当前帧。为了保持 60Hz 的刷新频率,GPU 线程与 UI 线程中执行每一帧耗费的时间都应该小于 16ms(1/60 秒)。
如果有一帧处理时间过长,就会导致界面卡顿,图表中就会展示出一个红色竖条。如果红色竖条出现在 GPU 线程图表,意味着渲染的图形太复杂,导致无法快速渲染;而如果是出现在了 UI 线程图表,则表示 Dart 代码消耗了大量资源,需要优化代码执行时间。图表分别体现了 UI帧率和 GPU帧率,如果出现了红色,说明对应的线程有太多工作要做。
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中设置 checkerboardOffscreenLayers: true 和 checkerboardRasterCacheImages: true。
提高流畅性的策略
- 代码调用时机是否可以延后?如底部导航栏式的页面,没有必要第一次进入就把每个子Page都创建出来。
- 尽量做到局部刷新。
- 把耗时的计算放到独立的isolate去执行。
- 检查不必要的 saveLayer。
- 检查静态图片是否添加缓存。
- 参考 relayout boundary 和 repaint boundary 的相关文档进行优化。
内存优化
在内存优化方面,目标是减少应用内存占用,减少被系统杀死的概率,同时尽可能的避免内存泄露,减少内存碎片化。
内存优化策略包括:
- 避免加载对象过大,如图片质量和尺寸不做限制就加载。
- 避免加载对象过多,如加载长列表;在调用频率很高的方法中创建对象。可以合理设置缓存大小/长度,在内存不足时或离开页面时清空缓存数据,使用ListView.build()来复用子控件,在自定义绘图中避免在onDraw中做创建对象操作,或者相同的参数设置,以及复用系统提供的资源,比如字符串、图片、动画、样式、颜色、简单布局。
- 检查内存泄露的问题,比如dispose需要销毁的listener等。
- 确保不可见的视图不在build过程中。
- 页面离开后的网络请求是否取消。