Flutter 应用程序性能优化建议
视频
https://www.bilibili.com/video/BV1ht421L7mP/
前言
原文 https://ducafecat.com/blog/boosting-flutter-performance-top-tips-for-developers
Flutter应用程序默认已经具有良好的性能,因此您只需要避免常见的陷阱,就可以获得出色的性能。
您设计和实现应用程序的用户界面的方式可能会对其运行效率产生重大影响。
本文这些最佳实践建议将帮助您编写性能最佳的Flutter应用程序。
那么让我们开始吧!
正文
代码结构拆分合理
干净架构
细致的拆分
https://marketplace.visualstudio.com/items?itemName=ducafecat.getx-template
使用状态管理
需要一套规范来耦合所有的内容
常见的优秀状态管理有:
- provider
- bloc
- getx
- riverpod
可以看下各种状态管理文章 https://ducafecat.com/blog/flutter-state-management-libraries-2024
使用代码分析工具
代码分析工具,如Flutter分析器和Lint,对于提高代码质量和减少错误和漏洞的风险非常有帮助。这些工具可以帮助识别潜在问题,防止它们成为问题,并提供改进代码结构和可读性的建议。
flutter analyze lib/
使用 Flutter Inspector 进行调试
flutter run --debug
之前录过一个 dev tools 性能调优的视频
https://www.bilibili.com/video/BV1Tb4y1p7t9
https://ducafecat.tech/2022/03/17/2022/flutter-devtools-performance/
懒加载和分页
一次获取和渲染大量数据可能会显著影响性能。实现延迟加载和分页,根据需要加载数据,特别是对于长列表或数据密集的视图。
ListView.builder 方式
List<Item> loadItems(int pageNumber) {
}
ListView.builder(
itemCount: totalPages,
itemBuilder: (context, index) {
return FutureBuilder(
future: loadItems(index),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// Build your list item here.
} else {
return CircularProgressIndicator();
}
},
);
},
);
pull_to_refresh_flutter 方式
build(BuildContext context) {
return Scaffold(
body: SmartRefresher(
enablePullDown: true,
enablePullUp: true,
header: WaterDropHeader(),
footer: CustomFooter(
builder: (BuildContext context,LoadStatus mode){
Widget body ;
if(mode==LoadStatus.idle){
body = Text("pull up load");
}
else if(mode==LoadStatus.loading){
body = CupertinoActivityIndicator();
}
else if(mode == LoadStatus.failed){
body = Text("Load Failed!Click retry!");
}
else if(mode == LoadStatus.canLoading){
body = Text("release to load more");
}
else{
body = Text("No more Data");
}
return Container(
height: 55.0,
child: Center(child:body),
);
},
),
controller: _refreshController,
onRefresh: _onRefresh, // 下拉刷新
onLoading: _onLoading, // 上拉载入
child: ListView.builder(
itemBuilder: (c, i) => Card(child: Center(child: Text(items[i]))),
itemExtent: 100.0,
itemCount: items.length,
),
),
);
}
Widget
https://pub-web.flutter-io.cn/packages/pull_to_refresh_flutter3
压缩图片
你获取到一张宽 40000 的图片,如果你直接打印,真的噩梦了。
你需要本地压缩后再显示 flutter_image_compress。
https://pub.dev/packages/flutter_image_compress
Future<Uint8List> testCompressFile(File file) async {
var result = await FlutterImageCompress.compressWithFile(
file.absolute.path,
minWidth: 2300,
minHeight: 1500,
quality: 94,
rotate: 90,
);
print(file.lengthSync());
print(result.length);
return result;
}
优化动画
避免使用对应用程序性能产生影响的繁重或复杂的动画,尤其是在旧设备上。谨慎使用动画,并考虑使用Flutter内置的动画,如 AnimatedContainer
, AnimatedOpacity
等。
// 1 秒太长了
AnimatedContainer(
duration: Duration(seconds: 1),
height: _isExpanded ? 300 : 1000,
color: Colors.blue,
);
// 缩短动画时长
AnimatedContainer(
duration: Duration(milliseconds: 500),
height: _isExpanded ? 300 : 100,
color: Colors.blue,
);
优化应用程序启动时间
通过优化初始化过程来减少应用程序的启动时间。使用 flutter_native_splash
包在应用程序加载时显示启动画面,并延迟非必要组件的初始化直到应用程序启动后。
https://pub.dev/packages/flutter_native_splash
多些组件抽取
不要去写层次很深的代码, 多些代码抽取。
// 主视图
Widget _buildView() {
List<Widget> ws = [];
// 标题
if (title != null) {
ws.add(_buildTitle(title!));
}
// 统计栏
ws.add(_buildTotalBar(win, draw, lose, winAvg, loseAvg));
// 视图
for (var item in fixtures) {
// 栏
if (item.league?.id != lastLeagueId) {
lastLeagueId = item.league?.id ?? 0;
ws.add(_buildLeagueBar(lastLeagueId, item.league?.name ?? ""));
}
// 行
ws.add(_buildRow(item));
// 分隔符
ws.add(Container(
height: 0.5,
color: AppColors.surfaceVariant,
));
}
return ws.toColumn();
}
使用级联(..)
如果你刚开始使用Flutter,你可能还没有使用过这个运算符,但当你想在同一个对象上执行某些任务时,它非常有用。
//Bad
var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;
//Good
var paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;
使用扩展运算符(...)
其它语言也有,可以用来合并集合,只是下面的用法太神奇。
// 很啰嗦
Widget build(BuildContext context) {
bool isTrue = true;
return Scaffold(
body: Column(
children: [
isTrue ? const Text('One') : Container(),
isTrue ? const Text('Two') : Container(),
isTrue ? const Text('Three') : Container(),
],
),
);
}
// 才知道可以这样用 ... 符号
Widget build(BuildContext context) {
bool isTrue = true;
return Scaffold(
body: Column(
children: [
if(isTrue)...[
const Text('One'),
const Text('Two'),
const Text('Three')
]
],
),
);
}
抽取你的样式定义
// 繁琐
Column(
children: const [
Text(
'One',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
Text(
'Two',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
)
// 复用 重构
Column(
children: [
Text(
'One',
style: Theme.of(context).textTheme.subtitle1,
),
Text(
'Two',
style: Theme.of(context).textTheme.subtitle1,
),
],
),
局部刷新
StatefulBuilder
方式
int a = 0;
int b = 0;
// 1、定义一个叫做“aState”的StateSetter类型方法;
StateSetter? aState;
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 2、将第一个“ElevatedButton”组件嵌套在“StatefulBuilder”组件内;
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
aState = setState;
return ElevatedButton(
onPressed: () {
a++;
// 3、调用“aState”方法对“StatefulBuilder”内部进行刷新;
aState(() {
});
},
child: Text('a : $a'),
);
},
),
ElevatedButton(
onPressed: () {
b++;
setState(() {
});
},
child: Text('b : $b'),
),
],
),
),
);
}
也可以用 getx
GetBuilder
这种状态组件实现局部刷新。
定义 GetBuilder
,设置 id
名称
build(BuildContext context) {
return GetBuilder<HomeIndexController>(
init: HomeIndexController(),
id: "home_index",
builder: (_) {
return Scaffold(
appBar: appBarWidget(...),
body: _buildView(),
);
},
);
}
Widget
控制器触发, 制定 id
名称,可以是一个列表
update(["home_index"]);
多使用 Widget
抽取组件,而不是函数
您可以节省CPU周期,并使用const构造函数,在仅在需要时进行重建,并获得更多的好处(例如重用等)。
// 定义成 widget
Widget build(BuildContext context) {
return Column(
children: [
HeaderWidget(),
SubHeaderWidget(),
ContentWidget()
]
);
}
使用 final
使用 final
关键字可以极大地提高您的应用程序的性能。当一个值被声明为 final
时,它只能被设置一次,之后不会再改变。这意味着框架不需要不断地检查变化,从而提高了性能。
final String tag;
final Color? color;
final Size? size;
final double? radius;
final Color? fontColor;
使用 const
如果已经定义了,您可以使用相同的 Widget 来节省RAM。 const widgets
在编译时创建,因此在运行时更快。
x = const Container();
y = const Container();
使用 const 类构造函数
这有助于 Flutter 仅重新构建应更新的 Widget。
class FbTagWidget extends StatelessWidget {
const FbTagWidget(this.tag,
{
super.key, this.color, this.size, this.radius, this.fontColor});
尽可能使用 private
关键词
这更像是 Dart 的最佳实践,而不是性能。
但是,最佳实践可以在某种程度上提高性能,比如理解代码,减少复杂性等等。
class Student{
String _name;
String _address;
Student({
required String name,
required String address,
}):
_name = name,
_address = address;
}
使用nil代替const Container()
零消耗
// 原来
text != null ? Text(text) : const Container()
// 后来
text != null ? Text(text) : const SizedBox()
// 现在
text != null ? Text(text) : nil
在ListView中使用itemExtent来处理长列表
这有助于Flutter计算滚动位置,而不是计算每个 Widget 的高度,并使滚动动画更加高效。
默认情况下,每个子项都必须确定其范围,这在性能方面是非常昂贵的。显式设置值可以节省大量的CPU周期。列表越长,使用此属性可以获得更多的速度提升。
final List<int> _listItems = <int>[1, 2, 3, 4, 5, 6, 7, 8, 9];
Widget build(BuildContext context) {
return ListView.builder(
itemExtent: 150,
itemCount: _listItems.length,
itemBuilder: (context, index) {
var item = _listItems[index];
return Center(
child: Text(item.toString())
);
}
}
避免在 setState 中使用 AnimationController
错误的方式,将 addListener
去掉。
void initState() {
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
)..addListener(() => setState(() {
}));
}
Column(
children: [
Placeholder(), // rebuilds
Placeholder(), // rebuilds
Placeholder(), // rebuilds
Transform.translate( // rebuilds
offset: Offset(100 * _controller.value, 0),
child: Placeholder(),
),
],
),
使用Keys来加速Flutter性能
在Flutter中,使用Keys
可以帮助加速性能并优化应用程序的重建过程。Keys
在Flutter中有多种用途,其中一项重要的功能是帮助Flutter识别小部件树中的特定小部件,从而在进行重建时更有效地更新小部件。以下是一些示例,说明如何使用Keys
来加速Flutter性能:
保留状态:使用GlobalKey
作为Key
的一种常见用法是在需要保留小部件状态的情况下。通过在重建时将相同的GlobalKey
分配给相同类型的小部件,可以确保小部件在重建后保留其先前的状态,而不会丢失用户的输入或滚动位置。
class MyWidget extends StatefulWidget {
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
// Widget content
);
}
}
列表中的重用:在ListView
或GridView
等可滚动列表中,使用Key
可以帮助Flutter跟踪列表项并在数据源更改时有效地更新列表项,而无需重新创建整个列表。
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
key: Key(items[index].id.toString()),
title: Text(items[index].title),
);
},
)
动态添加或移除小部件:在动态添加或移除小部件时,使用Key
可以帮助Flutter正确识别要添加或移除的小部件,而不会影响其他部分的布局。
List<Widget> widgets = [
Container(key: Key('1'), child: Text('Widget 1')),
Container(key: Key('2'), child: Text('Widget 2')),
];
// Add a new widget
widgets.add(Container(key: Key('3'), child: Text('Widget 3')));
// Remove a widget
widgets.removeWhere((widget) => widget.key == Key('2'));
通过使用Keys
,开发人员可以更精确地控制Flutter小部件树的重建过程,避免不必要的重建,提高应用程序的性能和响应性。
使用 ListView 列表视图时优化内存
ListView.builder(
...
addAutomaticKeepAlives: false (true by default)
addRepaintBoundaries: false (true by default)
);
- addAutomaticKeepAlives 当这个属性设置为true时,Flutter会尝试在滚动列表时保留列表项的状态。这意味着即使列表项在屏幕外被移除,它们的状态仍然会被保留,以便在滚回到它们时可以保持其状态。
- addRepaintBoundaries 当这个属性设置为true时,Flutter会尝试在列表项之间创建重绘边界。这意味着在滚动列表时,只有在需要时才会重绘列表项,而不是每次滚动都重绘所有内容。
使用 for/while 代替 foreach/map
如果你要处理大量的数据,使用正确的循环可能会对你的性能产生影响。
预缓存您的图片和图标
图片
precacheImage(
AssetImage(imagePath),
context
);
svg
precachePicture(
ExactAssetPicture(SvgPicture.svgStringDecoderBuilder, iconPath),
context
);
使用SKSL预热
如果一个应用在第一次运行时的动画不流畅,但后来相同的动画变得流畅,那很可能是由于着色器编译引起的不流畅。
flutter run --profile --cache-sksl --purge-persistent-cache
flutter build apk --cache-sksl --purge-persistent-cache
使用 RepaintBoundary
RepaintBoundary
是一个 Widget ,用于将其子部件的绘制内容分离为单独的绘制层。这样做的主要目的是减少不必要的重绘操作,提高应用程序的性能。当RepaintBoundary
包裹一个子部件时,该子部件及其所有子部件将被视为一个整体,即使其中的其他部分发生重绘,RepaintBoundary
内的内容也不会重绘。
RepaintBoundary
的主要作用包括:
减少重绘范围:通过将子部件包裹在
RepaintBoundary
中,可以将其视为一个整体,仅在该部件内部发生重绘时才重新绘制,而不会影响到其他部分。性能优化:避免不必要的重绘操作,可以提高应用程序的性能,特别是在具有复杂界面或动态内容的情况下。
避免全局重绘:在某些情况下,只需要更新特定部分的UI,而不是整个界面。通过使用
RepaintBoundary
,可以限制重绘的范围,避免全局重绘。边界控制:可以通过
RepaintBoundary
来控制重绘的边界,确保只在需要时才进行重绘操作,而不会影响到其他部分。
RepaintBoundary
是一个有用的工具,可以帮助优化Flutter应用程序的性能,特别是在需要控制重绘范围和避免不必要重绘操作的情况下。在开发复杂界面或需要动态更新的应用程序时,合理使用RepaintBoundary
可以提高应用程序的性能和用户体验。
class RepaintBoundaryExample extends StatelessWidget {
Widget build(BuildContext context) {
return RepaintBoundary(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('This is inside RepaintBoundary'),
SizedBox(height: 20),
CustomPaint(
size: Size(200, 200),
painter: MyPainter(),
),
],
),
);
}
}
class MyPainter extends CustomPainter {
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.blue
..strokeWidth = 5
..style = PaintingStyle.stroke;
canvas.drawRect(Rect.fromLTWH(50, 50, 100, 100), paint);
}
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
使用 Listview.builder
Listview.builder()
用了之后不出现在屏幕上的元素,不渲染。
不要使用 ShrinkWrap 来包裹可滚动 Widget
ShrinkWrap 动态确定子组件大小。
包裹滚动组件后可能有布局错误、性能问题。
处理高消耗操作时用 isolates
比如处理非常大的 json 文件、视频压缩。
这样不会卡主线程。
可以用一些 Dart 包简化代码。
https://pub-web.flutter-io.cn/packages/flutter_isolate
不要过度使用 isolates
如果你在每个最小的操作中都使用 isolates,你的应用程序可能会非常卡顿。
这是因为生成一个 isolates 并不是一项廉价的操作。它需要时间和资源。
释放你不用的内存数据
比如你载入一个图片数据进行加工,如加文字、加二维码,不用的时候请释放。
压缩数据处理
为了节省内存,请压缩您的数据。
比如你载入了百兆的 json 文件,你可以压缩起来放在内存中。
final response = await rootBundle.loadString('assets/en_us.json');
final original = utf8.encode(response);
final compressed = gzip.encode(original);
final decompress = gzip.decode(compressed);
final enUS = utf8.decode(decompress);
保持 Flutter 新稳定版本
在每个版本中,Flutter都变得越来越快。
所以不要忘记及时更新你的Flutter版本,并继续创作出令人惊艳的作品!
注意用稳定版。
https://docs.flutter.dev/release/archive?tab=macos
请多准备几台真机调试
始终在真实设备上测试您的应用程序性能,包括较旧的型号,以便发现在模拟器或较新设备上可能不明显的性能问题。
使用StatelessWidget而不是StatefulWidget
一个 StatelessWidget
比一个 StatefulWidget
更快,因为它不需要像其名称所暗示的那样管理状态。
所以如果可能的话,你应该优先选择它。
不要使用OpacityWidget
Opacity
Widget在与动画一起使用时可能会导致性能问题,因为 Opacity
Widget的所有子Widget都会在每个新帧中重新构建。在这种情况下,最好使用 AnimatedOpacity
。如果您想要淡入一张图片,请使用FadeInImageWidget。如果您想要具有不透明度的颜色,请绘制具有不透明度的颜色。
//不推荐
Opacity(opacity: 0.5, child: Container(color: Colors.red))
//推荐
Container(color: Color.fromRGBO(255, 0, 0, 0.5))
使用SizedBox而不是Container
一个 Container
Widget非常灵活。例如,您可以自定义填充或边框,而无需将其嵌套在另一个Widget中。但是,如果您只需要一个具有特定高度和宽度的框,最好使用 SizedBox
Widget。它可以被设置为const,而 Container
则不行。
在 Row/Column,
中添加空格时,更倾向于使用 SizedBox
而不是 Container
。
build(BuildContext context) {
return Column(
children: [
Text(header),
const SizedBox(height: 10),
Text(subheader),
Text(content)
]
);
}
Widget
不要用 Clip
Clip是一项非常昂贵的操作,当你的应用程序变慢时应该避免使用。如果Clip行为设置为 Clip.antiAliasWithSaveLayer
,它的代价会更高。尝试找到其他不需要Clip的方法来实现你的目标。例如,可以使用 borderRadius
属性来创建带有圆角边框的矩形,而不是使用Clip。
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50),
image: DecorationImage(
image: NetworkImage('https://example.com/image.jpg'),
fit: BoxFit.cover,
),
),
)
使用Offstage
OffstageWidget允许您隐藏一个Widget,而不需要从Widget树中移除它。这对于提高性能很有用,因为框架不需要重新构建隐藏的Widget。
Offstage(
offstage: !showWidget,
child: MyWidget(),
)
在Flutter中, Offstage
Widget用于在布局中隐藏子Widget,同时仍然是树的一部分。它可以用于有条件地显示或隐藏子Widget,而无需重新构建整个树。
Opacity
Widget用于控制子Widget的透明度。它接受一个介于0.0和1.0之间的值,其中0.0表示完全透明,1.0表示完全不透明。然而,重要的是要注意它可能会影响性能,所以只在必要时使用。
Visibility
Widget用于控制子Widget的可见性。它可以用于有条件地显示或隐藏子Widget,而无需重新构建整个树。
所有三个Widget都用于控制子Widget的显示,但它们的方式不同。
Offstage控制布局,Opacity控制透明度,Visibility控制可见性。
使用 addPostFrameCallback
在某些情况下,我们需要在帧渲染后执行某些操作。不要尝试使用任何延迟函数,也不要创建自定义回调!我们可以使用 WidgetsBinding.instance.addPostFrameCallback
方法来实现。这个回调将在帧渲染后被调用,并通过避免不必要的重建来提高性能。
WidgetsBinding.instance.addPostFrameCallback((_) {
// ...
});
使用 AutomaticKeepAliveClientMixin
当使用 ListView
或 GridView
时,子部件可以被多次构建。为了避免这种情况,我们可以使用 AutomaticKeepAliveClientMixin
来处理子部件。这将保持子部件的状态并提高性能。
class MyChildWidget extends StatefulWidget {
_MyChildWidgetState createState() => _MyChildWidgetState();
}
class _MyChildWidgetState extends State<MyChildWidget> with AutomaticKeepAliveClientMixin {
bool get wantKeepAlive => true;
Widget build(BuildContext context) {
return Text("I am a child widget");
}
}
在这个例子中, MyChildWidget
类使用 AutomaticKeepAliveClientMixin
混入,并且 wantKeepAlive
属性被设置为 true
。这将保持 MyChildWidget
的状态,并防止它被多次重建,从而提高性能。
避免使用 MediaQuery.of(context).size
当你在Flutter中使用MediaQuery.of(context).size
时,Flutter会将你的小部件与MediaQuery
的大小相关联。这意味着每次调用MediaQuery.of(context).size
时,Flutter会检测MediaQuery
的大小是否发生变化,从而可能导致不必要的重建(rebuilds)。
使用MediaQuery.sizeOf(context)
来避免这些不必要的重建,从而提高应用程序的响应性。通过使用MediaQuery.sizeOf(context)
,你可以绕过与MediaQuery
大小相关的重建过程,从而减少不必要的性能开销。
类似的优化方法也适用于其他MediaQuery
方法。举例来说,建议使用MediaQuery.platformBrightnessOf(context)
而不是MediaQuery.of(context).platformBrightness
,以避免不必要的重建,从而提高应用的响应性。
不要在调试模式下测量性能
一个用于性能和内存测量的特殊模式,即Profile模式。您可以通过Android Studio或Visual Studio Code等IDE运行它,也可以通过执行以下CLI命令来运行:
flutter run -profile
不要在模拟器中测量性能
多用真机性能调试
小结
优化Flutter应用的性能对于提供无缝的用户体验至关重要。通过实施这些提示,您可以进一步优化Flutter应用的性能。请记住,性能优化是一个持续的过程,定期进行分析和测试是确保应用程序保持高性能标准的关键。
感谢阅读本文
如果有什么建议,请在评论中让我知道。我很乐意改进。
© 猫哥
ducafecat.com
end