跟🤡杰哥一起学Flutter (八、进阶-异步编程速通🧨)

🥰 年前肝了篇 《跟🤡杰哥一起学Flutter (七、项目实战-UI部分🤷‍♀️)》代表Flutter实战小项目的落幕,意味着我们的Flutter已经入门,接下来就该 进阶 啦,即 专项知识点的学习 。恰逢有小伙伴在 《四、Dart基础语法速通🏃》提到:

索性Flutter进阶的第一篇就来刨刨 Flutter 中的 异步编程,本节学习路线如下:

  • 简单了解点概念性的东西,对Dart并发的整体架构建立一个初步认知,有助于后面的学习,涉及:Flutter 架构概览、Flutter 线程模型、Dart的并发实现(isolate机制)、Dart单线程模型(Event Loop)、同步 & 异步、耗时任务的分类(IO密集型 & 计算密集型)
  • 了解完概念,接着就是针对Dart异步编程三大API → Future、Stream、Isolate 的专项突袭了~

话不多说,直接开始🏃...

1. 概念相关

作为一只Flutter菜🐔,大部分时间其实都是在Framework层 堆组件 + 调API,概念部分就当看个乐子。大概了解下,暂时没必要深究,能涨见识最好,实在不理解也没关系,更没必要背哈😄~

1.1. Flutter 架构概览

Google 推出 Flutter 旨在提供一个 高性能跨平台一致UI框架 ,让开发者可以用 一套代码 构建出美观的移动、网页和桌面应用。

Google 选择 Dart 作为 Flutter 编程语言的一些可能考量:

  • Dart 拥有 类似C++和Java的熟悉语法,让熟悉这些语言的开发者能够快速上手;
  • Dart 是一种 多范式 语言,可以实现面向对象、函数式、响应式等不同的编程风格;
  • Dart 拥有庞大的 库生态系统,提供了丰富的核心库和三方包,满足各种开发需求;
  • Dart 支持 JIT(即时编译)AOT(预编译) ,实现热重载和热重启,以及生成高效的原生代码;
  • 等等...

Flutter主打一个 跨端 ,架构自然跟 Android架构大相径庭,《官方文档》给出这样的 Flutter分层架构图

上层组件依赖下层组件,组件间无法跨层访问下面,接着 从下往上 过下每一层都做了啥:

  • Embedder (嵌入/平台层):负责与底层操作系统进行交互,提供渲染、输入、辅助功能等服务,并且管理事件循环队列。根据不同的平台使用不同的语言编写,如:Android → Java & C++,iOS、macOS → Objective-C & Objective-C++,Windows、Linue → C++。Flutter 代码可以通过嵌入层集成到现有应用中,也可以作为应用的主体。
  • Engine (引擎层):提供Flutter核心API的底层实现 ,包括图形绘制、文本布局、文件和网络IO、插件架构和Dart运行时和编译环境。主要使用C++编写,并通过 dart:ui 库暴露给上面的框架层。
  • Framework (框架层):负责提供上层API的封装,用于构建高质量的应用,如widget、动画、手势、无障碍和文本输入等。使用Dart语言编写,包含了多个层次的库:
  • Foundation (基础库):提供一些基础的类和服务,如动画、绘制、手势等,以及一些平台无关的工具类,如日期、时间、集合等;
    • Rendering (渲染库):提供一套用于构建和布局UI树的抽象,以及一些具体的渲染对象,如盒子、文本、图片等。渲染库会根据UI树的变化,自动计算出需要重绘的部分,并调用底层的dart:ui库进行绘制。
    • Widgets (组件库):提供了一套基于渲染库的高级组件,以及一些管理组件状态和生命周期的机制,如StatefulWidget、StatelessWidget、InheritedWidget等。组件库是Flutter开发的基础,它实现了响应式编程模型,让开发者可以方便地构建复杂的用户界面。
    • Material、Cupertino库:提供了一套遵循 Material Design 和 iOS风格的组件,如按钮、导航栏、对话框等,以及一些与平台相关的主题、图标、字体等。这些库让开发者可以快速地创建符合不同平台设计规范的应用。

😏 顺带提一嘴 Flutter性能能媲美原生应用 的原因(摘自《Flutter框架分析(一)--架构总览》):

RN等JavaScript+原生渲染跨平台技术 需要先调用原生框架 ,再通过原生框架调用Skia,最后调用至GPU进行绘制。而 Flutter直接通过Skia调用GPU进行绘制,少了调原生框架那一步,这就是Flutter性能为啥能媲美原生应用的原因。另外,Android中的Skia升级比较缓慢,而Flutter自带Skia,升级方便,如果使用了更高性能的Skia库,其绘制性能甚至可能超过原生。

1.2. Flutter 线程模型

Flutter 引擎 并不创建和管理线程,而是抽象出 Task Runner 的概念,由对应的 平台层 来实现 四个主要的Task Runner。Flutter 引擎不关心它们是否为指向同一个Task Runner引用,或者运行在同一个线程中。不过官方还是建议:为了获得最佳性能,应该为每个Task Runner创建一个专用线程。

