Redian新闻
>
Flutter异步编程指南

Flutter异步编程指南

公众号新闻
来源 | OSCHINA 社区

作者 | 京东云开发者-京东物流 王志明

原文链接:https://my.oschina.net/u/4090830/blog/8611649

1 Dart 中的事件循环模型

在 App 开发中,经常会遇到处理异步任务的场景,如网络请求、读写文件等。Android、iOS 使用的是多线程,而在 Flutter 中为单线程事件循环,如下图所示
Dart 中有两个任务队列,分别为 microtask 队列和 event 队列,队列中的任务按照先进先出的顺序执行,而 microtask 队列的执行优先级高于 event 队列。在 main 方法执行完毕后,会启动事件循环,首先将 microtask 队列中的任务逐个执行完毕,再去执行 event 队列中的任务,每一个 event 队列中的任务在执行完成后,会再去优先执行 microtask 队列中的任务,如此反复,直到清空所有队列,这个过程就是 Dart 事件循环的处理机制。这种机制可以让我们更简单的处理异步任务,不用担心锁的问题。我们可以很容易的预测任务执行的顺序,但无法准确的预测到事件循环何时会处理到你期望执行的任务。例如创建了一个延时任务,但排在前面的任务结束前是不会处理这个延时任务的,也就说这个任务的等待时间可能会大于指定的延迟时间。
Dart 中的方法一旦开始执行就不会被打断,而 event 队列中的事件还来自于用户输入、IO、定时器、绘制等,这意味着在两个队列中都不适合执行计算量过大的任务,才能保证流畅的 UI 绘制和用户事件的快速响应。而且当一个任务的代码发生异常时,只会打断当前任务,后续任务不受影响,程序更不会退出。从上图还可以看出,将一个任务加入 microtask 队列,可以提高任务优先级,但是一般不建议这么做,除非比较紧急的任务并且计算量不大,因为 UI 绘制和处理用户事件是在 event 事件队列中的,滥用 microtask 队列可能会影响用户体验。
总结下 Dart 事件循环的主要概念:
  1. Dart 中有两个队列来执行任务:microtask 队列和 event 队列。

  2. 事件循环在 main 方法执行完毕后启动, microtask 队列中的任务会被优先处理。

  3. microtask 队列只处理来自 Dart 内部的任务,event 队列中有来自 Dart 内部的 Future、Timer、isolate message,还有来自系统的用户输入、IO、UI 绘制等外部事件任务。

  4. Dart 中的方法执行不会被打断,因此两个队列中都不适合用来执行计算量大的任务。

  5. 一个任务中未被处理的异常只会打断当前任务,后续任务不受影响,程序更不会退出。

1.1 向 microtask 队列中添加任务

可以使用顶层方法 scheduleMicrotask 或者 Future.microtask 方法,如下所示:
scheduleMicrotask(() => print('microtask1'));
Future.microtask(() => print('microtask2'));
使用 Future.microtask 的优势在于可以在 then 回调中处理任务返回的结果。

1.2 向 event 队列中添加任务

Future(() => print('event task'));

基于以上理论,通过如下代码可以验证 Dart 的事件循环机制:

