Flutter 性能优化:从乌龟到涡轮增压的兔子
有没有觉得 Flutter 是比赛中的乌龟?别担心!只要我们有几招,我们就能把乌龟变成涡轮增压的兔子。准备好放大了吗?让我们深入了解一些 Flutter 性能技巧!
Flutter 应用程序在默认情况下是高性能的,因此您只需要避免常见的陷阱即可获得出色的性能。如何设计和实现应用的 UI 会对应用的运行效率产生很大影响。
这些最佳实践建议将帮助您编写最高性能的 Flutter 应用程序。同时,在开发过程中使用像 appuploader 这样的 iOS 开发助手工具,可以更高效地管理应用构建和上传流程,让您更专注于性能优化本身。
1.使用 Clean Architecture(干净架构)
Clean Architecture 是一种软件设计模式,强调关注点分离和独立测试。此模式鼓励将应用程序逻辑分离到不同的层中,每一层负责一组特定的任务。Clean Architecture 非常适合大型应用程序,因为它提供了清晰的关注点分离,并使测试更容易。
以下是 Flutter 中的一个 Clean Architecture 实现示例:
dart
lib/
data/
models/
user_model.dart
repositories/
user_repository.dart
domain/
entities/
user.dart
repositories/
user_repository_interface.dart
usecases/
get_users.dart
presentation/
pages/
users_page.dart
widgets/
user_item.dart
main.dart
2.使用良好的状态管理
状态管理在 Flutter 应用性能中起着至关重要的作用。根据应用的复杂性选择正确的状态管理方法。对于中小型应用程序,内置的 setState
方法可能就足够了。但是,对于更大更复杂的应用程序,可以考虑使用状态管理库,如 bloc 或 riverpod。
dart
// Bad Approach
setState(() {
// Updating a large data structure unnecessarily
myList.add(newItem);
});
// Better Approach
final myListBloc = BlocProvider.of<MyListBloc>(context);
myListBloc.add(newItem);
3.使用代码分析工具提高代码质量
代码分析工具,如 Flutter Analyzer 和 Lint,对于提高代码质量和降低 bug 和错误的风险非常有帮助。这些工具可以帮助在潜在问题成为问题之前识别它们,还可以提供改进代码结构和可读性的建议。
以下是在 Flutter 中使用 Flutter Analyzer 的示例:
shell
flutter analyze lib/
4.使用自动化测试提高代码可靠性
自动化测试是构建大型应用程序的重要组成部分,因为它有助于确保代码可靠并按预期执行。Flutter 为自动化测试提供了几个选项,包括单元测试、小部件测试和集成测试。
下面是使用 Flutter Test 包进行自动化测试的示例:
dart
void main() {
test('UserRepository returns a list of users', () {
final userRepository = UserRepository();
final result = userRepository.getUsers();
expect(result, isInstanceOf<List<User>>());
});
}
5.使用 Flutter Inspector 进行调试
Flutter Inspector 是一个强大的工具,用于调试 Flutter 应用程序。它允许开发人员检查和操作小部件树,查看性能指标等。Flutter Inspector 可以通过 Flutter DevTools 浏览器扩展或通过命令行访问。
下面是使用 Flutter Inspector 进行调试的示例:
shell
flutter run --debug
6.延迟加载和分页
一次获取和呈现大量数据会显著影响性能。实现延迟加载和分页以根据需要加载数据,特别是对于长列表或数据密集型视图。
dart
// Bad Approach
// Fetch and load all items at once.
List<Item> allItems = fetchAllItems();
// Better Approach
// Implement lazy loading and pagination.
List<Item> loadItems(int pageNumber) {
// Fetch and return data for the specific page number.
}
// Use a ListView builder with lazy loading.
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();
}
},
);
},
);
7.缩小图像大小
大型图像文件可能会降低应用的性能,尤其是在加载多个图像时。压缩和调整图像大小,以减少其文件大小,而不会影响太多的质量。
dart
import 'package:flutter_image_compress/flutter_image_compress.dart';
// Original image file
var imageFile = File('path/to/original/image.png');
// Get the image data
var imageBytes = await imageFile.readAsBytes();
// Resize and compress the image
var compressedBytes = await FlutterImageCompress.compressWithList(
imageBytes,
minHeight: 200,
minWidth: 200,
quality: 85,
);
// Save the compressed image to a new file
var compressedImageFile = File('path/to/compressed/image.png');
await compressedImageFile.writeAsBytes(compressedBytes);
8.优化动画
避免使用可能影响应用性能的繁重或复杂的动画,尤其是在较旧的设备上。明智地使用动画,并考虑使用 Flutter 的内置动画,如 AnimatedContainer
、AnimatedOpacity
等。
dart
// Bad Approach
// Using an expensive animation
AnimatedContainer(
duration: Duration(seconds: 1),
height: _isExpanded ? 300 : 1000,
color: Colors.blue,
);
// Better Approach
// Using a simple and efficient animation
AnimatedContainer(
duration: Duration(milliseconds: 500),
height: _isExpanded ? 300 : 100,
color: Colors.blue,
);
9.优化应用程序启动时间
通过优化初始化过程减少应用的启动时间。使用 flutter_native_splash
包在应用加载时显示闪屏,并将非必要组件的初始化延迟到应用启动后。在 iOS 开发中,使用 appuploader 可以更高效地管理启动屏幕和其他启动优化配置。
10.避免使用深树,而是创建一个单独的小部件
你不想继续滚动你的 IDE 与一千行代码。尝试创建一个单独的小部件。它将看起来干净,易于重构。
dart
//Bad
Column(
children: [
Container(
//some lengthy code here
),
Container(
//some another lengthy code
),
//some another lengthy code
],
)
//Good
Column(
children: [
FirstLengthyCodeWidget(),
SecondLengthyCodeWidget(),
//another lengthy code widget etc
],
)
11.使用级联(..)
如果你刚开始使用 flutter,你可能还没有使用这个操作符,但是当你想在同一个对象上执行一些任务时,它非常有用。
dart
//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;
12.使用展开运算符(...)
这是 dart 提供的另一个漂亮的操作符。您可以简单地使用此操作符执行许多任务,例如 if-else、加入列表等。
dart
//Bad
@override
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(),
],
),
);
}
//Good
@override
Widget build(BuildContext context) {
bool isTrue = true;
return Scaffold(
body: Column(
children: [
if(isTrue)...[
const Text('One'),
const Text('Two'),
const Text('Three')
]
],
),
);
}
13.避免使用硬编码的样式、装饰等
如果您在应用程序中使用硬编码的样式、装饰等,并且稍后决定更改这些样式。你会一个接一个地修复它们。
dart
//Bad
Column(
children: const [
Text(
'One',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
Text(
'Two',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
)
//Good
Column(
children: [
Text(
'One',
style: Theme.of(context).textTheme.subtitle1,
),
Text(
'Two',
style: Theme.of(context).textTheme.subtitle1,
),
],
),
14.小心使用 build()
避免使用过大的单个小部件和较大的 build()
函数。根据封装以及它们的变化方式将它们拆分为不同的小部件。
当在 State 对象上调用 setState()
时,所有派生小部件都会重新生成。因此,将 setState()
调用本地化到 UI 实际需要更改的子树部分。如果更改只包含在树的一小部分中,请避免在树的较高位置调用 setState()
。
让我们看看这个例子,我们希望当用户按下图标时,只有这个图标的颜色会改变。
因此,如果我们在一个小部件中拥有所有这些 UI,当按下图标时,它将更新整个 UI。我们可以做的是将图标分隔为 StatefulWidget
。
之前
dart
import 'package:flutter/material.dart';
class FidgetWidget extends StatefulWidget {
const FidgetWidget({Key? key}) : super(key: key);
@override
_FidgetWidgetState createState() => _FidgetWidgetState();
}
class _FidgetWidgetState extends State<FidgetWidget> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('App Title'),
),
body: Column(
children: [
Text('Some Text'),
IconButton(
onPressed: () => setState(() {
// Some state change here
}),
icon: Icon(Icons.favorite),
),
],
),
);
}
}
之后
dart
import 'package:flutter/material.dart';
class MyIconWidget extends StatefulWidget {
const MyIconWidget({Key? key}) : super(key: key);
@override
_MyIconWidgetState createState() => _MyIconWidgetState();
}
class _MyIconWidgetState extends State<MyIconWidget> {
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: () => setState(() {
}),
icon: Icon(Icons.favorite),
);
}
}
15.使用 Widgets 而不是函数
您可以节省 CPU 周期,并与构造函数一起使用,在需要时进行重建,还有更多好处(重复使用等......)。
dart
//Bad
@override
Widget build(BuildContext context) {
return Column(
children: [
_getHeader(),
_getSubHeader(),
_getContent()
]
);
}
//Good
@override
Widget build(BuildContext context) {
return Column(
children: [
HeaderWidget(),
SubHeaderWidget(),
ContentWidget()
]
);
}
正如 Riverpod、Provider 和其他软件包的创建者 Remi Rousselet 所说。"类有更好的默认行为。使用方法的唯一好处就是可以少写一点代码。没有任何功能上的好处"。
16.尽可能使用 final
使用 final
关键字可以大大提高应用的性能。当一个值被声明为 final
时,它只能被设置一次,此后不会改变。这意味着框架不需要经常检查更改,从而提高了性能。
dart
final items = ["Item 1", "Item 2", "Item 3"];
在这个例子中,变量项被声明为 final
,这意味着它的值不能被改变。这提高了性能,因为框架不需要检查此变量的更改。
17.尽可能使用 const
dart
x = Container();
y = Container();
x == y // false
x = const Container();
y = const Container();
x == y // true
const widget
在编译时创建,因此在运行时速度更快。
18.尽可能使用 const 构造函数
dart
class CustomWidget extends StatelessWidget {
const CustomWidget();
@override
Widget build(BuildContext context) {
...
}
}
当构建自己的小部件或使用 Flutter 小部件时。这有助于 Flutter 只重建应该更新的小部件。
19.尽可能使用私有变量/方法
除非必要,否则尽可能使用 private
关键字。
dart
//Bad
class Student {
String name;
String address;
Student({
required this.name,
required this.address,
});
}
}
//Good
class Student{
String _name;
String _address;
Student({
required String name,
required String address,
}):
_name = name,
_address = address;
}
是的,与性能相比,它更像是 Dart 的最佳实践。但是,最佳实践可以在某种程度上提高性能,比如理解代码、降低复杂性等。
20.使用 nil 代替 const Container()
dart
// good
text != null ? Text(text) : const Container()
// Better
text != null ? Text(text) : const SizedBox()
// BEST
text != null ? Text(text) : nil
or
if (text != null) Text(text)
它只是一个基本的元素小部件,几乎没有成本。
21.在 ListView 中对长列表使用 itemExtent
这有助于 Flutter 计算 ListView
滚动位置,而不是计算每个小部件的高度,并使滚动动画更具性能。
默认情况下,每个孩子都必须确定其范围,这在性能方面是相当昂贵的。显式设置该值可以节省大量 CPU 周期。列表越长,使用此属性获得的速度就越快。
dart
//Nope
final List<int> _listItems = <int>[1, 2, 3, 4, 5, 6, 7, 8, 9];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _listItems.length,
itemBuilder: (context, index) {
var item = _listItems[index];
return Center(
child: Text(item.toString())
);
}
}
//Good
final List<int> _listItems = <int>[1, 2, 3, 4, 5, 6, 7, 8, 9];
@override
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())
);
}
}
22.避免将 AnimationController 与 setState 一起使用
这不仅会导致重新构建整个 UI 的动画部件,而且会使动画滞后。
dart
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(),
),
],
),
到
dart
void initState() {
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
);
// No addListener(...)
}
AnimatedBuilder(
animation: _controller,
builder: (_, child) {
return Transform.translate(
offset: Offset(100 * _controller.value, 0),
child: child,
);
},
child: Placeholder(),
),
23.使用 Keys 加速 Flutter 性能
使用 Keys 时,Flutter 能更好地识别小部件。这让我们的性能提高了 4 倍。
dart
// FROM
return value ? const SizedBox() : const Placeholder(),
// TO
return value ? const SizedBox(key: ValueKey('SizedBox')) : const Placeholder(key: ValueKey('Placeholder')),
----------------------------------------------
// FROM
final inner = SizedBox();
return value ? SizedBox(child: inner) : inner,
// TO
final global = GlobalKey();
final inner = SizedBox(key: global);
return value ? SizedBox(child: inner) : inner,
谨慎: ValueKey 会让你的代码看起来有点臃肿 GlobalKey 有点危险,但有时候值得。
24.使用图像 ListView 时优化内存
dart
ListView.builder(
...
addAutomaticKeepAlives: false (true by default)
addRepaintBoundaries: false (true by default)
);
ListView
无法杀死屏幕上不可见的子代。如果子代具有高分辨率图像,就会消耗大量内存。
通过将这些选项设置为 false,可能会导致使用更多的 GPU 和 CPU 工作,但它可以解决我们的内存问题,并且您将获得非常高性能的视图,而不会出现明显的问题。
25.使用 for/while 代替 foreach/map
如果你要处理大量的数据,使用正确的循环可能会影响你的性能。
26.预缓存您的图像和图标
这取决于具体情况,但我一般会在主系统中预先缓存所有图像。
对于图片
你不需要任何包装,只要用-
dart
precacheImage(
AssetImage(imagePath),
context
);
对于 SVG
您需要 flutter_svg
包
dart
precachePicture(
ExactAssetPicture(SvgPicture.svgStringDecoderBuilder, iconPath),
context
);
27.使用 SKSL 预热
shell
flutter run --profile --cache-sksl --purge-persistent-cache
flutter build apk --cache-sksl --purge-persistent-cache
如果一个应用程序在第一次运行时有不稳定的动画,然后在相同的动画中变得平滑,那么它很可能是由于着色器编译的不稳定。
28.考虑使用 RepaintBoundary
此小部件为其子小部件创建单独的显示列表,这可以提高特定情况下的性能。
29.如果可能,请使用生成器命名的构造函数
dart
Listview.builder()
生成器只会在屏幕上渲染显示的项目。即使看不到,也能显示所有的孩子。
30.不要使用 ShrinkWrap 任何可滚动的小部件
测量内容问题。
31.使用重载函数时使用 ISOLATE (隔离)
有些方法非常昂贵,比如图像处理,它们可能会让应用程序在主