这四个Task Runner分别为:

  • Platform Task Runner主Task Runner ,对应Android和iOS中的 主线程 ,对于Flutter引擎的接口调用都要保证在Platform Thread进行。不建议在此Runner执行繁重的耗时操作,虽然 不会直接导致Flutter App的卡顿 ,但长时间卡住主线程,App有可能被 系统Watchdog机制 强杀。
  • UI Task Runner负责执行Dart代码 ,包括 应用的主代码和 Root isolate ,它也可以调度和提交渲染帧,生成 layer tree 和 semantic tree,与平台无关,它只需要与 Flutter 引擎交互。它的性能直接 影响Flutter应用的流畅度(卡顿掉帧) ,所以避免在该Runner上执行耗时计算或者堵塞的操作。
  • Raster Task Runner :将UI Task Runner 生成的 layer tree 光栅化bitmap ,然后显示在屏幕上。它需要访问设备的 rasterizer (通常由GPU提供),所以它也被称为 GPU Task Runner
  • IO Task Runner:负责执行IO相关的任务,如文件读写、网络请求、图片解码等,它可以创建和管理其它Dart isolate,以便在后台执行一些复杂的计算。

注:Android和iOS平台,每个Flutter引擎实例启动时,默认会为UI、GPU、IO Runner 各自创建一个新的线程,而 所有Flutter引擎实例共享同一个Platform Runner和线程

😀 简单提炼下要点:

Flutter中的 Task Runner 是 管理和运行任务的抽象 ,由 平台层 实现与 对应系统线程的关联 。少在 Platform RunnerUI Runner 里做耗时操作,前者可能会导致ANR,后者可能会导致Flutter应用卡顿掉帧。

1.3. Dart 并发实现 → isolate

上面提到了 isolate(隔离区) ,它是Dart并发编程的 执行单元 ,基于 Actor模式 的具体实现,这种 并发编程模型 的核心思想:将程序分解成多个Actor对象,彼此独立的并发实体,通过消息传递进行通信。在Dart变现为:

每个 isolate 拥有自己的 内存空间 (堆栈)单线程控制的运行实体 (保证代码顺序执行 → 消息队列+事件循环) ,没有 共享内存的并发 ,无需在isolate内部管理 同步和锁问题 ,由此简化了并发模型。isolate 间只能通过 消息传递(Port端口) 进行通信,所以Dart中的消息传递总是 异步 的。

😳 网上很多文章喜欢把 isolate 称为轻量级的线程,个人觉得不太贴切,内存隔离 + 消息通信 使得 isolate 更像是 轻量级的进程。Dart 本身抽象了isolate 和 thread,实际底层还是使用操作系统提供的OS Thread。

当Dart VM创建一个 isolate 时,底层会为其分配一个OS Thread (新建或复用),Dart VM会管理这些线程,并确保Dart代码能够在正确的 isolate 环境中运行。具体源码可以看下 isolate.cc 中的 SpawnIsolateTask 类,其中的 thread->isolate() 让线程关联对应的isolate实例。

Isolate 由 Engine层 里的 Dart Isolate Setup 模块创建和管理,Dart VM启动时创建 的第一个 isolate → Root Isolate ,它是 Dart程序的入口点 ,负责执行UI线程上的所有Dart代码。可以通过 Dart的C++调用能力 把UI渲染相关的任务交给UI线程,也可以接收来自UI线程的事件通知,还可以通过 Isolate.spawnIsolate.spawnUri 方法创建其它Isolate,用于执行一些耗时的计算任务或IO操作,避免阻塞UI线程。

1.4. Dart 单线程模型 → Event Loop

使用 单线程模型 来进行 逻辑处理UI渲染 ,这个线程一般称作 UI线程/主线程 ,这样设计的目的:保持UI一致性和可靠性避免多线程带来的并发问题和线程安全问题。而Dart中的单线程模型指的是:

每个 isolate只有一个 Event Loop (事件循环),它会 按顺序执行同步代码和异步回调,不会出现并发执行的情况。

所谓的 Event Loop(事件循环) 就是:只要事件队列不为空,就不断从中获取一个事件并对其进行处理。事件可能是:用户输入、文件 I/O 通知、计时器等,简易示例图如下:

在Dart中,main()函数执行完毕后,Main Isolate的线程不会立即结束,而是开始逐一处理事件队列中的事件:

当然,这是简化过后的流程图,Dart中的事件循环,其实包含了两个队列:

  • 微任务队列 (microtask queue):即时执行的 小任务,不含任何I/O操作或其它需要等待的操作,很少用到;
  • 事件队列 (event queue):需要较长执行时间的 宏观任务,如: I/O操作、定时器、绘制事件和用户交互等;

在每次事件循环中,Dart 总是先查询 微任务队列 中是否有可执行任务,没有才会去处理 事件队列 里的任务。完整流程示意图如下:

1.5. 同步 & 异步

上面提到 同步异步 ,它们是两种不同的 程序执行模式

  • 同步模式 :程序 按照顺序依次执行 ,每个操作都必须等待上一个操作完成才能继续执行。即 串行执行 (线性、有序)。同步模式可以保证,程序执行的可靠性和稳定性,但执行效率可能会受到限制。
  • 异步模式 :程序 不按照顺序依次执行 ,而是通过回调函数、事件驱动或者其他方式实现 并行执行 (非线性、无序),每个操作可以在前一个操作尚未完成时开始执行。异步模式可以提高程序的执行效率,充分利用计算资源。

举个简单例子 (App下载视频):

  • 同步就是,你得跟个智障😑一样等视频下载完,才能执行其它操作;
  • 异步就是,提交了下载任务,App后台自动下载,不妨碍你做其它操作,等视频下载完它再通知你一下👻;

应该很好理解,总结下就是:任务执行过程中有需要等待结果的耗时任务,可以考虑使用异步提高效率

1.6. 耗时任务的分类

