Flutter面试:Dart基础2

文章目录

Dart是不是单线程模型?是如何运行的?

一句话总结

  • Dart 是单线程模型,它的核心是事件循环机制 ,通过微任务队列事件队列来高效地处理异步操作,从而保证了UI线程的流畅性。

核心概念

Dart是单线程模型,不必像在Java或C#中那样担心共享内存的锁竞争和线程同步问题。运行机制可以从以下三个层面来理解:

1. 核心机制:事件循环 (Event Loop)

Dart 的事件循环依赖两个有序队列,按优先级执行:

  • 微任务队列 (Microtask Queue) :优先级最高;通过 scheduleMicrotask() 显式添加,附着在这个 Future 上的回调(包括 thencatchErrorwhenComplete 以及 await 的续延代码)都会被调度到微任务队列中执行。
  • 事件队列 (Event Queue) :我们日常使用的绝大部分异步操作都来源于这里,例如I/O操作(文件、网络请求)、手势识别绘图消息Timer (Future.delayed)。
事件循环的工作流程图:
flowchart TD A[事件循环启动] --> B{微任务队列
是否为空?} B -- 否 --> C[取出并执行
一个微任务] C --> B B -- 是 --> D{事件队列
是否为空?} D -- 否 --> E[取出并执行
一个事件] E --> F[事件执行完毕] F --> B D -- 是 --> G[等待新事件到来] G --> B
事件循环代码演示:
dart 复制代码
void main() {
  // ------------------------------
  // 1. 同步代码(优先执行,事件循环启动前)
  // ------------------------------
  print("1. 同步代码 - 开始执行");

  // ------------------------------
  // 2. 微任务队列(Microtask Queue):优先级高于事件队列
  //    通过 Future.microtask 添加
  // ------------------------------
  Future.microtask(() {
    print("2. 微任务A - 微任务队列");
  });

  // ------------------------------
  // 3. 事件队列(Event Queue):优先级低于微任务队列
  //    通过 Future 或 Future.delayed 添加(即使延迟0秒)
  // ------------------------------
  // 事件1:执行时会新增微任务
  Future(() {
    print("4. 事件1 - 事件队列(开始执行)");
    // 在事件执行过程中新增微任务
    Future.microtask(() {
      print("5. 微任务B(事件1中新增) - 微任务队列");
    });
    print("6. 事件1 - 事件队列(执行结束)");
  });

  // 再添加一个微任务(微任务队列按添加顺序执行)
  Future.microtask(() {
    print("3. 微任务C - 微任务队列");
  });

  // 事件2:在事件1之后执行
  Future.delayed(Duration.zero, () {
    print("7. 事件2 - 事件队列");
  });

  // ------------------------------
  // 1. 同步代码(继续执行)
  // ------------------------------
  print("8. 同步代码 - 执行结束(事件循环即将启动)");
}
执行结果:
plaintext 复制代码
1. 同步代码 - 开始执行
8. 同步代码 - 执行结束(事件循环即将启动)
2. 微任务A - 微任务队列
3. 微任务C - 微任务队列
4. 事件1 - 事件队列(开始执行)
6. 事件1 - 事件队列(执行结束)
5. 微任务B(事件1中新增) - 微任务队列
7. 事件2 - 事件队列
步骤拆解(对应执行结果):
  1. 同步代码优先执行
    打印 18(同步代码不进入任何队列,事件循环启动前就执行完毕)。
  2. 事件循环启动,先清空微任务队列
    按添加顺序执行微任务 A 和 C,打印 23(微任务队列此时为空)。
  3. 处理事件队列的第一个事件(事件 1)
    • 执行事件 1 的代码,打印 46
    • 事件 1 执行过程中新增微任务 B(加入微任务队列)。
  4. 事件 1 执行完毕后,再次清空微任务队列
    执行新增的微任务 B,打印 5(微任务队列再次为空)。
  5. 处理事件队列的下一个事件(事件 2)
    执行事件 2,打印 7(事件队列此时为空)。
  6. 所有队列清空,程序结束

2. 实现异步的关键:Future 和 async/await

  • Future :它不是一个并行计算的结果,而是一个 "承诺" ,承诺在未来某个事件循环轮次中给你一个值(或一个错误)。当你调用http.get()Future.delayed()时,Dart会立刻返回一个Future对象,并立即返回(不会阻塞),I/O操作(网络/文件)由操作系统异步处理(Dart线程之外),主线程(Dart线程)仅处理回调。
  • async/await :这只是编写异步代码的 "语法糖" ,让异步代码看起来和同步代码一样直观。在函数前加上async关键字,其返回值会被自动包装为Futureawait关键字的作用是:告诉事件循环"我这边有个Future需要一些时间才能完成,你先去处理其它事件(微任务或UI事件),等这个Future有结果了再回来继续执行我后面的代码。" 这非常重要,它不会阻塞线程,而是让出执行权。
代码演示:
dart 复制代码
// 需安装http库 flutter pub add http
import 'package:http/http.dart' as http;

void loadData() async {
  print('1: 开始请求');
  var data = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
  ); // 不会阻塞,只是"等待"
  print('3: 收到数据: $data');
}

