Flutter渲染性能优化全攻略(解决应用卡顿)

大规模应用开发过程中,性能优化是重中之重,其中包括了包体积,UI 渲染、交互等多个方面。

之前的文章中,我通过 Flutter 应用的混淆为入口,探讨了应用包体积优化的实践方案,本文,我把话题再次转到渲染性能上来。

其中就会涉及到一个非常关健的概念 ——「FPS,Frame Per Second」即「每秒展示帧数」,它代表了应用的流畅度。

我们知道,动画和物体动态的运动都是由在一段时间内一系列连续变化的静态帧构成的。

在考虑应用的渲染性能时,我们就是在试图分析应用每秒渲染的帧数。

从物理角度看,对于连续的一系列图像帧,人脑会根据眼睛发出的视觉信号做出反应,一个个静态帧的切换到达一定速度后,就可以欺骗我们的大脑,让我们以为它们是连续的,FPS 就是图像帧切换的速度单位。

因此有人说,物体运动的概念其实就是一种思维的束缚。

当 FPS 达到 10-12 时,大脑便可以感知运动,此时并不流畅,达到 24 FPS 时,人眼就能看到流畅的运动了,但是在电影和视频中,则至少需要每秒 60 帧的速度才可以使人的大脑轻松感知到流畅地运动。

帧数不同的感觉

因此,就有了下面这个公式:

1000ms / 60 frames = 16.666 ms/frame

我们需要在 16.66 毫秒内完成整个帧的计算,布局和渲染,否则不流畅,就需要掏出我们的 24K 合金双摄眼,找到优化点,让应用保持流畅。

定位渲染方向

那么,当我们优化 Flutter 中的渲染性能时,入口点在哪里。

Flutter 应用的每一帧都由框架层和引擎层互相协作完成。

最初,某些外部事件(如手势,网络等)或者异步任务会导致屏幕更新,该消息消息页会通知到引擎层。

Flutter 框架层会拦截了该请求,执行 Tickers 相关的任务(如动画)。

这些任务也可能会重新发出一个请求,以供以后的帧渲染。(如动画暂停后再继续,需要在以后的阶段接收另一个 Begin 帧)。

然后,引擎层就可以开始做屏幕渲染工作了,但在开始之前,Flutter 框架依然会拦截该请求,并根据当前的组件结构和尺寸大小计算出更新布局、绘制相关的所有数据。

完成这些任务后,如果最终确定真的要在屏幕上绘制一些东西,它就会将需要渲染的新数据发送到 Flutter Engine,做最终的屏幕更新。

渲染内部流程

整个过程都在 Flutter 的 UI 线程中运行,如若阻塞,就会卡顿。

通常,应用开发者不需要关心引擎层的逻辑,但并不意味着我们不需要关心渲染性能。

引擎层的功能其实也是单一的,他只是拿到框架层的数据去做渲染而已。但是框架层是由我们控制的,我们所写的每一个组件都在框架层之上。

如何将传递给引擎层的更新数据做到最优,就是渲染优化时我们需要考虑的问题。

这些更新数据就是由 Flutter 中重要的三棵树生成的,建议不熟悉的读者去回看之前的这篇文章。

我们需要做的就是让 Flutter 中重建组件的个数尽量少。

所以我说,优秀的 Flutter 代码可以在每次帧渲染的过程更新数据几乎为 0,这也是最优情况。

而最差的情况就是,每次帧渲染时要渲染整个应用的所有组件,这其中性能差异巨大。

在实际开发过程中,如果将整个页面写在一个单独的 StatefulWidget 中,那么每次状态更新时都会导致很多不必要的 UI 重建。

因此, 我们要学会拆解组件,使用良好设计模式和状态管理方案,当需要更新状态时将影响范围降到最小。

测定方法

在 Android Studio 中,找到 Flutter Performance (View > Tool Windows > Flutter Performance),就可以直接看到正在重建的 widget 数量。

这里,勾选 Show widget rebuild information 复选框,此功能也能够帮助你检测帧的渲染和显示时间是否超过 16ms。

Flutter performance window

在其他如 VSCode 编辑器中也可以使用 DevTool 做性能分析。

Dart DevTools Screens

优化方法

合理使用const关键词

const 您可以通过将其附加到Widget的构造函数来抑制Widget的重建(它与Widget缓存时的状态相同)。

构建组件时使用 const 关键词,可以抑制 widget 的重建。

如下这个 Flutter 的默认计数器应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

在不修改代码的默认情况下,widget 重建情况如下:

未使用 const 的 widget

修改如下部分代码:

1
2
3
4
5
6
7
-            Text(
- 'You have pushed the button this many times:',
- ),

+ const Text(
+ 'You have pushed the button this many times:',
+ ),

结果:

使用 const 的 widget

const 在 Dart 中用于声明常量,应用到 widget 中就相当于告诉 Flutter,“我这个组件不会碎状态更新而改变了。”,因此达到了减少重建的效果。

使用 const 也需要注意如下几点:

  • const 修饰类的构造函数时,它要求该类的所有成员都必须是final的。
  • const 变量只能在定义的时候初始化。

合理利用 const 关键词,可以在很大程度上优化应用的性能,国外开发者 Crizant Lai 给出一了一份详细的数据。

如下示例:

这里,他使用 AnimatedPositioned 做动画组件,内部放一个展示 Flutter Logo 的 Image widget,分别使用 const 和 不使用 const 修饰 Image:

1
2
3
4
5
const Image(
width: 100,
height: 100,
image: AssetImage('assets/logo.png'),
)

最终测试性能如下:

FPS

内存

合理使用组件

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 中的着色器:

1
flutter run --profile --cache-sksl

如果该应用已经运行,且没有带有 --cache-sksl 参数,则还需要 --purge-persistent-cache

1
flutter run --profile --cache-sksl --purge-persistent-cache

该参数可能会删除 SkSL 着色器捕获的较旧的非 SkSL着色器缓存,因此只能在第一次运行时使用 --cache-sksl

在不同平台上,可以执行以下命令,使用 SkSL 预热功能构建应用程序:

安卓

1
flutter build apk — bundle-sksl-path flutter_01.sksl.json

ios

1
flutter build ios --bundle-sksl-path flutter_01.sksl.json

结论

本文从原理到应用,分析了 Flutter 应用性能优化的入口及方法,但关于性能优化的话题还远不止于此,诸如状态管理方案的选择、动画的使用都是我们在实际开发过程中应当注意的优化方向。

感谢阅读。如果你发现文章中有任何可以改进的地方,请直接联系我,我很乐意继续完善下去。💙

如果本文对你所有帮助,欢迎 “点赞” 和 “在看”!👏

关于框架层更多内容,欢迎大家阅读我的新书《Flutter开发之旅从南到北》。

本文同步发表于我的微信公众号「MeandNi」,更多优质文章欢迎点击扫码或在微信搜索关注。

扫一扫,关注「Meandni」
文章作者: Joker
文章链接: https://meandni.com/2021/01/05/flutter-optimization/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Joker's Blog