App中的耗时任务可以笼统地分为两类:

  • IO密集型:涉及大量的输入/输出操作,如:文件读写、网络通信、数据库访问等;执行此类任务时CPU的利用率相对较低,主要时间花费到等待IO操作的完成上。可以利用异步IO、多线程等方式提高并发性。
  • 计算密集型 (CPU密集型) :涉及大量的计算操作,如:复杂的数学运算、图像/视频处理等;执行此类任务时CPU的利用率相对较高,主要时间花费在进行各种计算操作上。可以利用多核CPU、并行计算等方式提高计算速度。

接着写下代码演示下这两种类型的任务:

dart 复制代码
class _TaskScreenState extends State<TaskScreen> {
  String text = "任务结果";

  // IO密集型任务
  void doIOTask() async {
    Response response = await Dio().get("https://www.wanandroid.com/banner/json");
    setState(() {
      text = response.data.toString();
    });
  }

  // CPU密集型任务
  void doCPUTask() {
    int sum = 0;
    for(int i = 0; i < 1000000000; i++) {
      sum += i;
    }
    setState(() {
      text = "计算结果:$sum";
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const CircularProgressIndicator(),
        const SizedBox(height: 10),
        Text(text),
        MaterialButton(
            color: Colors.blue,
            onPressed: () {
              doIOTask();
            },
            child: const Text('执行IO密集型任务')),
        MaterialButton(
            color: Colors.blue,
            onPressed: () {
              doCPUTask();
            },
            child: const Text('执行计算密集型任务'))
      ],
    );
  }
}

运行后,分别点击两个按钮执行任务,效果如下:

可以看到,请求网络不会卡顿执行大规模计算直接卡住 ,为啥?因为前面是 异步 ,后面是同步,具体分析下:

  • await Dio().get(xxx):告诉Dart启动网络请求,挂起当前函数的执行,让出控制权,直到请求完成。在这段挂起的时间里,Dart的事件循环可以处理其他事件,比如用户界面更新、输入事件或计时器。UI保持响应状态,网络数据获取后可以重新开始执行并更新UI。
  • doCPUTask() 中的代码是计算密集型的任务,在循环中执行大规模的同步计算,并没有让出控制权,由于Dart是单线程的,直接堵塞事件循环,导致应用无法处理其它时间,如屏幕刷新或用户交互,这样UI就会冻结直到计算完成。

🤡 直接使用 Future + await + async 三个关键字 同步变异步

dart 复制代码
void doCPUTask() async {
  return Future((){
    int sum = 0;
    for(int i = 0; i < 1000000000; i++) {
      sum += i;
    }
    return sum;
  }).then((sum) => setState(() {
      text = "计算结果:$sum";
    })
  );
}

🤣 抱歉,一样卡死,尽管使用了异步Future ,但 同步和计算密集型的代码 依旧会在单一的isolate上运行,Event Loop依旧需要等待这段同步代码执行完毕后,才能继续处理其它时间。为了解决这个问题,可以把 计算密集型任务 丢到 新的isolate 中运行,计算完毕后再通过端口发送回主isolate,并更新UI。具体代码如下:

dart 复制代码
void doCPUTask() async {
  // 创建一个ReceivePort,用来接收来自新Isolate的消息
  final resultPort = ReceivePort();
  // 创建并启动一个新的Isolate,它将并行执行累加操作
  await Isolate.spawn((sendPort) {
    // 在新建的isolate中执行耗时计算操作
    int sum = 0;
    for(int i = 0; i < 1000000000; i++) {
      sum += i;
    }
    // 计算完毕,通过Port发送结果回主Isolate
    sendPort.send(sum);
  }, resultPort.sendPort);
  // 等待并获取新Isolate发送回来的第一个消息
  int sum = await resultPort.first;
  // 更新UI
  setState(() {
    text = "计算结果:$sum";
  });
}

😁 修改后运行,此时发现执行计算密集型任务也不会导致UI卡顿啦!关于概念的东西就了解到这吧,接着具体讲下Dart的异步编程API~

2. Dart异步编程API详解

Dart 中的异步编程API,主要是通过 FutureStream 两个API来实现的,一一讲解下~

2.1. Future

Dart中的 Future 代表 一个异步任务,定义一个异步任务的代码示例如下:

dart 复制代码
Future<String> fetchData() {
  // 休眠3s模拟执行耗时操作
  return Future.delayed(const Duration(seconds: 3), () {
    return "Hello Flutter!";
  });
}

😁然后是获取异步返回结果的两种方式~

2.1.1. async + await

两个关键字提供了一种 类似同步代码的方式来编写异步操作,好处 → 直观、易于阅读和维护:

  • async → 用于声明一个异步函数;
  • await → 用于等待一个异步操作的结果;

调用上述异步任务的代码示例如下:

dart 复制代码
Future<void> printData() async {
  var result = await fetchData();
  print(result);
}

另外,建议对异步操作中可能出现的错误进行处理,直接使用 try-catch 关键字进行异常捕获:

dart 复制代码
  Future<void> printData() async {
    try {
      var result = await fetchData();
      print(result);
    }  on IntegerDivisionByZeroException catch (e) {
      print("除0异常");
    } catch (e) {
      print(e);
    } finally {
      print("异常与否最终都要执行的代码块");
    }
  }

对了,还有一点要注意:使用await会等待,直到异步操作完成才继续往下执行代码,比如这样的代码:

dart 复制代码
// 定义三个异步请求
Future<String> fetchUserOrder1() => Future.delayed(Duration(seconds: 1), () => 'Order 1');

Future<String> fetchUserOrder2() => Future.delayed(Duration(seconds: 2), () => 'Order 2');

Future<String> fetchUserOrder3() => Future.delayed(Duration(seconds: 3), () => 'Order 3');

