文章目录
- 知识回顾
- 前言
- 源码分析
- 1. 渲染过程
- 2. 分析工具
- 3. 优化方法
- 合理使用const关键词
- 合理使用组件
- 管理着色器编译垃圾
知识回顾
前言
项目迭代开发一定程度后,性能优化是重中之重,其中包括了包体积,UI 渲染、交互等多个方面。
通过 Flutter 应用的混淆为入口,我们主要探讨了UI 渲染的优化。
其中就会涉及到一个非常关健的概念 ——「FPS,Frame Per Second」即「每秒展示帧数」,它代表了应用的流畅度。
我们知道,动画和物体动态的运动都是由在一段时间内一系列连续变化的静态帧构成的。
在考虑应用的渲染性能时,我们就是在试图分析应用每秒渲染的帧数。
从物理角度看,对于连续的一系列图像帧,人脑会根据眼睛发出的视觉信号做出反应,一个个静态帧的切换到达一定速度后,就可以欺骗我们的大脑,让我们以为它们是连续的,FPS 就是图像帧切换的速度单位。
因此有人说,物体运动的概念其实就是一种思维的束缚。
当 FPS 达到 10-12 时,大脑便可以感知运动,此时并不流畅,达到 24 FPS 时,人眼就能看到流畅的运动了,但是在电影和视频中,则至少需要每秒 60 帧的速度才可以使人的大脑轻松感知到流畅地运动。
1000ms / 60 frames = 16.666 ms/frame
我们需要在 16.66 毫秒内完成整个帧的计算,布局和渲染,否则不流畅,就需要掏出我们的 24K 合金双摄眼,找到优化点,让应用保持流畅。
源码分析
1. 渲染过程
Flutter 应用的每一帧都由框架层和引擎层互相协作完成。
最初,某些外部事件(如手势,网络等)或者异步任务会导致屏幕更新,该消息消息页会通知到引擎层。
Flutter 框架层会拦截了该请求,执行 Tickers 相关的任务(如动画)。
这些任务也可能会重新发出一个请求,以供以后的帧渲染。(如动画暂停后再继续,需要在以后的阶段接收另一个 Begin 帧)。
然后,引擎层就可以开始做屏幕渲染工作了,但在开始之前,Flutter 框架依然会拦截该请求,并根据当前的组件结构和尺寸大小计算出更新布局、绘制相关的所有数据。
完成这些任务后,如果最终确定真的要在屏幕上绘制一些东西,它就会将需要渲染的新数据发送到 Flutter Engine,做最终的屏幕更新。
整个过程都在 Flutter 的 UI 线程中运行,如若阻塞,就会卡顿。
通常,应用开发者不需要关心引擎层的逻辑,但并不意味着我们不需要关心渲染性能。
引擎层的功能其实也是单一的,他只是拿到框架层的数据去做渲染而已。但是框架层是由我们控制的,我们所写的每一个组件都在框架层之上。
如何将传递给引擎层的更新数据做到最优,就是渲染优化时我们需要考虑的问题。
这些更新数据就是由 Flutter 中重要的三棵树生成的,建议不熟悉的读者去回看之前的这篇文章。
我们需要做的就是让 Flutter 中重建组件的个数尽量少。
2. 分析工具
在 Android Studio 中,找到 Flutter Performance (View > Tool Windows > Flutter Performance),就可以直接看到正在重建的 widget 数量。
这里,勾选 Show widget rebuild information 复选框,此功能也能够帮助你检测帧的渲染和显示时间是否超过 16ms。
3. 优化方法
合理使用const关键词
const 您可以通过将其附加到Widget的构造函数来抑制Widget的重建(它与Widget缓存时的状态相同)。
构建组件时使用 const 关键词,可以抑制 widget 的重建。
const 在 Dart 中用于声明常量,应用到 widget 中就相当于告诉 Flutter,“我这个组件不会碎状态更新而改变了。”,因此达到了减少重建的效果。
使用 const 也需要注意如下几点:
当const 修饰类的构造函数时,它要求该类的所有成员都必须是final的。
const 变量只能在定义的时候初始化。
合理利用 const 关键词,可以在很大程度上优化应用的性能
合理使用组件
Flutter 实现的一些效果背后可能会使用 saveLayer() 这个代价很大的方法。
为什么 saveLayer 代价很大?
调用 saveLayer() 会开辟一片离屏缓冲区。将内容绘制到离屏缓冲区可能会触发渲染目标切换,这些切换在较早期的 GPU 中特别慢。
——来自 flutter.cn,https://flutter.cn/docs/testing/best-practices
如下这几个组件,底层都会触发 saveLayer() 的调用,同样也都会导致性能的损耗:
ShaderMask
ColorFilter
Chip,当 disabledColorAlpha != 0xff 的时候,会调用 saveLayer()。
Text,如果有 overflowShader,可能调用 saveLayer() ,
官方也给了我们一些非常需要注意的优化点:
由于 Opacity 会使用屏幕外缓冲区直接使目标组件中不透明,因此能不用 Opacity Widget,就尽量不要用。有关将透明度直接应用于图像的示例,请参见 Transparent image,比使用 Opacity widget 更快,性能更好。
要在图像中实现淡入淡出,请考虑使用 FadeInImage 小部件,该小部件使用 GPU 的片段着色器应用渐变不透明度。
很多场景下,我们确实没必要直接使用 Opacity 改变透明度,如要作用于一个图片的时候可以直接使用透明的图片,或者直接使用 Container:Container(color: Color.fromRGBO(255, 0, 0, 0.5))
Clipping 不会调用 saveLayer()(除非明确使用 Clip.antiAliasWithSaveLayer),因此这些操作没有 Opacity 那么耗时,但仍然很耗时,所以请谨慎使用。
要创建带圆角的矩形,而不是应用剪切矩形,请考虑使用很多 widget 都提供的 borderRadius属性。
管理着色器编译垃圾
有时候,应用中的动画首次运行时会看起来非常卡顿,但是运行多次之后便可以正常运行,这可能就是由于着色器编译混乱导致的。
在图形渲染,着色器相当于是在 GPU 运行的一组代码。想要达到 60fps,需要在 16 毫秒内绘制一个平滑的帧,但是在编译着色器时,它花费的时间可能比应该花费的时间更多,可能会接近几百毫秒,并且会导致丢失数十个帧,将 fps 从 60 降至 6。
解决方法
Flutter 1.20 之后,Flutter 为开发者提供了非常方便的一组命令行工具,由此开发人员可以使用 Skia Shader Language 格式收集最终用户可能需要的着色器, 一旦将 SkSL 着色器打包到应用程序中,当用户打开应用程序时,就会自动进行预编译。
运行应用,添加 --cache-sksl 参数捕获 SkSL 中的着色器:
flutter run --profile --cache-skslflutter run --profile --cache-sksl --purge-persistent-cache
该参数可能会删除 SkSL 着色器捕获的较旧的非 SkSL着色器缓存,因此只能在第一次运行时使用 --cache-sksl。
在不同平台上,可以执行以下命令,使用 SkSL 预热功能构建应用程序:
flutter build apk — bundle-sksl-path flutter_01.sksl.json