Flutter异步编程
Isolates
许多流行的编程语言(如Java和C#)都具有非常广泛的API来处理多线程和并行计算。由于它们能够解决复杂的数据竞争问题,因此它们可以处理复杂的多线程场景。然而,Dart不具备以下任何一项:
- 无法启动多个线程进行繁重的后台计算;
- 例如,没有线程安全类型(如 AtomicInteger)的等效项;
- 没有互斥锁、信号量或其他类来防止数据竞争以及所有这些由多线程编程引起的问题;
- 线程之间共享内存,多个不同线程可以可以对同一块内存进行读写,可能就引起数据竞争问题;
Isolate可以理解为特殊的线程,不同Isolate之间不进行内存共享,所以Dart不需要数据同步原语来防止数据竞争问题的发生。每个Isolate都有自己的内存和自己的事件循环。事件循环按照事件添加到事件队列的顺序处理事件。在Main Isolate(Flutter的UI Isolate)中,这些事件可以是任何内容,从处理用户在 UI中的点击,到执行函数,再到在屏幕上绘制框架。下图显示了一个示例事件队列,其中有3个事件等待处理。
如果某个进程无法在帧间隙(即两帧之间的时间)内完成,则最好将工作转移给另一个Isolate,以确保主隔离区每秒可以生成60帧。在 Dart中生Isolate时,它可以与主Isolate同时处理工作,而不会阻塞主Isolate。有时,应用需要执行异常大的计算,从网络请求数据,从本地文件读取数据,从本地数据库读取数据,这可能会导致“UI卡顿”(动作不流畅)。如果您的应用因此而出现卡顿,您可以将这些相关的操作移至辅助新的Isolate。这允许底层运行时环境与主UI隔离的工作同时运行计算,并充分利用多核设备。充分利用多核(CPU),也就是多个线程同时运行,并发运行。之前做Android原生开发的时候,原理也是相通的,上面相关的操作都需要再worker线程进行处理,然后再把数据发送到UI线程。
例如,请考虑以下代码它从文件加载大型JSON文件,并将该JSON转换为自定义Dart对象。如果JSON解码过程未卸载到新的隔离区,则此方法会导致UI在几秒钟内无响应。await Isolate.run方法内部记得用return语句返回。
// Produces a list of 211,640 photo objects.
// (The JSON file is ~20MB.)
Future<List<Photo>> getPhotos() async {
final String jsonString = await rootBundle.loadString('assets/photos.json');
final List<Photo> photos = await Isolate.run<List<Photo>>(() {
final List<Object?> photoData = jsonDecode(jsonString) as List<Object?>;
return photoData.cast<Map<String, Object?>>().map(Photo.fromJson).toList();
});
return photos;
}
futures, async, await
异步编程是为了在后台执行耗时的操作,以便我们同时可以做其他事情。一个Future<T> 表示将来可用的值或错误。每当您使用耗时的函数并在相当长的时间后返回结果时,都应使用此泛型类。以下是您可以轻松精简执行流程的方法:
Future<String> createOrderMessage() async {
var order = await fetchUserOrder();
return 'Your order is: $order';
}
Future<String> fetchUserOrder() async =>
// Imagine that this function is more complex and slow.
Future.delayed(
const Duration(seconds: 2),
() => 'Large Latte',
);
void main() async{
print(await createOrderMessage());
}
注意以下几点就可以:
- 异步方法返回值是Future<T>,方法体之前用async限定。
- 异步方法调用之前需要添加await关键字。
- 方法内部语句调用异步方法,相应的方法体之前也需要async限定。
- 如果一个异步方法没有返回值,则需要限定为Future<void>。
- Future可以处于以下两种状态之一:未完成或已完成。当您调用返回Future的函数时,该函数会将要完成的工作排队并返回未完成的Future。当Future的操作完成时,Future会返回值或返回错误。
- async和await必须是你的首选;它们大大减少了冗长的代码,使代码看起来几乎与同步代码相同。同时try-catch也会优于catchError,是更好地处理异常的方法。
最佳实践代码如下:
Future example() async {
try {
final String data = await httpGetRequest();
final String other = await anotherRequest(data);
return other;
} on Something catch (e) {
print(e.message);
return "fail";
}
}
这样就避免了回调地狱的问题,异步操作的代码像是同步代码一样优雅,这样使代码更加易读,更加易维护,更加易理解。
异步操作可让您的程序在等待其他操作完成的同时完成工作。以下是一些常见的异步操作:
- 通过网络获取数据。
- 读写数据库。
- 从文件读取数据或者向文件写数据。
这里面需要注意的点。同步操作:同步操作会阻止其他操作执行,直到它完成为止。同步函数:同步函数仅执行同步操作。异步操作:异步操作一旦启动,就会允许其他操作在其完成之前执行。异步函数:异步函数至少执行一个异步操作,也可以执行同步操作。
Streams
Streams提供异步数据序列。数据序列包括用户生成的事件和从文件读取的数据。您可以使用Stream API 中的await for或listen()来处理Streams。同步数据序列用Iterable<T>表示,异步数据序列用Stream<T>表示。示例代码如下:
Future<int> sumStream(Stream<int> stream) async {
var sum = 0;
try {
await for (final value in stream) {
sum += value;
}
} catch (e) {
return -1;
}
return sum;
}
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
if (i == 4) {
throw Exception('Intentional exception');
} else {
yield i;
}
}
}
Iterable<int> makeStream() sync* {
print('making a stream');
for (int i = 0; i < 10; i++) {
yield 25;
}
}
void main() async {
var stream = countStream(10);
var sum = await sumStream(stream);
print(sum); // -1
final streams = makeStream();
print('printing and using the streams');
for(var v in streams) {
print(v);
}
}
注意如下几点:
-
比如countStream方法,Stream<T>是返回类型。返回类型是Stream<T>的方法体前面需要加async*修饰。向Stream流里添加数据调用yield,同时方法没有return语句。
-
比如方法sumStream,遍历Stream<T>的数据使用await for。
-
比如说makeStream方法,Iterable<int>是发挥的数据类型,sync*代表是同步数据,同时也用yield发送数据,方法也没有return语句,遍历(消费)Iterable<int>使用for循环遍历即可。不能使用await。
{Function? onError, void Function()? onDone, bool? cancelOnError});
Stream的listen()方法允许您开始监听流。在此之前,Stream是一个惰性对象,描述您想要看到的事件。当您监听时,将返回一个 StreamSubscription 对象,该对象表示生成事件的活动流。这类似于Iterable只是一个对象集合,但迭代器是执行实际迭代的那个。 Stream订阅允许您暂停订阅、在暂停后恢复订阅以及完全取消订阅。您可以设置要为每个数据事件或错误事件以及流关闭时调用的回调。
总结
多线程是软件开发过程中不能忽视的一个方面。因为Isolate没有内存和数据共享的问题,所以没有数据竞争的烦恼和数据同步的需要。防止UI卡顿,尤其是UI冻结都是软件开发中需要考虑的问题,耗时或者CPU密集型的长时间任务就需要新建Isolate来执行,来防止页面卡顿。Isolate之间数据传输也是需要考虑的问题。异步方法的调用推荐使用Future<T> await async的组合,异步的方法调用会像同步方法调用一样简洁高效,避免回调地狱。Future<T>可以理解为一个异步数据,而Stream<T>相当于一个异步队列,也就是Future<T>的一个队列。希望文章对您有帮助,如果文中有问题,希望您不吝指教。
转载自:https://juejin.cn/post/7394854112511623187