Future<void> doTasks() async {
	var startTime = DateTime.now().second;
  await fetchUserOrder1();
  await fetchUserOrder2();
  await fetchUserOrder3();
  var endTime = DateTime.now().second;
  print(endTime - startTime);	// 输出:6
}

因为等待,所以总的运行时间为:1+2+3=6s,如果想三个请求同时执行,可以改下写法:

dart 复制代码
Future<void> doTasks() async {
  var startTime = DateTime.now().second;
  var order1 = fetchUserOrder1();
  var order2 = fetchUserOrder2();
  var order3 = fetchUserOrder3();
  await order1;
  await order2;
  await order3;
  var endTime = DateTime.now().second;
  print(endTime - startTime);	// 输出:3
}

也可以用下下面提到的Future.wait()方法来实现多个耗时任务并行。

2.1.2. then() + catchError() + whenComplete()

这三个方法是Future的一个API,它允许你在Future 成功完成时异常结束时任务完成时(无论成败) 时执行一个回调参数,使用代码示例如下:

dart 复制代码
void printData() {
  fetchData().then((result) {
    print("获取异步结果并输出:$result");
  }).catchError((error) {
    print("捕获异常:$error");
  }).whenComplete(() {
    print("无论是否捕获异常,都会执行的代码块");
  });
}

Future 的 then() 方法代码如下:

dart 复制代码
Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError})

返回一个Future,所以在处理连续请求时,可以 连续追加多个then 来规避回调地狱,伪代码如下:

dart 复制代码
fetchData()
    .then((value) => "写入数据库")
    .then((value) => "刷新UI")
    .then((value) => "埋点上报")
    .catchError((error, stackTrace) => print("stackTrace"));

2.1.3. FutureBuilder

FutureBuilder 是 Flutter 提供的 可以根据异步操作结果自动更新UI的组件,它的构造函数中的参数如下:

  • future必须,代表 FutureBuilder 需要监听的 Future,通常是IO或网络请求等耗时操作;
  • initialData :可选,提供一个初始值给后面builder回调函数,后者可以调用 snapshot.data 获取这个值;
  • builder必须 ,包含一个BuildContextAsyncSnapshot 参数的函数,定义了根据不同的future状态构建不同的UI;

写个简单代码示例:

dart 复制代码
class UserListPage extends StatefulWidget {
  const UserListPage({Key? key}) : super(key: key);

  @override
  State<UserListPage> createState() => _UserListPageState();
}

class _UserListPageState extends State<UserListPage> {
  // 请求接口
  Future<List> _fetchRemoteData() async {
    final response = await Dio().get('https://jsonplaceholder.typicode.com/users');
    return response.data;
  }

  // 模拟加载本地缓存
  List _loadLocalData() => [
        {
          "id": 1,
          "name": "Leanne Graham",
          "username": "Bret",
          "email": "Sincere@april.biz",
        },
        {
          "id": 2,
          "name": "Ervin Howell",
          "username": "Antonette",
          "email": "Shanna@melissa.tv",
        },
        {
          "id": 3,
          "name": "Clementine Bauch",
          "username": "Samantha",
          "email": "Nathan@yesenia.net",
        },
      ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FutureBuilder Example'),
      ),
      body: FutureBuilder(
        future: _fetchRemoteData(),
        initialData: _loadLocalData(),
        builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
          // 根据不同的Future状态返回不同的UI
          switch (snapshot.connectionState) {
            case ConnectionState.none:
              print("Future还未开始执行");
              return const Center(
                child: Text('Future还未开始执行'),
              );
            case ConnectionState.waiting:
              print('Future已经开始执行');
              if(snapshot.hasData) {
                print("接收到从initialData传递过来的初始数据长度:${snapshot.data?.length}");
              }
              return const Center(
                child: Text('Future已经开始执行'),
              );
            case ConnectionState.active:
              print('Future正在执行,但还没有完成');
              return const Center(
                child: Text('Future正在执行,但还没有完成'),
              );
            // Future 已经执行完成
            case ConnectionState.done:
              // 出现异常
              if (snapshot.hasError) {
                print('Future执行失败:${snapshot.error}');
                return Center(
                  child: Text('Future执行失败:${snapshot.error}'),
                );
              } else {
                print('Future执行成功,返回数据长度:${snapshot.data?.length}');
                return Center(
                  child: Text('Future执行成功,返回数据:${snapshot.data?.length}'),
                );
              }
          }
        },
      ),
    );
  }
}

运行后控制台输出结果如下:

😳 运行结果只走了 waitingdone 状态,其它两种状态很少发生,FutureBuilder 构建时如果指定了future,它会立即开始等待future的执行,通常直接进入 waiting状态 而不会走 none状态 ,而 active状态 只能用于具有中间值的 StreamBuilder只有在done状态前,snapshot.data的值才为initalData传递过来的初始数据,done后异常会置为null,执行成功则为future的返回值。

2.1.4. Future.wait()

在实际开发场景中,有时需要 等待多个Future返回并收集返回结果 ,比如:某个页面需要加载多个接口,需要所有接口结果都返回,才刷新UI,一种简单粗暴的处理方式是:加载UI前判断返回值是否都拿到了,是再刷新。那有更简便的方法吗?😁有的,Future提供了一个 wait() 的静态方法,该方法接收一个 Future列表 作为参数,当列表中所有Future都完成后才会触发回调。使用代码示例如下:

