本文目的
- 介绍应用流畅性的检测和优化策略
- 介绍内存的检测和优化策略
目录结构
- Flutter的绘图原理和UI的基本流程
- 流畅性
- 内存优化
性能优化涉及了应用的方法面面,很难一言以蔽之。本文我们主要讨论性能优化的两大主题 —— 流畅性和内存优化,并分别介绍了他们的检测方法和优化策略。另外,我们在优化的同时要用数据来证明优化的效果。关于如何检测应用流畅性和获取内存状态,Dart 提供了一个性能检测工具Observatory,参考使用介绍性能检测工具 Observatory 、DevTools的基础使用及测试用例
Flutter的绘图原理和UI的基本流程
Flutter的绘图原理
从图中可以看到,当GPU发出vsync信号时,会执行Dart代码绘制新UI,Dart-code会被执行为Layer Tree,然后经过Compositor合成后交由Skia引擎渲染处理为GPU数据,最后通过GL/Vulkan发给GPU。 而我们要分析的地方就在Dart->Layer Tree这里。
UI的基本流程
比如用户一个输入操作,可以理解发出为Vsync信号,这时,flutter会先做Animation相关工作,然后Build当前UI,之后视图开始布局和绘制。生成视图数据,但是只会生成Layer Tree,并不能直接使用,还是需要Composite合成为一个Layer进行Rasterize光栅化处理。层级合并的原因是因为一般flutter的层级很多,直接把每一层传给GPU传递,效率很低,所以会先做Composite,提高效率。 光栅化之后才会给Flutter-Engine处理,这里只是Framework层面的工作,所以看不到Engine,而我们分析的也只是Framework中的一小部分。
流畅性
App 流畅性的关键指标有 UI帧率,GPU帧率,我们期望它能达到 60fps,也就是16ms每帧。
以 profile / release 模式运行
为了获取最接近生产环境的数据,我们应该选择一台尽可能低端的真机,并且以 profile 模式或者 release 模式下运行app。
- 因为 debug 模式会有一些额外的检查工作,比如
assert()
等- 为了加速开发效率,debug 模式是以 JIT(Just in time)模式编译 dart 代码的,而 profile 和 release 是提前编译为机器码 AOT(Ahead Of Time),所以 debug 会慢很多
- 在 Android Studio中, 在菜单栏中点击
Run > Flutter Run 'main.dart' in Profile Mode
- 在 VS Code中可以用命令行启动: $ flutter run --profile
检测帧率
Flutter 给我们提供了 Performance Overlay
,有三种开启方式:
- 在Android Studio中: 选中
View > Tool Windows > Flutter Inspector
. 点击下面这个按钮。 - 在 VS Code中 选中
View > Command Palette…
会显示一个 command 面板,在命令面板中输入performance
并选择Toggle Performance Overlay
,如果命令显示为不可用,需要检查 app 是否正在运行. - 代码中打开 在
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。如果大多数帧都在第一格,说明达到了期望的帧率。图表分别体现了 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和图形缓存的检查
- 比如要实现一个混合图层的半透明效果:如果把透明度设置在顶层控件上,CPU会把每个子控件图层渲染出来,再执行
MaterialApp(
showPerformanceOverlay: true,
checkerboardOffscreenLayers: true, // 使用了saveLayer的图形会显示为棋盘格式并随着页面刷新而闪烁
checkerboardRasterCacheImages: true, // 做了缓存的静态图片在刷新页面时不会改变棋盘格的颜色;如果棋盘格颜色变了说明被重新缓存了,这是我们要避免的
);
提高流畅性的策略
- 代码调用时机是否可以延后?如底部导航栏式的页面,没有必要第一次进入就把每个子Page都创建出来
- 尽量做到局部刷新,例如globalkey的使用,参照用例Flutter局部刷新方法
- 把耗时的计算放到独立的isolate去执行
- 检查不必要的 saveLayer
- 检查静态图片是否添加缓存
内存优化
在内存优化方面,我们的目标是希望减少应用内存占用,减少被系统杀死的概率,同时尽可能的避免内存泄露,减少内存碎片化。内存占用情况检测
内存优化策略
- 加载对象是否过大?如图片质量和尺寸不做限制就加载
- 加载对象是否过多?如加载长列表;在调用频率很高的方法中创建对象
- 合理设置缓存大小/长度
- 在内存不足时或离开页面时清空缓存数据
- 使用ListView.build()来复用子控件
- 自定义绘图中避免在onDraw中做创建对象操作,或者相同的参数设置
- 复用系统提供的资源,比如字符串、图片、动画、样式、颜色、简单布局,在应用中直接引用
- 是否存在内存泄露的问题?比如dispose需要销毁的listener等
- 不可见的视图是否也在build?
- 页面离开后的网络请求是否取消?