void main() {
  loadData();
  print('2: 执行其它操作');
}
执行结果:
plaintext 复制代码
1: 开始请求
2: 执行其它操作
3: 收到数据: Instance of 'Response'
梳理流程:
  1. 发起请求(同步) :

    • http.get() 被调用。
    • 它同步地创建一个 Future 对象并返回。
    • 它同步地发起一个系统调用(比如用 libcurl 或系统的 HTTP 库),将这个网络 I/O 操作委托给操作系统在 Dart 线程之外 执行。此时,Dart 线程本身是空闲的,可以继续执行事件循环。
  2. 遇到 await(同步 -> 异步) :

    • await 关键字检查它右边的 Future。如果这个 Future 尚未完成(http.get 返回的肯定未完成),它会暂停当前函数的执行
    • 向这个未完成的 Future 订阅一个回调 。这个回调包含了 await 之后的所有代码(即 print('3: ...'))。
    • 这个回调的调度机制,与 future.then(...) 的回调调度机制是完全相同的。
  3. 等待与完成(在Dart线程外) :

    • Dart 事件循环继续运行,处理微任务和UI事件(比如执行 print('2'))。
    • 操作系统在后台处理网络请求。
  4. 请求完成,通知Dart(事件队列) :

    • 当操作系统完成网络请求后,它会通过一种机制(如 epollkqueueIOCP)通知 Dart 运行时。
    • 这个"通知"作为一个事件,被放入 Dart 的事件队列(Event Queue)中。 这个事件本身不包含数据处理逻辑,只是一个信号。
  5. 事件循环处理完成事件(从事件队列到微任务队列) :

    • 事件循环从事件队列中取出这个"网络请求完成"的事件。
    • 处理这个事件的代码(Dart 运行时内部代码)会完成 之前 http.get 返回的那个 Future 对象(即调用 _completer.complete(response))。
    • 最关键的一步来了:当 future.complete() 被调用时,Dart 的运行时会 schedule 一个微任务。这个微任务的任务就是:执行所有通过 thencatchErrorawait 注册在这个 Future 上的回调函数。
  6. 执行回调(微任务队列) :

    • 当事件循环处理完当前的事件后,它会检查微任务队列。
    • 它发现了第5步中被调度的微任务,于是执行它。
    • 这个微任务的内容就是执行 print('3: 收到数据: $data')

操作系统完成I/O -> 发送信号至 Dart 事件队列 -> 事件循环处理该信号 -> Future.complete()被调用 -> 调度一个微任务来执行所有回调 -> 事件循环处理微任务 -> 执行 await 之后的代码。

3. 处理真正耗时计算:Isolate

事件循环和异步处理对于I/O操作是完美的,但对于CPU密集型计算 (如图像处理、加密解密、复杂JSON解析),如果计算本身就在Dart线程上运行,它就会阻塞事件循环,导致队列中的所有其他任务(包括UI渲染)都无法处理,应用就会卡顿。

Dart的解决方案是 Isolate

  • 不共享内存 :每个Isolate都有自己的独立内存堆(Heap)和事件循环 。Isolate之间不共享任何状态,通信的唯一方式是通过消息传递(Passing Messages)
  • 真正的并行:由于现代设备是多核CPU,不同的Isolate可以被调度到不同的CPU核心上真正地并行运行。
  • 通信成本 :因为不共享内存,Isolate之间传递消息时,数据会发生拷贝 。虽然对于简单数据很快,但对于大型数据(如图片、大列表),这个拷贝成本会很高。通常使用SendPortReceivePort进行通信。

在Flutter中,你可以使用Isolate.spawn()或更高级的compute()函数来将繁重任务抛到新的Isolate中执行。

代码演示:
dart 复制代码
import 'package:flutter/foundation.dart';

// 使用 compute (Flutter提供的简便API)
void main() async {
  var result = await compute(heavyTask, 1000000000);
  print(result);
}

// 这个函数会在新的Isolate中执行
int heavyTask(int count) {
  int sum = 0;
  for (int i = 0; i < count; i++) {
    sum += i;
  }
  return sum;
}
相关推荐
猿java15 分钟前
精通MySQL却不了解OLAP和 OLTP,正常吗?
java·后端·面试
weixin_4565881534 分钟前
【java面试day16】mysql-覆盖索引
java·mysql·面试
宫水三叶的刷题日记1 小时前
真的会玩,钉钉前脚辟谣高管凌晨巡查工位,小编随后深夜发文
前端·后端·面试
RaidenLiu2 小时前
从 Provider 迈向 Riverpod 3:核心架构与迁移指南
前端·flutter
胡gh3 小时前
聊一聊构建工具:Vite和Webpack
面试·webpack·vite
胡gh4 小时前
如何聊懒加载,只说个懒可不行
前端·react.js·面试
汪子熙4 小时前
浏览器里出现 .angular/cache/19.2.6/abap_test/vite/deps 路径究竟说明了什么
前端·javascript·面试
CptW5 小时前
字节面试题:实现任务调度器(Scheduler)
面试·typescript