dart 复制代码
Future<void> doTasks() async {
  try {
    // 等待所有异步请求完成
    var orders = await Future.wait([
      fetchUserOrder1(),
      fetchUserOrder2(),
      fetchUserOrder3(),
    ]);
    // 所有请求完成后,执行UI操作
    for (var order in orders) {
      print('Completed: $order'); // 实际上,这里可以是你的UI操作代码
    }
  } catch (error) {
    // 等待期间任何一个 Future 失败都会抛出异常
    print('Error: $error');
  }
}

2.1.5. Future.delayed()

创建一个 延迟执行回调的Future ,内部实现为 Timer + 延时执行的Future:

2.1.6. Future.microtask() & Future.sync()

前者作用将创建的事件发送到微任务队列,具有比事件队列优先执行的特点;

后者作用同步调用,立即执行Future里的内容;

简单示例:

dart 复制代码
Future<void> doTasks() async {
  Future.microtask(() => print(9));
  Future(() => print(10));
  Future.sync(() => print(11));
}
// 输出结果:11、9、10

2.1.7. Future.value() & Future.error()

前者作用:创建一个已经完成并且成功的Future对象,其值为指定的数值;

后者作用:创建一个已经完成且包含错误的Future对象,其错误值为指定的错误对象;

这两个方法可用于 立即创建并返回Future对象,无需等待异步操作完成,简单示例:

dart 复制代码
Future<String> fetchRemoteData() {
  // 模拟从远程服务器获取数据的异步操作
  return Future.delayed(Duration(seconds: 2), () {
    // 假设数据成功返回
    return Future.value('Remote data');
    // 如果发生错误,可以使用 Future.error 来返回失败的 Future 对象:
    // return Future.error(Exception('Failed to fetch remote data'));
  });
}

2.1.8. Future.any()

函数描述 :接受一个 Iterable 对象并返回一个 Future → 集合中第一个完成的Future结果;

使用场景:当存在多个异步操作,而你只关心最快完成的操作的结果时,可以用它;

简单示例:

dart 复制代码
import 'dart:async';

void main() {
  Future<String> firstFuture = Future.delayed(Duration(seconds: 2), () => "First Future Completed");
  Future<String> secondFuture = Future.delayed(Duration(seconds: 1), () => "Second Future Completed");

  Future.any([firstFuture, secondFuture])
    .then((result) {
      print("The first future to complete is: $result");
    });
}
// 输出:The first future to complete is: Second Future Completed

2.1.9. Future.forEach

函数描述:接受一个 Iterable 对象和一个回调函数,并返回一个Future → 所有异步操作完成后才会完成;

使用场景:需要对Iterable对象中的每个元素执行一个异步操作,且在全部异步操作完成后执行操作,可以用它;

简单示例:

dart 复制代码
import 'dart:async';

void main() {
  List<int> numbers = [1, 2, 3, 4, 5];
  
  Future.forEach(numbers, (int number) {
    return Future.delayed(Duration(seconds: number), () {
      print("Processed number: $number");
    });
  }).then((_) {
    print("All numbers processed");
  });
}

2.1.10. Future.doWhile()

函数描述:创建一个循环异步执行一个函数,直到该函数返回false;

使用场景轮询,执行一个异步操作多次,直到达到预期的结果,可以用它;

简单示例:

dart 复制代码
Future<void> asyncLooper() async {
  int counter = 0;

  await Future.doWhile(() async {
    // 执行一些异步操作
    await Future.delayed(Duration(seconds: 1));
    // 假设我们的异步操作是打印计数器的当前值
    print('Counter is at $counter');

    // 更新计数器
    counter++;

    // 如果计数器小于5,返回true继续循环;否则返回false停止循环
    return counter < 5;
  });

  // 当循环结束时,这里的代码会执行
  print('Done looping');
}

2.2. Stream (流)

Future 代表 一个异步任务Stream 则代表 一个异步任务序列 ,即 一连串的异步任务。你可以监听Stream来获取它的结果 (数据和错误),也可以在Stream完成前对它进行暂停或停止监听,它有两种类型:

  • Single-subscription单订阅流,只能被一个监听器监听;
  • Broadcast广播流,能被多个监听器同时监听;

2.2.1. 创建Stream的几种方法

① 使用 Stream的构造方法创建

  • Stream.fromFuture(): 将一个Future转化为Stream流;
  • Stream.fromFutures() :将一个Future列表转换为Stream流;
  • Stream.fromIterable() :将一个Iterable (如List或Set) 转换成 Stream流;
  • Stream.periodic() :创建一个周期性发出事件的Stream流;
  • Stream.empty() :创建一个空的流,不包含任何事件;
  • Stream.value() :创建一个单一值的流,流中只有一个事件;
  • Stream.error() :创建一个包含错误事件的流;
  • Stream.multi() :允许你使用一个事件生成器函数来控制流的发送,用于创建具有复杂行为的流;

简单代码示例:

dart 复制代码
Future<String> fetchAsyncData() async {
  await Future.delayed(const Duration(seconds: 2));
  return 'Future Fetched data';
}

void testStream() {
  Stream.fromIterable([1, 2, 3, 4, 5]).listen((event) => print(event)); // 输出:1、2、3、4、5
  Stream.periodic(const Duration(seconds: 1), (computationCount) => computationCount)
      .take(5)
      .listen((event) => print(event)); // 输出(间隔1s):0、1、2、3、4
  Stream.fromFuture(fetchAsyncData()).listen((event) => print(event));  // 输出:Future Fetched data
}

② 使用 async * + yieldyield* 创建