void main() {
print('main start');

Future(() => print('event task1'));

Future.microtask(() => print('microtask1'));

Future(() => print('event task1'));

Future.microtask(() => print('microtask2'));

print('main stop');

执行结果:

main start
main stop
microtask1
microtask2
event task1
event task1
通过输出结果可以看到,任务的执行顺序并不是按照编写代码的顺序来的,将任务添加到队列不会立刻执行,而执行顺序也完全符合前面讲的规则,当前 main 方法中的代码执行完毕后,才会去执行队列中的任务,且 microTask 队列的优先级高于 event 队列。

2 Dart 中的异步实现

在 Dart 中通过 Future 来执行异步任务, Future 是对异步任务状态的封装,对任务结果的代理,通过 then 方法可以注册处理任务结果的回调方法。
创建方法 Future 方式:
Future()
Future.delayed()
Future.microtask()
Future.sync()

2.1 Future()

factory Future(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
Timer.run(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
上面是 Future () 的源码,可以看到内部是通过启动一个没有延迟的计时器来添加任务的,实用 try catch 来捕获任务代码中可能出现的异常,我们可以在 catchError 回调中来处理异常。

2.2 Future.delayed()

factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
if (computation == null && !typeAcceptsNull<T>()) {
throw ArgumentError.value(null, "computation", "The type parameter is not nullable");
}
_Future<T> result = new _Future<T>();
new Timer(duration, () {
if (computation == null) {
result._complete(null as T);
} else {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
}
});
return result;
}
Future.delayed () 与 Future () 的区别是通过一个延迟的计时器来添加任务。

2.3 Future.microtask()

factory Future.microtask(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
scheduleMicrotask(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
Future.microtask () 是将任务添加到 microtask 队列,通过这种可以很方便通过 then 方法中的回调来处理任务的结果。

2.4 Future.sync()

factory Future.sync(FutureOr<T> computation()) {
try {
var result = computation();
if (result is Future<T>) {
return result;
} else {
// TODO(40014): Remove cast when type promotion works.
return new _Future<T>.value(result as dynamic);
}
} catch (error, stackTrace) {
var future = new _Future<T>();
AsyncError? replacement = Zone.current.errorCallback(error, stackTrace);
if (replacement != null) {
future._asyncCompleteError(replacement.error, replacement.stackTrace);
} else {
future._asyncCompleteError(error, stackTrace);
}
return future;
}
}
Future.sync () 中的任务会被立即执行,不会添加到任何队列。
在第一个章节中讲到了可以很容易的预测任务的执行顺序,下面我们通过一个例子来验证:
void main() {
print('main start');

Future.microtask(() => print('microtask1'));

Future.delayed(new Duration(seconds:1), () => print('delayed event'));
Future(() => print('event1'));
Future(() => print('event2'));

Future.microtask(() => print('microtask2'));

print('main stop');
}

执行结果:

main start
main stop
microtask1
microtask2
event1
event2
delayed event
因为代码比较简单,通过代码可以很容易的预测到执行结果,下面将复杂度稍微提高。
void main() {
print('main start');

Future.microtask(() => print('microtask1'));

Future.delayed(new Duration(seconds:1), () => print('delayed event'));

Future(() => print('event1'))
.then((_) => print('event1 - callback1'))
.then((_) => print('event1 - callback2'));

Future(() => print('event2')).then((_) {
print('event2 - callback1');
return Future(() => print('event4')).then((_) => print('event4 - callback'));
}).then((_) {
print('event2 - callback2');
Future(() => print('event5')).then((_) => print('event5 - callback'));
}).then((_) {
print('event2 - callback3');
Future.microtask(() => print('microtask3'));
}).then((_) {
print('event2 - callback4');
});

Future(() => print('event3'));

Future.sync(() => print('sync task'));

Future.microtask(() => print('microtask2')).then((_) => print('microtask2 - callbak'));

print('main stop');
}

执行结果:

main start
sync task
main stop

microtask1
microtask2
microtask2 - callbak

event1
event1 - callback1
event1 - callback2

event2
event2 - callback1
event3

event4
event4 - callback

event2 - callback2
event2 - callback3
event2 - callback4

microtask3
event5
event5 - callback

delayed event
看到结果后你可能会疑惑,为什么 event1、event1 - callback1、event1 - callback2 会连续输出,而 event2 - callback1 输出后为什么是 event3,event5、event5 - callback 为什么会在 microtask3 后输出?
这里我们补充下 then 方法的一些关键知识,理解了这些,上面的输出结果也就很好理解了:
  1. then 方法中的回调并不是按照它们注册的顺序来执行。

  2. Future 中的任务执行完毕后会立刻执行 then 方法中的回调,并且回调不会被添加到任何队列中。

  3. 如果 Future 中的任务在 then 方法调用之前已经执行完毕了,那么会有一个任务被加入到 microtask 队列中。这个任务执行的就是被传入 then 方法中的回调。

2.5 catchError、whenComplete

Future(() {
throw 'error';
}).then((_) {
print('success');
}).catchError((error) {
print(error);
}).whenComplete(() {
print('completed');
});
输出结果:
error
completed
通过 catchError 方法注册的回调,可以用来处理任务代码产生的异常。不管 Future 中的任务执行成功与否,whenComplete 方法都会被调用。

2.6 async、await

使用 async、await 能以更简洁的编写异步代码,是 Dart 提供的一个语法糖。使用 async 关键字修饰的方法返回值类型为 Future,在 async 方法内可以使用 await 关键字来修饰异步任务,在方法内部达到同步执行的效果,可以达到简化代码和提高可读性的效果,不过如果想要处理异常,需要实用 try catch 语句来包裹 await 修饰的异步任务。
void main() async {
print(await getData());
}

Future<int> getData() async {
final a = await Future.delayed(Duration(seconds: 1), () => 1);
final b = await Future.delayed(Duration(seconds: 1), () => 1);
return a + b;
}

3 Isolate 介绍

前面讲到耗时任务不适合放到 microtask 队列或 event 队列中执行,会导致 UI 卡顿。那么在 Flutter 中有没有既可以执行耗时任务又不影响 UI 绘制呢,其实是有的,前面提到 microtask 队列和 event 队列是在 main isolate 中运行的,而 isolate 是在线程中运行的,那我们开启一个新的 isolate 就可以了,相当于开启一个新的线程,使用多线程的方式来执行任务,Flutter 也为我们提供了相应的 Api。

3.1 compute

void main() async {
compute<String, String>(
getData,
'Alex',
).then((result) {
print(result);
});
}

String getData(String name) {
// 模拟耗时3秒
sleep(Duration(seconds: 3));
return 'Hello $name';
}
compute 第一个参数是要执行的任务,第二个参数是要向任务发送的消息,需要注意的是第一个参数只支持顶层参数。使用 compute () 可以方便的执行耗时任务,但是滥用的话也会适得其反,因为每次调用,相当于新建一个 isolate。上面的代码执行一个经历了 isolate 的创建以及销毁过程,还有数据的传递会经历两次拷贝,因为 isolate 之间是完全隔离的,不能共享内存,整个过程除去任务本身的执行时间,也会非常的耗时,isolate 的创建也比较消耗内存,创建过多的 isolate 还有 OOM 的风险。这时我们就需要一个更优的解决方案,减少频繁创建销毁 isolate 所带来的消耗,最好是能创建一个类似于线程池的东西,只要提前初始化好,后面就可以随时使用,不用担心会发生前面所讲的问题,这时候 LoadBalancer 就派上用场了

3.2 LoadBalancer

// 用来创建 LoadBalancer
Future<LoadBalancer> loadBalancerCreator = LoadBalancer.create(2, IsolateRunner.spawn);

// 全局可用的 loadBalancer
late LoadBalancer loadBalancer;

void main() async {
// 初始化 LoadBalancer
loadBalancer = await loadBalancerCreator;

// 使用 LoadBalancer 执行任务
final result = await loadBalancer.run<String, String>(getData, 'Alex');
print(result);
}

String getData(String name) {
// 模拟耗时3秒
sleep(Duration(seconds: 3));
return 'Hello $name';
}
使用 LoadBalancer.create() 方法可以创建出一个 isolate 线程池,能够指定 isolate 的数量,并自动实现了负载均衡。应用启动后在合适的时机将其初始化好,后续就有一个全局可用的 LoadBalancer 了。

4 实用经验

4.1 指定任务的执行顺序

在开发中经常会有需要连续执行异步任务的场景,例如下面的例子,后面的一步任务直接需要以来前面任务的结果,所有任务正常执行完毕才算成功。
void main() async {
print(await getData());
}

Future<int> getData() {
final completer = Completer<int>();
int value = 0;

Future(() {
return 1;
}).then((result1) {
value += result1;
return Future(() {
return 2;
}).then((result2) {
value += result2;
return Future(() {
return 3;
}).then((result3) {
value += result3;
completer.complete(value);
});
});
});

return completer.future;
}
这种方式出现了回调地狱,代码非常难以阅读,实际开发中还会有处理异常的代码,会显得更加臃肿,编写难度也大,显然这种方式是不建议使用的。

4.2 使用 then 的链式调用

void main() async {
print(await getData());
}

Future<int> getData() {
int value = 0;
return Future(() => 1).then((result1) {
value += result1;
return Future(() => 2);
}).then((result2) {
value += result2;
return Future(() => 3);
}).then((result3) {
value += result3;
return value;
});
}
回调地狱的问题解决了,代码可读性提高很多。

4.3 使用 async、await

void main() async {
print(await getData());
}


Future<int> getData() async {
int value = 0;

value += await Future(() => 1);
value += await Future(() => 2);
value += await Future(() => 3);

return value;
}
效果显而易见,代码更加清晰了。

4.4 取消任务

在前面讲到了 Dart 方法执行时是不能被中断的,这就意味着一个 Future 任务开始后必然会走到完成的状态,但是很多时候我们需要又取消一个异步任务,唯一的办法就是在任务结束后不执行回调代码,就可以实现类似取消的效果。

4.5 CancelableOperation

在 Flutter 的 async 包中,提供了一个 CancelableOperation 给我们使用,使用它可以很简单的实现取消任务的需求。
void main() async {
// 创建一个可以取消的任务
final cancelableOperation = CancelableOperation.fromFuture(
Future(() async {
print('start');
await Future.delayed(Duration(seconds: 3)); // 模拟耗时3秒
print('end');
}),
onCancel: () => print('cancel...'),
);

// 注册任务结束后的回调
cancelableOperation.value.then((val) {
print('finished');
});

// 模拟1秒后取消任务
Future.delayed(Duration(seconds: 1)).then((_) => cancelableOperation.cancel());
}
CancelableOperation 是对 Future 的代理, 对 Future 的 then 进行了接管,判断 isCanceled 标记决定是否需要执行用户提供的回调。

往期推荐



16款开源的全文搜索引擎

中兴新支点OS桌面环境正式开源,仅104M,速度提升20%

Slint 1.0正式发布,Rust编写的原生GUI工具包



🌟 活动推荐


2023 年 5 月 27-28 日,GOTC 2023 全球开源技术峰会将在上海张江科学会堂隆重举行。

为期 2 天的开源行业盛会,将以行业展览、主题发言、特别论坛、分论坛、快闪演讲的形式来诠释此次大会主题 ——“Open Source, Into the Future”。与会者将一起探讨元宇宙、3D 与游戏、eBPF、Web3.0、区块链等热门技术主题,以及 OSPO、汽车软件、AIGC、开源教育培训、云原生、信创等热门话题,探讨开源未来,助力开源发展。

长按识别下方二维码立即查看 GOTC 2023 详情/报名。

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
小美女与小帅弟看看我随拍的猫冰岛Arnarstapi,夜幕不落Logstash、Fluentd、Fluent Bit 和 Vector,谁才是开源日志收集最强王者?开源设计系统 PatternFly 的 5 个最佳实践 | Linux 中国5月CFA考试确认信下载/打印流程指南!加拿大航空 Air Canada (AC) Aeroplan 里程指南【2023.2更新】[败家] 反季入坑单板,Flow NX2 carbon 及 Flux GX 固定器如何更好地与AI交流:一份Prompt工程指南 | 5Y ViewTipping Livestreamers ‘Out of Control’: China State Broadcaster老乡见老乡 美食共品尝【本周折扣汇总】Ocado超市75折!NIKE/Hollister/Lululemon最低4折!资生堂半价!【2023年更新版】阿拉斯加航空 Alaska Airlines (AS) Mileage Plan 里程指南HotelFT 更新:新增 Hyatt Privé, Marriott STARS & Luminous 查询地图和更多详情【本周折扣汇总】北脸/lulu/Hollister半价起!英中航班3.3k拿下!Myprotein/Manuka蜂蜜2折!人类满级小孩在油管上学编程:8个YouTube免费学编程频道How a Forgotten Park Birthed a Revolution首开!Deloitte (US) 开放2025金融中后台Summer/Winter实习被米饭杀手惊艳到了!锁死Costco的这款黄油鸡酱Butter Chicken周末厨房丨【印式奶油鸡块】 Butter Chicken (Murgh Makhni)百度推出AI编程助手 "Comate":覆盖30多种编程语言行业入门|留学生保姆级Cover Letter指南【5.3折扣】Kenzo 6折!Urban Outfitters 1天闪促!Elizabeth Arden 买2送1!As City Bus Service Teeters in Henan, Local Government Steps In乘风破浪五千年(5) 殷人的“美洲之旅”蚂蚁:实现异步的8种方式,你知道几个?实现异步的方式,你知道几个?ChatGPT火爆,最全prompt工程指南登GitHub热榜,标星4.7k!把ChatGPT加入Flutter开发,会有怎样的体验?‘Teenager Mode’ in Video Apps Has Flaws, Authorities FindiPhone 8 Plus white 256g battery health 100%新加坡航空 Singapore Airlines (SQ) KrisFlyer 里程指南【2023重写版】Flutter如何将代码显示到界面上Flutter热更新技术探索Times Square Billboard Attracts Chinese Attention Seekers
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。