Flutter 中的异步的使用
前言
Flutter的网络请求会卡顿吗?看图:
明显 Flutter 在文件 IO 的时候卡顿了,为什么?怎会如此?
我们都知道很多语言都是支持多线程的,例如 Android 中一个 App 就有主线程和其他子线程,我们有 Handler 之类的工具进行线程间的通信。
而 Flutter 的 Dart 语法,它是单线程的,也就是说只有主线程。所以在 Flutter 中我们一般的异步操作,实际上还是通过单线程通过调度任务优先级来实现的。
如果此时的耗时操作超过了 16ms 那么主线程就有可能发生卡顿,这也是大家推荐 16ms 以下的异步用 Future,超过16ms 的异步用 Isolate 的建议了。
而很多一些开源项目或Demo很少提及 Isolate 导致很多 Flutter 开发者以为异步就是 Future 这一种用法,最后主线程卡顿不知道是什么问题,不知道怎么解决。
(PS,偷偷的说其实我自己也觉得 Isolate 不好用)
那么接下来我们就把 Future 与 Isolate 的异步简单的过一遍。
一、Future的分类与使用
当一个方法加上 async 关键字,表明此方法是异步操作。
scss
void loadSth() async {
Future(() {
...
}).then((value) {
...
}
);
}
如果这个方法有返回值,那么可以加上 Future 的方式返回一个 Future类型的参数:
csharp
Future<String> loadSth() async {
Future.value("123");
}
那么如何创建一个 Future 呢? 除了我们常用的网络框架自带 Future 之外,我们还能使用一些 Future 操作符来创建不同功能的 Future。
比较常用的:
直接通过高阶函数创建Future
scss
void testFuture() {
Future(() => print('执行任务')),
}
Future.value() 方法非常简单,直接返回一个 Future 的返回值
scss
void testValue() {
Future<int>.value(123).then((value) {
print('value: $value');
});
}
Future.delayed 我们能够实现延时调用功能:
dart
void testDelayed() {
print('开始执行: ${DateTime.now()}');
Future.delayed(const Duration(seconds: 2), () {
print('延时2秒执行: ${DateTime.now()}');
});
print('3.结束执行: ${DateTime.now()}');
}
Future.wait 表示同时执行多个异步任务,在所有任务执行完成,或者发生异常时进行回调,
less
await Future.wait([
Future.delayed(const Duration(milliseconds: 1000)), //默认1秒延时
DirectoryUtil.getInstance(),
FlutterBugly.init(
androidAppId: "dab97e43dc",
iOSAppId: "ab0671c657",
), //Bugly的初始化
Future(() {
//极光推送初始化与监听
jpush.init()
//初始化定位
LocationUtil().initBaiduLocation();
}),
]);
例如,我们就能把延时1秒和其他任务一起执行,最少登录1秒,超过1秒则按最大时间算,这样就完成一个并发任务。
Future.forEach 顾名思义就知道,它遍历每个item处理,遍历结束后,返回执行结果。
scss
Future.forEach([1,2,3,4], (element){
return Future.delayed(Duration(seconds: 3),(){
print(element);
});
});
比如上传多个表单数据,可以遍历执行多个表单数据进行处理,然后返回执行的结果再进行请求。
Future.microtask 表示将一个 Future 添加到微任务队列中执行,这个微任务队可以理解成优先处理的任务。
(在每一次事件循环中,Dart 总是先去第一个 microtask queue 中查询是否有可执行的任务,如果没有,才会处理后续正常的 event queue 的流程。)
Future.sync会阻塞当前代码,sync的任务执行完了,代码才能继续走。
简单的举例:
scss
void test() {
testFuture();
print("在testFuture()执行完毕之后打印。");
}
void testFuture() async {
Future((){
print("任务1,来自高阶函数创建");
});
Future.sync(() {
print("任务2,来自同步创建");
});
Future((){
print("任务3,来自高阶函数创建");
});
Future.microtask((){
print("任务2,来自微队列创建");
});
print("在testFuture()内部最后执行");
}
执行 test() 打印结果为:
可以看到 sync() 会阻塞当前代码,只有等 sync 的任务执行完了,代码才能继续往下走。
再来一段代码:
ini
void testFuture() async {
Future future2 = Future.sync(() {
return "任务2,来自同步创建";
});
Future future0 = Future.value(0); // 同步任务优先级最高
Future future1 = Future((){
return "任务1,来自高阶函数创建";
});
Future future3 = Future.sync(() => Future.value(5));
Future.sync((){
print("Future.sync,高阶函数创建的优先级比无Future的要更高");
});
Future future4 = Future((){
return "任务4,来自高阶函数创建";
});
Future future5 = Future.microtask((){
return "任务5,来自微队列创建" ;
});
future0.then(print);
future1.then(print);
future2.then(print);
future3.then(print);
future4.then(print);
future5.then(print);
}
我们可以得知如下结论:
- 无返回的 Future.sync 直接执行,优先级最高。
- 其次是 Future.value 约等于同步执行。
- 再其次是 Future.sync 带 Future类型的返回值,优先级也比较高。
- 再其次就是 Future.microtask 微队列的优先级。
- 再其次就是 Future.sync 不带 Future类型的返回值,由于内部会进行Future的包装所以排在微队列后面。
- 最后就是其他的普通 Future 创建方式了,按顺序执行。
Future 的接收 我们可以用 await 和 then 来接收:
then 与 await-async 调用的区别在于,Future.then 无需给方法添加 async关键字,缺点在于返回值会嵌套到 Future.then 中,导致方法获取返回值更加麻烦,示例如下:
perl
future.then((value){
print("任务执行结束:$value");
})
可以看到 then 我们需要传参一个高阶函数来接收,其实是破坏了上下文环境,我个人是更推荐使用 async - await 的方式来进行接收。
捕获 Future 的异常我们可以用 catchError 也可以 then 中的 onError 也可以用 try - catch 传统的方式。
scss
future.then((value){
print("任务执行结束:$value");
}).whenComplete((){
debugPrint("完成任务");
}).catchError((error){
print(error.toString());
});
我个人也更推荐使用 try - catch 传统的方式来捕获异常,因为 catchError 回调必须返回一个 Future ,有时候我们的Future本身没有返回值。
我个人也不喜欢这种破坏上下文环境的嵌套用法,个人主观比较喜欢使用 async-await 和 try-catch 方案。(个人喜好)
Future 是我们开发中异步开发用的最多的,而在一些特殊场景下我们实在是需要开启一些线程去执行特殊的一些操作,我们也可以用 Isolate 的方式去操作。
二、Isolate的几种使用方式
Dart 中异步操作可以用来执行耗时操作。可以在等待一个操作完成的同时进行别的操作。
而我们常用的一些异步或等待的一些操作一般来说分为两种,一种是不调用CPU纯等待,例如网络请求获取数据,或者纯粹的 delay 。另一个是调用CPU的等待,例如 File 文件的 IO 或者一些复杂的计算。
这里点一下文章开头的场景,就是 IO 文件导致的卡顿。所以此场景就可以使用 Isolate 的方式来进行优化。
那么 Isolate 如何使用呢?有几种使用方式呢?
在使用前我们要明白一个点,Dart 虽然是一个单线程语言,但是也提供了多线程的机制,也就是 Isolate 。但重点是在多线程与主线程之间的资源是隔离的,是不互通的。
这一点和 Java Android 这种多线程很不一样,这 Isolate 线程的使用有点进程的味道了。所以才说 Dart 的 Isolate 是进程级别的线程,它们有独立的堆栈和内存空间,并且不能共享状态。这一点和进程是类似的,因此我们可以认为 Isolate 具有类似于进程的一些特点。
使用方式一 ,Isolate.spawn :
dart
void download() async{
const String downloadLink = '下载链接';
final resultPort = ReceivePort();
await Isolate.spawn(downloadAndRead, [resultPort.sendPort, downloadLink]);
String fileContent = await resultPort.first as String;
print('展示文件内容: $fileContent');
}
Future<void> downloadAndRead(List<dynamic> args) async {
SendPort resultPort = args[0];
String fileLink = args[1];
print('获取下载链接: $fileLink');
// ... 模拟Http网络下载
String fileContent = '文件内容';
await Future.delayed(const Duration(seconds: 2));
Isolate.exit(resultPort, fileContent);
}
比如我们下载一个文件到本地 SD 卡,并且从 SD 卡读取文件展示,都进行了 IO 的操作,那么我们可以使用原始的 Isolate.spawn 用法。
Isolate 之间的通信是通过 SendPort 和 ReceivePort 进行消息传递和接收,主线程创建了一个ReceivePort用于接收Isolate的结果,而downloadAndRead函数则通过SendPort将结果发送回主线程。
使用方式二 ,Isolate.run :
Isolate.run 是对 Isolate.spawn 的简化形式。两者都用于在 Dart 中创建并发执行的隔离(Isolate)
dart
void download() async{
const String downloadLink = '下载链接';
String fileContent = await Isolate.run(() => downloadAndRead(downloadLink));
print('展示文件内容: $fileContent');
}
Future<String> downloadAndRead(String fileLink) async {
print('获取下载链接: $fileLink');
// ... 模拟Http网络下载
String fileContent = '文件内容';
await Future.delayed(const Duration(seconds: 2));
return fileContent;
}
与之前的例子相比,这种用法更加简洁,直接使用了 Isolate.run() 来创建和执行 Isolate ,并且通过 await 关键字等待 Isolate 的结果。这种方式适用于不需要明确控制 Isolate 之间通信的场景,省略了 ReceivePort。它会自动创建一个隐式的 ReceivePort,以便在隔离内部进行消息传递,避免了每次都需要手动创建 ReceivePort 的麻烦。而是只关注结果的情况即可。
使用方式三,compute :
compute() 是 Flutter 中对 Isolate.run() 的封装
dart
void download() async{
const String downloadLink = '下载链接';
String fileContent = await compute(downloadAndRead,downloadLink);
print('展示文件内容: $fileContent');
}
Future<String> downloadAndRead(String fileLink) async {
print('获取下载链接: $fileLink');
// ... 模拟Http网络下载
String fileContent = '文件内容';
await Future.delayed(const Duration(seconds: 2));
return fileContent;
}
或者直接写匿名高阶函数:
dart
void download() async{
const String downloadLink = '下载链接';
String fileContent = await compute((link) async {
print('开始下载: $link');
await Future.delayed(const Duration(seconds: 2));
return '下载的内容';
}, downloadLink);
print('展示文件内容: $fileContent');
}
compute() 是 Flutter 中对 Isolate.run() 的封装,简化了使用流程,通过使用 compute() 函数,您可以方便地在后台执行函数,并且不需要显式地使用Isolate来创建和管理线程。compute()函数会自动处理这些细节,并提供简洁的并发执行的能力。
运行报错?
E/flutter (13544): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Invalid argument(s): Illegal argument in isolate message: object is unsendable - Library:'dart:ui' Class: Path (see restrictions listed at
SendPort.send()
documentation for more information)
这是因为你传递的是普通函数,我们需要把函数定义为顶层函数或者静态函数才行,Isolate 是独立的执行单元,它与主线程(UI 线程)隔离,拥有自己的内存空间和执行环境。为了在 Isolate 中执行任务,必须将任务封装为静态函数或顶级函数,以确保在 Isolate 中正确地调用。
如何修改文章开头的网络请求上传文件卡顿的问题呢?
懂了,把网络请求丢到 Isolate 中去请求。
啊... 这... 不是这么玩的,一个是麻烦,我们的网络请求都是通过封装的,一些类与资源都是在主线程定义的,比如一些网络请求拦截器我们添加了很多的设备新,token请求,加密信息等,都是在主线程资源。
我们之前重点强调,主线程与 Isolate 线程是资源隔离的,那么你就要把全部的资源传输到 Isolate 线程...烦死了。
其实我们只需要修改读取文件的那一步即可,之前的伪代码:
scss
//File文件流
if (pathStreams != null && pathStreams.isNotEmpty) {
for (final entry in pathStreams.entries) {
final key = entry.key;
final value = entry.value;
if (value.isNotEmpty) {
// 以流方式压缩,获取到流对象
Uint8List? stream = await FlutterImageCompress.compressWithList(
value,
minWidth: 1000,
minHeight: 1000,
quality: 80,
);
//传入压缩之后的流对象
map[key] = MultipartFile(
stream,
filename: "file_stream",
);
}
}
}
}
var form = FormData(map);
if (!AppConstant.inProduction) {
print('Post请求FromData参数,fields:${form.fields.toString()} files:${form.files.toString()}');
}
//以 Post-FromData 的方式上传
req = post(url, form, headers: headers);
只需要修改到文件操作即可:
scss
// 压缩函数
Future<Uint8List> compress(Uint8List data) async {
// 在后台线程中执行压缩操作
// ...
// 返回压缩后的数据
return compressedData;
}
if (pathStreams != null && pathStreams.isNotEmpty) {
for (final entry in pathStreams.entries) {
final key = entry.key;
final value = entry.value;
if (value.isNotEmpty) {
// 使用 compute 函数在后台线程中执行压缩操作
Uint8List stream = await compute(compress, value);
// 传入压缩之后的流对象
map[key] = MultipartFile(
stream,
filename: "file_stream",
);
}
}
}
虽然如此,就算如此,还是会有部分网络请求会存在一些 IO 行为或者一些大数据序列化与反序列化,或多或少还是会造成 UI 的轻微卡顿。
两种办法:
- 换成原生的桥接请求方式,特别是一些混编应用,复用Native的网络请求效率更高。
- 换成DIO请求框架,默认的Http是基于Dart SDK 提供的 dart:io 库来进行网络请求,本质上还是在其自己的事件循环中执行的。而 DIO 支持创建额外的 Isolate 以便在多个处理器核心上执行并行代码。能有效的避免阻塞主线程,从而提高应用的响应性和流畅性。
总结
相信看完之后能对 Flutter 的异步如何使用有那么一些了解了,那么此时我们再回到文章开头的观点。
如果操作超过了 16ms 那么主线程就有可能发生卡顿,这也是大家推荐 16ms以下的异步用 Future,超过16ms的异步用 Isolate 的建议了。
这句话其实不准确,是否需要使用 Isolate 并不只取决于操作的耗时。我们还要区分任务的类型,是否是 CPU 密集型的任务。如果是 CPU 密集型的任务,那么即使操作超过16ms,我们也可以考虑使用 Isolate 的方式将其放在单独的线程中执行,以避免阻塞主线程。反之,如果任务只是普通的异步等待,不需要大量的 CPU 计算,那么我们可以直接使用 Future 来进行异步处理。例如,Future.delay(60) 这样的操作就不会造成 UI 的卡顿。
明白了这一点的前提下,我们再知道 Future 和 Isolate 的优缺点。就可以在指定的场景下使用对应的操作了。
Ok,本文的代码文章都是 Demo 性质,文中只贴出一些核心代码,主要是理清一些思路。如果代码、注释、理解有不到位或错漏的地方,希望同学们可以指出。
如果感觉本文对你有一点点的启发,还望你能点赞
支持一下,你的支持是我最大的动力。
Ok,这一期就此完结。