注意这个 async * 是有个*星号的哈,不是 async !它用于标记一个 异步生成器函数 (返回Stream对象的函数),可以在等待异步操作完成的同时产生多个值。然后是给 Stream监听器传递值 的两种方式:

  • yield:每次调用yield时都会向Stream中添加一个值,函数执行到yield语句时会暂停,直到Stream监听器装备好接收下一个值才继续执行,这允许你构建一个可以产生多个值的函数,且这些值不是立即生成的,而是随着消费者的接收能力逐一生成。
  • yield* :将一个 Stream 的所有值插入到另一个 Stream 中,当生成器函数遇到yield*时,它会等待并传递所有来自另一个 Stream 的值,直到那个 Stream 完成。

简单代码示例如下:

dart 复制代码
// 定义一个异步生成器函数,它使用yield来产生数字1到3
Stream<int> numberStream() async* {
  for (int i = 1; i <= 3; i++) {
    yield i; // 向 Stream 发送值 i
    await Future.delayed(Duration(seconds: 1)); // 模拟异步等待
  }
}

// 另一个异步生成器函数使用yield*来传递numberStream生成的所有值
Stream<int> replicatedNumberStream() async* {
  yield* numberStream(); // 将 numberStream 的所有值传递给当前 Stream
}

void main() async {
  print('Start listening to numberStream');
  await for (int number in numberStream()) {
    print('Got a number from numberStream: $number');
  }

  print('Start listening to replicatedNumberStream');
  await for (int number in replicatedNumberStream()) {
    print('Got a number from replicatedNumberStream: $number');
  }
}

// 输出结果:
// Start listening to numberStream
// Got a number from numberStream: 1
// Got a number from numberStream: 2
// Got a number from numberStream: 3
// Start listening to replicatedNumberStream
// Got a number from replicatedNumberStream: 1
// Got a number from replicatedNumberStream: 2
// Got a number from replicatedNumberStream: 3

③ 使用 StreamController 创建

这种方式创建和使用Stream流更加灵活,先明确四个角色:

  • Stream数据源,可以被监听,单订阅流只能有一个监听器,而广播流可以有多个监听器;
  • StreamControllerStream流的控制器 ,可以在Stream上发送数据、错误和完成事件、也可以检查Stream是否暂停、是否有订阅者,以及在其它任何发生改变时获取到回调。提供了两个工厂方法来创建实例:StreamController()StreamController.broadcast() ,分别对应单订阅流和广播流。
  • StreamSink添加Stream事件的抽象类,用于添加数据、错误和关闭事件到Stream上,StreamController 实现了此接口,因此它也可以作为一个StreamSink使用。
  • StreamSubscriptionStream的监听对象 ,它可以监听Stream上的数据、错误和完成事件,也可以暂停、恢复和取消订阅。当你调用Stream的 listen() 方法时,会返回一个 StreamSubscription 对象,可以使用它来控制订阅。

简单代码示例如下:

dart 复制代码
import 'dart:async';

void main() {
  // 创建一个单订阅流的 StreamController
  // 如果想创建广播流可以用 StreamController.broadcast()
  var controller = StreamController<int>();

  // 订阅Stream
  controller.stream.listen(
    (data) {
      print('Received data: $data');
    },
    onDone: () {
      print('Stream is closed');
    },
    onError: (error) {
      print('Error occurred: $error');
    },
  );

  // 往Stream中添加数据
  controller.sink.add(1);
  controller.sink.add(2);
  controller.sink.addError('Something went wrong');
  controller.sink.add(3);

  // 关闭StreamController时,会向Stream发送关闭事件
  // 需要在确保不再发送数据的情况下执行此操作,以防止内存泄露和资源浪费
  controller.close();
}

// 输出结果:
// Received data: 1
// Received data: 2
// Error occurred: Something went wrong
// Received data: 3
// Stream is closed

有些读者可能会疑惑:没有定义StreamSubscription变量兜住listen()的返回值,然后调用cancel() 取消订阅不会内存泄露 吗?

答:不会。cancel() 方法一般在监听器不需要接收数据,但Stream还未结束时使用。当调用StreamController的close() 方法时,该控制器上的Stream会结束,一旦Stream结束,它会自动发送一个完成事件给所有监听器,并关闭Stream。这种情况下,监听器就不需要显式调用 cancel() 来取消订阅,因为Stream已经完成。

另外,构造参数中还支持传入一个bool类型的 sync参数 (默认false) 决定是否创建一个同步类型的StreamController,即事件添加和监听处于同一个Event Loop中,不太建议设置为true,可能导致潜在的堆栈溢出错误。

2.2.2. StreamBuilder

和前面学的 FutureBuilder 类似,状态监听有些不同,简单改下前面的示例实现相同的效果:

dart 复制代码
class _UserListPageState extends State<UserListPage> {
  late StreamController<List> _usersStreamController; // Stream控制器
  late Stream<List> _usersStream; // 流


  @override
  void initState() {
    super.initState();
    _usersStreamController = StreamController();
    _usersStream = _usersStreamController.stream;
    fetchData();
  }

  @override
  void dispose() {
    _usersStreamController.close(); // 销毁时要调close()
    super.dispose();
  }

  Future<void> fetchData() async {
    try {
      var remoteData = await _fetchRemoteData(); // 请求远程数据
      _usersStreamController.add(remoteData); // 发送远程数据
      // 延时3s发送一个空数据
      await Future.delayed(const Duration(seconds: 3), () async {
        _usersStreamController.add([]); // 发送远程数据
      });
    } catch (e) {
      _usersStreamController.addError(e); // 发送错误
    }
  }

  // 请求接口
  Future<List> _fetchRemoteData() async {
    final response = await Dio().get('https://jsonplaceholder.typicode.com/users');
    return response.data;
  }

