Appearance
flutter-background-boundary
1. 实验目标与工程要点
用最小页面验证 Flutter 的后台执行边界:Dart 代码不能给自己争取后台执行窗口。
WidgetsBindingObserver.didChangeAppLifecycleState记录inactive / hidden / paused / resumed(detached部分平台才会出现)- 用
Timer.periodic(1s)模拟「后台同步任务」,观察 App 进paused后 tick 是否还在触发 - 回到
resumed时用 墙钟秒数 vs 实际 tick 次数 的差值,量化后台期间被系统挂起吞掉的时间
工程要点:
- observer 必须
initState注册、dispose注销,成对出现 - 「Dart 任务还活着」≠「还在执行」:paused 后定时器被冻结,isolate 不改变这一点
- 真正可靠的后台要落到原生能力(WorkManager / FGS / BGTask),Flutter 只做调用与展示层
2. 关键实现速览
2.1 生命周期监听 + 事件日志
dart
class _FlutterBackgroundBoundaryDemoPageState
extends State<FlutterBackgroundBoundaryDemoPage>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_log('initState:已注册 WidgetsBindingObserver');
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.paused:
_log('AppLifecycleState.paused:进后台 → Dart 定时器即将被冻结');
break;
case AppLifecycleState.resumed:
_reportMissedTicks();
break;
// inactive / hidden / detached 同样记录 ...
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}为什么看这段:它把宿主 App 的前后台信号变成可读日志,是判断「页面没销毁但任务停了」的第一手证据。
2.2 模拟后台任务与挂起量化
dart
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
_tickCount++;
_log('模拟同步任务 tick #$_tickCount');
});
// 回前台时对比:如果 Dart 任务能在后台继续跑,missed 应恒为 0
final wallSeconds = DateTime.now().difference(startedAt).inSeconds;
final missed = wallSeconds - _tickCount;
_log('量化:墙钟走了约 $wallSeconds 秒,任务只执行了 $_tickCount 次,'
'约 $missed 秒被系统挂起吞掉');为什么看这段:Timer.periodic 是典型「Dart 运行时任务」,它不会自动升级成后台服务;missed ≈ 后台停留秒数 就是「isolate 不等于保活」的可观测证据。
完整源码:background_boundary_demo_page.dart
3. 运行方式
bash
cd labs/flutter/host
flutter pub get
flutter run -d macos # 或 ios / android(真机效果最明显)
# 首页进入「Flutter 后台边界演示」4. 预期现象与观察重点
场景 A:启动模拟任务并停留前台
预期顺序:
text
已启动模拟后台同步任务(Timer.periodic 1s)
→ 模拟同步任务 tick #1 → #2 → #3 ...观察点:
- 前台每秒稳定追加一条 tick
场景 B:切到后台再回来(核心场景)
预期行为:
- 切后台瞬间日志追加
inactive/hidden/paused - 后台停留期间没有任何新 tick
- 回前台追加
resumed+ 一条量化日志,形如:
text
量化:墙钟走了约 12 秒,任务只执行了 3 次,约 9 秒被系统挂起吞掉观察点:
missed是否约等于你在后台停留的时长- 页面状态没有销毁(日志历史还在),但任务确实停了 —— 这就是边界本身
场景 C:桌面端对照(macOS)
预期行为:
- macOS 上窗口最小化/遮挡的行为与移动端不同,tick 可能不会立刻冻结
观察点:
- 同一份 Dart 代码在不同宿主上后台约束不一致,说明边界由平台决定,不由 Flutter 决定
5. 常见误区
| 误区 | 实际情况 |
|---|---|
| Timer / Future 在退后台后还能稳定跑 | paused 后定时器被冻结,回前台也不会补跑错过的 tick |
| 开一个 isolate 就能后台保活 | isolate 解决计算隔离,不改变系统对进程的后台授权 |
收到 detached 就能做清理 | detached 仅部分平台在终止前出现,不能当可靠回调 |
| 页面日志还在 = 任务一直在执行 | 状态保留与执行窗口是两回事:内存活着 ≠ 时间片在走 |
6. 复习检查题
为什么 Dart 定时器进后台就停了?
答:因为 App 进入
paused后宿主系统挂起/限制进程执行,Dart 运行时拿不到时间片;这是平台约束,不是代码问题。这个实验怎么证明「isolate 不是保活方案」?
答:
resumed后的量化日志显示墙钟时间与实际执行次数的差值约等于后台时长——即使把任务挪进 isolate,进程级挂起同样会冻结它。需要可靠完成的下载/同步应该落在哪一层?
答:原生层(Android WorkManager/FGS、iOS BGTask 等)承接执行与授权,Flutter 只做调用入口与前台展示。
7. 对应知识库文档
- 理论主文档:Flutter 后台能力边界