👨🏻‍💻's 博客

慢品人间烟火色,闲观万事岁月长

0%

Flutter - UIKit开发相关指南 - 线程和异步

线程和异步

编写异步代码

Dart采用单线程执行模型,支持Isolates(在另一个线程上运行Dart代码)、事件循环和异步编程。除非生成一个Isolates,否则Dart代码将在主UI线程中运行,并由事件循环驱动。Flutter的事件循环相当于iOS的主线程上的RunLoop。

Dart的单线程模型,不代表阻塞型的操作都会导致UI卡顿。实际上可以采用Dart语言提供的异步功能比如async/await来执行异步的操作。

因为要请求网络,所以添加http模块

1
$ fltter pub add http
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
64
65
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

void main() {
runApp(const MainApp());
}

class MainApp extends StatelessWidget {
const MainApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(home: ThreadSample());
}
}

class ThreadSample extends StatefulWidget {
const ThreadSample({super.key});

@override
State<ThreadSample> createState() => _ThreadSampleState();
}

class _ThreadSampleState extends State<ThreadSample> {
List<Map<String, Object?>> data = [];
@override
/// 1. 初始化_ThreadSampleState Widget的状态
void initState() {
super.initState();
/// 2.加载数据
loadData();
}

Future<void> loadData() async {
/// 3. 发起异步请求
final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final http.Response response = await http.get(dataURL);
/// 4. 等响应结束后调用setState() 更新data 触发build方法
setState(() {
data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();
});
}

Widget getRow(int index) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text('Row ${data[index]['title']}'),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('线程与异步示例')),
// 5. 显示列表,长度为data.length,内容通过getRow方法返回data的子元素
body: ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return getRow(index);
},
),
);
}
}

2025-05-12 16.39.30.png

切到后台线程

因为Flutter是单线程模型,不需要考虑线程管理相关的问题。在执行I/O密集型的操作时,比如访问磁盘或网络,可以使用async/await,但是当在执行CPU计算密集型的操作时,则应该将其移到独立线程(Isolate)以避免阻塞事件循环。

Isolates 是独立的执行线程,它们与主线程内存堆不共享任何内存。这意味着你无法访问主线程中的变量,或通过调用 setState() 来更新用户界面。

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
runApp(const SampleApp());
}

class SampleApp extends StatelessWidget {
const SampleApp({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Sample App', home: SampleAppPage());
}
}

class SampleAppPage extends StatefulWidget {
const SampleAppPage({super.key});

@override
State<SampleAppPage> createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
List<Map<String, Object?>> data = [];

@override
void initState() {
super.initState();

/// 主1. 加载数据
loadData();
}

bool get showLoadingDialog => data.isEmpty;

Future<void> loadData() async {
/// Opens a long-lived port for receiving messages.
/// 打开端口用于接收数据
final ReceivePort receivePort = ReceivePort();

/// 主2.Isolate开启子线程
/// The [entryPoint] function must be able to be called with a single
/// argument, that is, a function which accepts at least one positional
/// parameter and has at most one required positional parameter.
///
/// The entry-point function is invoked in the new isolate with [message]
/// as the only argument.
/// 第一个参数:至少包含一个参数的函数指针,这里关联的是dataLoader,参数是SendPort
///
/// [message] must be sendable between isolates. Objects that cannot be sent
/// include open files and sockets (see [SendPort.send] for details). Usually
/// the initial [message] contains a [SendPort] so that the spawner and
/// spawnee can communicate with each other.
/// 第二个参数: 不同Isolate之间传递的数据,通常初始化时传的message包含一个SendPort
///
/// receivePort.sendPort
/// [SendPort]s are created from [ReceivePort]s.
/// Any message sent through a [SendPort] is delivered to its corresponding [ReceivePort].
/// There might be many [SendPort]s for the same [ReceivePort].
/// 通过SendPort发送的消息会传送给关联的ReceivePort
await Isolate.spawn(dataLoader, receivePort.sendPort);

/// 主3. first是一个Future,它会在接收到第一个消息时完成
/// 一旦收到第一个消息,它就会关闭ReceivePort,并且不再监听其它消息
/// 适用于只接收单个消息的情况
final SendPort sendPort = await receivePort.first as SendPort;
try {
/// 主4. 使用await调用sendReceive
final List<Map<String, dynamic>> msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
);

/// 主5.设置数据,通知Flutter刷新UI
setState(() {
data = msg;
});
} catch (e) {
print('Error in loadData:$e');
}
}

// 子1. 执行子线程上的函数
static Future<void> dataLoader(SendPort sendPort) async {
// 子2.打开端口接收数据
final ReceivePort port = ReceivePort();

/// 子3. 发送自己的接收端口
sendPort.send(port.sendPort);

/// 子4:等待消息
await for (final dynamic msg in port) {

/// 子5: 接收到url + 主线程的接收端口
final String url = msg[0] as String;
final SendPort replyTo = msg[1] as SendPort;

/// 子6: 发起网络请求
final Uri dataURL = Uri.parse(url);
final http.Response response = await http.get(dataURL);

/// 下面这种写法在sendReceive会报
/// Unhandled
/// Exception: type 'Future<dynamic>' is not a subtype of type
/// 'Future<List<Map<String, dynamic>>>'
///
/// replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);
/// 因为Dart在运行时无法检查Future<T>中的T,直接转换Future的泛型参数会失败
/// 强制类型转换
final data = jsonDecode(response.body) as List;
final typedata = data.cast<Map<String, dynamic>>();

/// 子7: 将网络请求的结果发送到主线程
replyTo.send(typedata);
}
}

Future<dynamic> sendReceive(SendPort port, String msg) {
// 主5.创建接收数据的端口
final ReceivePort response = ReceivePort();
// Sends an asynchronous [message] through this send port, to its corresponding [ReceivePort].
// 主6. 主线程异步发送url + 通知其它线程接收端口
port.send(<dynamic>[msg, response.sendPort]);
return response.first;
}

Widget getBody() {
/// 数据为空显示进度条
bool showLoadingDialog = data.isEmpty;

if (showLoadingDialog) {
return getProgressDialog();
} else {
return getListView();
}
}

Widget getProgressDialog() {
return const Center(child: CircularProgressIndicator());
}

ListView getListView() {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, position) {
return getRow(position);
},
);
}

Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text("Row ${data[i]["title"]}"),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: getBody(),
);
}
}

202505131636-w400

错误

[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: ClientException with SocketException: Failed host lookup: 'jsonplaceholder.typicode.com'

[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: ClientException with SocketException: Failed host lookup: ‘jsonplaceholder.typicode.com’ (OS Error: nodename nor servname provided, or not known, errno = 8), uri=https://jsonplaceholder.typicode.com/posts

首次启动需要同意网络权限,看报错是DNS找不到域名,所以还是网络问题,在手机上授权后再重新用flutter运行工程能恢复

参考

1.给 UIKit 开发者的 Flutter 指南
2.flutter 中 ReceivePort 的 first 和 listen