  // 模拟加载本地缓存
  List _loadLocalData() => [
        {
          "id": 1,
          "name": "Leanne Graham",
          "username": "Bret",
          "email": "Sincere@april.biz",
        },
        {
          "id": 2,
          "name": "Ervin Howell",
          "username": "Antonette",
          "email": "Shanna@melissa.tv",
        },
        {
          "id": 3,
          "name": "Clementine Bauch",
          "username": "Samantha",
          "email": "Nathan@yesenia.net",
        },
      ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FutureBuilder Example'),
      ),
      body: StreamBuilder(
        stream: _usersStream,
        initialData: _loadLocalData(),
        builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
          // 根据不同的Future状态返回不同的UI
          switch (snapshot.connectionState) {
            case ConnectionState.none:
              print("Future还未开始执行");
              return const Center(
                child: Text('Future还未开始执行'),
              );
            case ConnectionState.waiting:
              print('Future已经开始执行');
              if (snapshot.hasData) {
                print("接收到从initialData传递过来的初始数据长度:${snapshot.data?.length}");
              }
              return const Center(
                child: Text('Future已经开始执行'),
              );
            case ConnectionState.active:
              print('Future正在执行,但还没有完成 返回数据长度:${snapshot.data?.length}');
              return const Center(
                child: Text('Future正在执行,但还没有完成'),
              );
            // Future 已经执行完成
            case ConnectionState.done:
              // 出现异常
              if (snapshot.hasError) {
                print('Future执行失败:${snapshot.error}');
                return Center(
                  child: Text('Future执行失败:${snapshot.error}'),
                );
              } else {
                print('Future执行成功,返回数据长度:${snapshot.data?.length}');
                return Center(
                  child: Text('Future执行成功,返回数据:${snapshot.data?.length}'),
                );
              }
          }
        },
      ),
    );
  }
}

运行后控制台输出结果如下:

相比 FutureBuilder,它会走 waiting状态 ,每次往Stream里发信息时都会走 active 状态,调用StreamControllerclose() 关闭Stream 或者异常,才会走 done 状态。

2.2.3. 处理 Stream 的API

上面我们使用 listen() 方法来对Stream进行监听,监听数据、错误以及关闭事件。除此之外还有下述API:

dart 复制代码
//获取Stream中的第一个元素,若Stream为空,则等待直到有元素可用。
Future<T> get first;

//检查Stream中是否没有元素,并返回对应的Future布尔值。
Future<bool> get isEmpty;

//获取Stream中的最后一个元素,若Stream没有结束,则等待直到Stream结束。
Future<T> get last;

//获取Stream中元素的个数,并返回一个Future整数。
Future<int> get length;

//当Stream中只有一个元素时返回它,否则如果是空或有多个元素则抛出异常。
Future<T> get single;

//检查Stream中的元素是否至少有一个满足条件`test`,是则返回true,否则返回false。
Future<bool> any(bool Function(T element) test);

//确定Stream中是否包含某个特定元素`needle`。
Future<bool> contains(Object? needle);

//丢弃所有的元素,当流关闭时,返回一个包含`futureValue`的Future。
Future<E> drain<E>([E? futureValue]);

//获取Stream中指定位置`index`的元素。
Future<T> elementAt(int index);

//检查Stream中的所有元素是否都满足条件`test`,是则返回true,否则返回false。
Future<bool> every(bool Function(T element) test);

//返回Stream中第一个满足条件`test`的元素,如果没有符合的元素则调用`orElse`。
Future<T> firstWhere(bool Function(T element) test, {T Function()? orElse});

//使用`combine`函数将Stream中的所有元素合并成一个值。
Future<S> fold<S>(S initialValue, S Function(S previous, T element) combine);

//对Stream中的每个元素执行操作`action`。
Future forEach(void Function(T element) action);

//将Stream中的所有元素连接成一个字符串,用`separator`分隔。
Future<String> join([String separator = '']);

//返回Stream中最后一个满足条件`test`的元素,如果没有符合的元素则调用`orElse`。
Future<T> lastWhere(bool Function(T element) test, {T Function()? orElse});

//将Stream的所有元素添加到目标`streamConsumer`中。
Future pipe(StreamConsumer<T> streamConsumer);

//将Stream的元素两两结合(从Stream的开头开始),使用`combine`函数将它们合并成一个值,并返回这个值。
Future<T> reduce(T Function(T previous, T element) combine);

//返回Stream中唯一满足条件`test`的元素,如果没有符合或存在多个则抛出异常,如果没有找到并且`orElse`被提供,则调用`orElse`。
Future<T> singleWhere(bool Function(T element) test, {T Function()? orElse});

//将Stream整理成一个Future列表。
Future<List<T>> toList();

//将Stream整理成一个Future集合。
Future<Set<T>> toSet();

2.2.4. 修改 Stream 的API

下述API可以 对原始Stream进行处理并返回新的Stream,调用这些方法后,监听原始Stream上的监听器会先监听转换后的新Stream,待新的 Stream 处理完成后才会转而回去监听原始的 Stream。

dart 复制代码
// 将流中的事件转换为特定类型<R>。
Stream<R> cast<R>();

// 展开流中的每个元素到零个或多个事件。
Stream<S> expand<S>(Iterable<S> Function(T element) convert);

// 将流中的每个事件映射为不同类型的事件。
Stream<S> map<S>(S Function(T event) convert);

// 跳过流的前'count'个事件。
Stream<T> skip(int count);

// 当遇到符合给定条件的事件时,跳过流中的事件。
Stream<T> skipWhile(bool Function(T element) test);

// 只获取流中的前'count'个事件。
Stream<T> take(int count);

