面试
Flutter面试题1
事件循环、Future / Stream、Isolate、热重载与 JIT / AOT。
和 高频面试题 2 重叠的概念,这里写 Dart / 发布视角。
事件循环怎么跑
考察事件循环与执行模型。
- 用什么驱动应用?
事件循环,调度输入、动画和 UI 更新。由 Engine + Dart VM 组成。 - 大致流程:
- 引擎处理输入、动画
- Dart VM 跑业务
- 引擎更新界面并上屏
- 进入下一轮
- Dart 单线程还是多线程?
单线程。一条事件循环 + 两个队列。async不会开线程。 - 微任务队列和事件任务队列,微任务队列优先:
- 同步代码跑完
- 清空微任务队列
- 从事件队列取一个任务
- 再回到微任务
print('sync');
scheduleMicrotask(() => print('micro'));
Future(() => print('event'));
// sync → micro → event
| 微任务 | 事件 | |
|---|---|---|
| 谁在这 | scheduleMicrotask、then、await 续体 | Future(() {})、Timer、IO、点击、帧 |
| 规则 | 当前轮先清空 | 一次只取一个 |
面试点:事件驱动 + 帧循环;「单线程 + 两个队列 + 微任务优先」。
Future 和 Stream
- 差在哪?
Future:一次结果。Stream:一串结果。async/await只是 Future 的写法。 - Stream 两种形态:
- 单订阅(Single-subscription)
只能被订阅一次,第二次listen会报错。事件按顺序交给唯一消费者。
典型:HTTP 响应体、文件读、一次性请求结果流。 - 广播(Broadcast)
可以多次订阅,多个订阅者同时收到同一批事件。后订阅的人通常只收到之后的事件。
典型:全局事件总线、通知广播。默认StreamController是单订阅,广播用StreamController.broadcast()或asBroadcastStream()。
- 单订阅(Single-subscription)
- 会阻塞吗?
不阻塞事件循环。await/listen/await for只暂停当前函数,控制权交回循环,UI 还能跑。
但 Future / Stream 回调里的同步重活仍在主 Isolate,会堵帧。IO 等结果不堵;解析、加密、大循环要丢 Isolate。 - 容易漏的点:
Future 忘了处理 error;Stream 忘了cancel。单订阅流不要当事件总线用。
| Future | Stream | |
|---|---|---|
| 结果 | 一次 | 一串 |
| 典型 | 请求、读文件 | 分页、socket、EventChannel |
| 收尾 | try/catch | dispose 里 cancel |
| 单订阅 | 广播 | |
|---|---|---|
| 订阅 | 只能一次 | 可以多次 |
| 消费者 | 一个 | 多个同时收 |
| 典型 | 请求结果流、读文件 | 事件总线、通知广播 |
面试点:单订阅 = 单消费者,广播 = 多消费者。一次结果不必上 Stream。
async 不阻塞、也不开线程。Isolate 怎么隔离
- 是什么?
Dart 的并发单元:独立堆、独立事件循环,不共享内存,靠 Port 传消息。 - 什么时候用?
解析大 JSON、加密、压图这类 CPU 重活。async/await只是「先去做别的,算完再回来」,计算本身还在主 Isolate,UI 照样卡。要另开一个 Isolate 才能真并行。
final data = await Isolate.run(() => parseHugeJson(raw));
| Isolate | async | |
|---|---|---|
| 解决 | 不堵 UI 的 CPU 重活 | 等 IO |
| 内存 | 不共享 | 仍在主 Isolate 同一堆 |
| 限制 | 不能 setState,结果要传回来 | 不增加核 |
面试点:主 Isolate 跑 UI,后台 Isolate 跑重任务。不是
async 的升级版,也不是 Java 线程。热重载、热重启、热更新
- 冷启动: 从 0 启动,最慢。
- 热重载: 再执行
build,尽量保状态。 - 热重启: 清 Dart 状态,重跑
main(),不重装 App。 - 热更新: 线上发补丁。官方不直接支持,偏安全和合规。
口诀:热重载改 UI、保状态;热重启清状态;热更新是发版,不是
r。Hot Reload 原理
Dart 的中间表示(IR),产物常是 .dill。不是源码,也不是 CPU 指令。源码先编成 Kernel,VM 再执行。结构稳定、可按方法替换,所以能增量更新。
本质上是以二进制形式序列化后的完整程序快照—— 一份 "已经编译完成、但还没有面向任何具体机器" 的中间产物,类比一下就懂了:它相当于 Java 的 .class(JVM 字节码)。
Debug 用 JIT,走的就是这份可增量更新的 Kernel:
- 扫描改动
- 只把改过的部分编成新 Kernel
- 推到设备上正在跑的 VM
- 和新老 Kernel 合并(换方法体,进程不重启)
- Framework 重建 Widget,尽量保状态
Release 是 AOT:Kernel 已被编死成本机二进制,不能局部替换,所以没有热重载。
热重载经常无效
StatelessWidget→StatefulWidget- 全局 /
static初始化变了 main()、initState()变了- 枚举 / 泛型破坏性调整
- 改了原生或插件
| 热重载 | 热重启 | 热更新 | |
|---|---|---|---|
| 做什么 | 增量换 Dart | 重跑 main() | 线上补丁 |
| 状态 | 尽量保 | 清空 | 和开发期不是一类 |
面试点:Stateless 改 Stateful 要热重启。热更新不要讲成热重载的线上版。
JIT 和 AOT
| JIT | AOT | |
|---|---|---|
| 模式 | Debug | Release / Profile |
| 产物 | Kernel,可增量替换 | 本机二进制 |
| 图什么 | 热重载、调试 | 启动、帧率、上架 |
看卡顿用 profile,看包体用 release,不要拿 Debug 包说事。
为什么能热重载:Debug 用 JIT → 增量 Kernel 推到 VM → 合并后重建 Widget,进程不重启。