// 当事件满足提供的条件时,获取流中的事件。
Stream<T> takeWhile(bool Function(T element) test);

// 过滤流中满足特定条件的事件。
Stream<T> where(bool Function(T event) test);

// 异步转换流中的每个事件。
Stream<E> asyncExpand<E>(Stream<E>? Function(T event) convert);

// 异步映射流中的每个事件。
Stream<E> asyncMap<E>(FutureOr<E> Function(T event) convert);

// 如果提供了比较函数,则返回由不重复元素组成的流,否则比较元素的相等操作符。
Stream<T> distinct([bool Function(T previous, T next)? equals]);

// 处理流中的错误事件。
Stream<T> handleError(Function onError, {bool Function(dynamic error)? test});

// 如果流中的事件处理超过时间限制,则执行超时操作。
Stream<T> timeout(Duration timeLimit,
    {void Function(EventSink<T> sink)? onTimeout});

// 使用指定的流转换器转换流。
Stream<S> transform<S>(StreamTransformer<T, S> streamTransformer);

😑 折腾 Stream流 的 API/操作符 太多了,限于篇幅,这里只是简述函数作用,具体使用示例可以看下张风捷特烈大佬的《【Flutter 异步编程 - 玖】 | 学习 Stream 的元素转换操作》~

2.3. Isolate (隔离)

🤡 前面讲概念的时候已经演示过如何使用Isolate了:

在执行 计算密集型耗时任务 的场景,创建新的isolate来处理耗时任务,避免堵塞主isolate,任务执行完毕后通过端口通知主isolate。

先补充下这四个角色的描述吧:

  • Isolate独立的Dart执行上下文,可以通过 spwan() 或 spawnUri() 来创建一个新的 isolate;
  • ReceivePort & SendPort:收发其它Isolate消息的端口,可以通过 sendPort属性获取一个SendPort对象,用于发送消息给对应的ReceiverPort;
  • Capability:isolate的唯一标识,用于控制isolate的暂停和恢复;

再写个简单代码示例:

dart 复制代码
  void doCPUTask() async {
    // 创建一个ReceivePort,用来接收来自新Isolate的消息
    final receivePort = ReceivePort();

    // 创建并启动一个新的Isolate,它将并行执行累加操作
    final isolate = await Isolate.spawn((sendPort) {
      int sum = 0;
      for (int i = 0; i < 1000000000; i++) {
        sum += i;
      }
      // 计算完毕,通过Port发送结果回主Isolate
      sendPort.send(sum);
      sendPort.send('Hello');
      sendPort.send('World');
      // 发送一个错误消息给子Isolate
      sendPort.send(Error());
      sendPort.send('exit');
    }, receivePort.sendPort);

    // 监听ReceivePort的信息
    receivePort.listen((message) {
      // 打印接收到的消息
      print('接收到消息: $message');
      // 如果接收到的消息是'exit',则关闭ReceivePort并杀死子Isolate
      if (message == 'exit') {
        receivePort.close();
        isolate.kill();
      }
    }, onError: (error, stackTrace) {
      print("处理错误信息:$error}");
    }, onDone: () {
      print("ReceivePort 关闭");
    });
  }

😭 这里有个小坑务必注意!!!我想把 spawn()的第一个函数抽取成一个单独的函数:

dart 复制代码
// 具体执行的CPU密集型操作
void echo(SendPort sendPort) {
  int sum = 0;
  for (int i = 0; i < 1000000000; i++) {
    sum += i;
  }
  // 计算完毕,通过Port发送结果回主Isolate
  sendPort.send(sum);
  sendPort.send('Hello');
  sendPort.send('World');
  // 发送一个错误消息给子Isolate
  sendPort.send(Error());
  sendPort.send('exit');
}

// 创建并启动一个新的Isolate,它将并行执行累加操作
final isolate = await Isolate.spawn(echo, receivePort.sendPort);

结果一直报这个错;

dart 复制代码
Unhandled Exception: Invalid argument(s): Illegal argument in isolate message: object is unsendable - Library:'dart:async' Class: _AsyncCompleter@4048458 (see restrictions listed at `SendPort.send()` documentation for more

搜了一圈网上的文档,都说 sendPort.send() 试图 发送一个不可发送的对象 ,只能发送简单的值类型,但是我发送的对象都是可以传递的,更玄学的是我把send()部分的代码注释了,还是报这个错。最后发现报错的真正原因: isolate.spawn() 是一个 静态方法 ,这个函数必须为 静态或顶级函数!!!只需要改下echo()函数的位置,就TM好了💀...

另外,Dart 2.15 开始支持传递一个 立即调用的闭包 作为第一个参数,这也是我一开始写的代码没报错的原因~

Tips:🐶2333,本来还想刨下这些异步API源码,看看背后的实现原理,后面想想还是算了,篇幅太长,读者看着也难受,后面再专门开一篇来跟跟源码吧,本节就酱~

参考文献

相关推荐
m0_748247808 分钟前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
迷雾漫步者1 小时前
Flutter组件————PageView
flutter·跨平台·dart
枫叶丹44 小时前
【HarmonyOS之旅】HarmonyOS开发基础知识(三)
华为od·华为·华为云·harmonyos
迷雾漫步者9 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
SoraLuna9 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
拭心11 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
AORO_BEIDOU13 小时前
单北斗+鸿蒙系统+国产芯片,遨游防爆手机自主可控“三保险”
华为·智能手机·harmonyos
带电的小王13 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
coder_pig13 小时前
📝小记:Ubuntu 部署 Jenkins 打包 Flutter APK
flutter·ubuntu·jenkins
梦想平凡13 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle