Appearance
内存、生命周期、系统约束
一句话定位
这一模块不是把“内存 / 生命周期 / 后台限制”并排罗列,而是按知识导航树收口为一条判断主线:
内存层 → Android 页面层 → Flutter 页面层 → iOS 约束层
读完后,应该能先判断问题属于哪一层,再决定继续看对象引用、宿主页面、Flutter 页面状态,还是系统授权边界。
本篇覆盖声明
- 主线深写:JVM / Android / Kotlin / Dart / Flutter 的内存、生命周期、前后台限制
- 必要对照:iOS 只保留帮助跨端判断所必需的生命周期与后台限制边界
- 暂不展开:服务端与 Web 不在本模块主线,只在对照表里保留最低必要映射
复习目标不是背 API,而是建立四级导航判断:
- 内存层:对象在哪、为什么没释放、GC 为什么抖动
- Android 页面层:宿主页面什么时候创建、覆盖、恢复、销毁
- Flutter 页面层:Widget / Element / State 为什么重建、缓存、dispose
- iOS 约束层:跨端方案落到 iOS 时,系统为什么不保证同等后台窗口
为什么需要这棵知识树
这部分如果不先分层,Android / Flutter / iOS 的很多线上问题会被误判:
- 把 对象没释放 误判成 生命周期没走
- 把 页面没销毁 误判成 系统一定还允许继续执行
- 把 Flutter 页面没 dispose 和 Android Activity 没 onDestroy 混成一件事
- 把 Android 能做的后台方案直接套到 iOS,导致设计先天不可落地
- 把“异步”“线程”“isolate”“后台任务”当成同一类能力,结果选型失真
对 Android 背景转 Flutter / 跨端的工程师来说,这一模块的价值是:
- 把 Java/Kotlin/JVM 经验 映射到 Dart/Flutter
- 把 对象生命周期、页面生命周期、系统授权边界 拆开理解
- 把 宿主页面行为 和 Flutter 页面行为 分开看
- 为后续
03-background-tasks、04-state-routing-architecture建立前置判断
知识导航树:从对象到系统约束
第一层:内存层(先判断对象为什么还活着)
先读:JVM / ART / Dart / Flutter 内存模型总览
这一层解决:
- 对象在堆、栈、引用链里处于什么状态
- GC 为什么没有立刻回收
- Dart isolate 为什么不是 JVM 线程共享堆模型
- Android / Flutter 泄漏问题为什么经常不是“页面没关掉”这么简单
内存层深入专题:
- JVM / ART 垃圾回收算法与 GC 演进深度剖析 — JVM / ART GC Roots、可达性、收集器与停顿(Android/JVM 分支第一篇)
- Java / Android 引用类型与 Dart 弱引用对照 — Java 强 / 软 / 弱 / 虚引用与 Dart 弱引用对照
- Android / Java 内存泄漏定位、LeakCanary 原理与 Profiler 实战 — 泄漏定位:LeakCanary / Profiler / 常见持有链(依赖 GC 与引用知识)
- Dart GC 机制、对象分配模式与主 Isolate 卡顿归因 — Dart GC、分配模式、Dart Heap 与主 isolate 卡顿(Flutter 分支)
- Android View 绘制流程、Choreographer 与 VSync 帧同步 — View 绘制三部曲、Choreographer 与 VSync 帧同步
- 10. iOS ARC 自动引用计数与 Java/Dart GC 跨端对照 — iOS ARC 自动引用计数与 Java/Dart GC 跨端对照
- Android View 绘制流程、Choreographer 与 VSync 帧同步 已在本层列出,下面两条是与之相邻的显示链路专题
- SurfaceFlinger、BufferQueue 与三重缓冲:帧如何合成上屏 — SurfaceFlinger / BufferQueue / 三重缓冲,Choreographer 之后帧如何合成上屏
- iOS Core Animation:CALayer、渲染服务与显示链路 — iOS Core Animation 图层合成、离屏渲染与 CADisplayLink 显示链路
适合带着这些问题进入:
- 页面退出了,内存为什么没降?
- 一句话答:页面结束只走完 UI 生命周期,只要还有强引用链(单例 / 缓存 / 回调持有页面对象),GC 就不会回收,内存自然不降。
- 明明已经
dispose/onDestroy,对象为什么还在?- 一句话答:
dispose/onDestroy只是通知回调,并不会切断其它地方对对象的引用链;对象仍可达,回收就不会发生。
- 一句话答:
- 短命对象很多时,为什么主线程 / 主 isolate 会抖?
- 一句话答:大量临时对象会持续触发分配与 GC,GC 暂停 / 回收本身会占用主线程或主 isolate 时间,表现为卡顿抖动。
内存专题推荐顺序
总览页只负责建立共同坐标,深入内容按问题分成两条路径:
text
01 JVM / ART / Dart / Flutter 内存模型总览
├── Android / JVM:05 GC → 08 引用 → 06 泄漏 → 02 Android 生命周期
└── Dart / Flutter:07 Dart GC → 03 Flutter 生命周期 → 图片/FFI/Engine 内存- Android/JVM 路径:先理解 GC Roots 和可达性,再理解引用强度,最后使用 LeakCanary、Hprof 和 Profiler 定位真实持有链。
- Dart/Flutter 路径:先理解 Dart 对象分配和 isolate heap,再结合 Widget/Element/State 生命周期,最后处理图片缓存、FFI、Native 和 GPU 资源。
- JMM 旁路:如果问题是“线程 A 的写入何时对线程 B 可见”,转到 JMM:happens-before / 安全发布;它不是 GC 或对象布局专题。
第二层:Android 页面层(先建立宿主页面判断)
这一层解决:
onCreate → onStart → onResume → onPause → onStop → onDestroy的执行顺序- 页面是被覆盖、返回、重建,还是因为配置变更重走一遍
- Fragment 生命周期与 View 生命周期为什么要拆开看
- ViewModel / SavedStateHandle 在页面销毁与恢复之间扮演什么角色
对应实验入口:
- android-activity-lifecycle-basic
- android-activity-lifecycle-compose
- android-activity-lifecycle-java-xml
- viewmodel-savedstate-demo —— ViewModel 跨旋转 + SavedStateHandle 进程恢复基础实验
适合带着这些问题进入:
- 页面只是不可见,还是已经进入销毁路径?
- 一句话答:不可见(
onStop)≠ 销毁(onDestroy);是否进入销毁路径才决定状态如何存活、资源是否释放。
- 一句话答:不可见(
- 为什么旋转屏幕后页面像“重建”但业务状态不一定全丢?
- 一句话答:配置变更走“销毁 + 重建”,但
ViewModel跨配置变更存活、SavedStateHandle保存可持久化状态,所以业务数据不一定丢。
- 一句话答:配置变更走“销毁 + 重建”,但
- 为什么
onStop并不等于拿到了后台长期执行许可?- 一句话答:
onStop只代表页面不可见,进程随时可能被 LMK / 系统回收;后台执行窗口由系统政策决定,不由回调决定。
- 一句话答:
第三层:Flutter 页面层(再把宿主经验迁移到 Flutter)
这一层解决:
- Widget / Element / RenderObject 三层模型分别负责什么
initState / didChangeDependencies / build / deactivate / dispose各自意味着什么AppLifecycleState与宿主前后台是怎样映射的- 页面返回、缓存、keepAlive、路由恢复为什么不能直接套 Android Activity 心智
对应实验入口:
适合带着这些问题进入:
- 为什么返回上一页后又
build了一次?- 一句话答:路由变化会改动 Element 树,触发重新
build;build是渲染流水线的一环,不等于重建 State。
- 一句话答:路由变化会改动 Element 树,触发重新
- 为什么
build不能当onCreate用?- 一句话答:
build会被频繁调用(依赖变化、父级 rebuild、尺寸变化),不是一次性初始化时机;一次性初始化应放initState。
- 一句话答:
- Flutter 页面状态丢失,到底是 Widget 重建、Element 替换,还是宿主生命周期变化?
- 一句话答:三者表现不同——Widget 重建不丢 State;Element 子树销毁才丢 State;宿主前后台变化触发
AppLifecycleState,需先定位是哪一种。
- 一句话答:三者表现不同——Widget 重建不丢 State;Element 子树销毁才丢 State;宿主前后台变化触发
第四层:iOS 约束层(最后收口跨端设计边界)
最后读:iOS 生命周期与后台限制
这一层解决:
- active / inactive / background / suspended / terminated 各是什么现实状态
- 为什么 iOS 不保证 Android 式通用后台能力
- BGTask / Background Fetch 为什么只能当机会窗而不是准点执行
- 跨端方案到了 iOS,哪些能力必须收缩成保守设计
适合带着这些问题进入:
- Android / Flutter 方案迁到 iOS 时,哪里会失效?
- 一句话答:依赖 Android 通用后台能力(WorkManager 等价物、开机自启、无限制后台执行)的部分会失效;iOS 只有白名单例外和系统给的机会窗。
- 哪些后台能力属于白名单例外,而不是通用能力?
- 一句话答:后台音频、定位、VoIP、BGTask 等需声明能力且受系统条件约束,属于例外而非任何 App 都有的通用执行窗口。
- 为什么“技术上能写”不等于“系统一定会给执行窗口”?
- 一句话答:能否写代码与系统是否授权并调度执行是两个层面;iOS 由系统决定何时、给多少后台执行机会。
推荐阅读路径:按问题而不是按文件名走
路径 A:先查“对象为什么没释放”
- JVM / ART / Dart / Flutter 内存模型总览
- Android 生命周期总览(若问题发生在 Android 宿主)
- Flutter 生命周期总览(若问题发生在 Flutter 页面)
- iOS 生命周期与后台限制(若最终要做跨端收口)
路径 B:先查“页面为什么重建 / 恢复路径不对”
路径 C:先查“离开页面后还能不能继续做事”
路径 D:先看渲染与显示链路(与内存/生命周期相邻的显示层)
- Android View 绘制流程、Choreographer 与 VSync 帧同步(Choreographer / VSync 驱动绘制)
- SurfaceFlinger、BufferQueue 与三重缓冲:帧如何合成上屏(帧如何合成上屏、三重缓冲为何抗抖动)
- iOS Core Animation:CALayer、渲染服务与显示链路(iOS 侧 CALayer / 渲染服务 / 离屏渲染,与 Android 对位)
导航总表:每一层读什么
| 导航层 | 先回答什么 | 对应文档 | 关键入口 |
|---|---|---|---|
| 内存层 | 对象为什么还活着 / 为什么 OOM / 为什么 GC 卡顿 | JVM / ART / Dart / Flutter 内存模型总览 | 内存区域、GC、isolate 独立堆、泄漏判断 |
| Android 页面层 | 宿主页面为什么创建、覆盖、恢复、销毁 | Android 生命周期总览 | android-activity-lifecycle-basic · android-activity-lifecycle-compose · android-activity-lifecycle-java-xml |
| Flutter 页面层 | Widget / Element / State 为什么重建、缓存、dispose | Flutter 生命周期总览 | flutter-lifecycle-observer |
| iOS 约束层 | 跨端方案落到 iOS 时系统为什么不保证同等后台窗口 | iOS 生命周期与后台限制 | 前后台、挂起、后台模式、BGTask 边界 |
分层概念地图:不要把四层问题混掉
| 层级 | 关注对象 | 核心问题 | 常见误判 |
|---|---|---|---|
| 内存层 | 对象、引用、堆、栈、GC、isolate heap | 为什么对象还活着 / 为什么 OOM / 为什么 GC 卡顿 | 以为页面退出对象就会自动释放 |
| Android 页面层 | Activity、Fragment、ViewModel、SavedStateHandle | 为什么页面重建 / 为什么返回会再执行 / 状态如何保存 | 以为 onResume / onDestroy 足以解释所有状态问题 |
| Flutter 页面层 | Widget、Element、State、路由栈、AppLifecycleState | 为什么 build 重跑 / 为什么状态丢失 / 为什么 dispose 没按想象触发 | 以为 Flutter 页面等同于 Android Activity |
| iOS 约束层 | 进程、前后台、挂起、系统调度窗口 | 退后台后还能不能跑 / 哪些能力受 OS 白名单控制 | 以为异步任务或 isolate 就等于后台能力 |
底层机制:这一模块真正要建立什么判断
1. 内存回收不是页面回调
- Activity
onDestroy、Flutterdispose、iOS 进入 background,都不是 GC 回收动作本身 - 真正决定对象能否回收的,是引用链是否断开、GC 何时扫描、宿主/框架是否还保留对象
- 所以“页面结束了对象为什么还活着”通常要回到 引用关系 查,不是先怪生命周期回调
2. Android 页面层与 Flutter 页面层要分开看
- Android 宿主页面回答的是 Activity / Fragment 何时进入、覆盖、恢复、销毁
- Flutter 页面层回答的是 Widget / Element / State 何时重建、缓存、dispose
- 同一个“返回上一页”动作,在两层里触发的机制并不相同
3. 页面生命周期不是系统后台授权
onStop不等于“你还能放心继续跑长任务”- Flutter
AppLifecycleState.paused不等于“Dart 代码还能无限执行” - iOS
background更不等于“拿到了持续后台窗口”
也就是说:
- 页面层 回答“用户界面现在处于什么阶段”
- 系统层 回答“OS 现在还允许你做什么”
4. Android / Flutter / iOS 的共同问题,落点却不同
同一个业务需求“页面离开后继续做事”,在不同层里可能对应:
- 释放页面绑定资源,避免泄漏
- 把 UI 任务迁移到 ViewModel / repository
- 把短任务交给前台内存中的异步执行
- 把可靠任务交给 WorkManager / 系统托管下载
- 在 iOS 上改产品策略,接受机会型刷新而不是准点执行
跨平台对照表
| 维度 | Android / JVM | Flutter / Dart | iOS | 复习时该抓什么 |
|---|---|---|---|---|
| 对象默认承载 | 线程共享堆 + 线程栈 | isolate 独立堆 + 事件循环 | ARC + 系统生命周期约束 | 对象活多久和页面回调不是一回事 |
| 页面前台可交互 | onResume | 页面在前台 + 可交互;App 级看 resumed | active | 不要把“可见”与“可交互”混掉 |
| 页面离开前台 | onPause / onStop | 路由覆盖 + AppLifecycleState 变化 | inactive / background | 页面退下去不等于进程还能一直跑 |
| 页面最终销毁 | onDestroy | dispose | 可能被挂起或终止 | 销毁时机不等于对象立即释放 |
| 长任务承载 | 协程 / 线程 + WorkManager / FGS 等 | Future / isolate,但受宿主限制 | 白名单后台模式 / BGTask | 先问系统是否授权,再谈实现 |
| 高频事故 | 泄漏、重复请求、状态丢失、后台限制误判 | build 重复、副作用乱放、setState after dispose | 误把 iOS 当 Android 用 | 先分清内存层、Android 页面层、Flutter 页面层、iOS 约束层 |
常见场景:按导航树落位
1. Android 页面关了,内存没降
典型判断顺序:
- 先回到内存层,看引用链是否真的断开
- 再看 Android 页面层 是否真的走到
onDestroy - 再看有没有单例、静态引用、observer、handler、协程作用域持有页面
- 最后区分只是 GC 还没发生,还是引用链真的没断
2. Flutter 返回上一页后又执行了一遍
典型判断顺序:
- 先定位到 Flutter 页面层,确认是
build再跑,还是initState/dispose真发生了 - 再看副作用是不是误写在
build - 再看状态放在页面 State 里还是更上层作用域里
- 若页面退后台,还要同时看宿主层与
AppLifecycleState
3. 跨端要做“后台继续同步”
典型判断顺序:
- 先问问题还停留在页面层,还是已经升级到系统约束层
- 如果只是页面切换,可能只要脱离页面作用域即可
- 如果是 Android 真后台可靠任务,要继续看
../03-background-tasks/README.md - 如果要兼容 iOS,先假设没有通用准点保活,再反推产品方案
常见坑
| 坑 | 典型现象 | 正确收口 |
|---|---|---|
| 把对象生命周期和页面生命周期混为一谈 | 页面退出后还怀疑“为什么对象不死” | 先查引用链、GC 时机、缓存持有者 |
把 Flutter build 当 onCreate | 返回页面或父组件刷新就重复请求 | 一次性初始化放 initState / 状态层 |
把 Android onStop 当后台长期执行许可 | 退后台后任务被系统限制或杀掉 | 页面回调只说明 UI 阶段,不说明 OS 授权 |
| 以为 Flutter isolate 能绕过 iOS/Android 后台限制 | 代码写了但系统不给执行机会 | isolate 解决 CPU 与隔离,不解决后台授权 |
| 把 iOS BGTask 当 WorkManager/AlarmManager | 执行时机漂移大、不触发 | 只把它当机会窗,不当准点调度 |
相近概念对比
| 概念 | 它回答的问题 | 不回答什么 |
|---|---|---|
| GC | 对象何时可能被回收 | 页面什么时候回调 |
| Activity / Fragment 生命周期 | Android 宿主页面何时进入、退出、重建 | 系统是否允许长期后台运行 |
| Widget / State 生命周期 | Flutter 页面何时重建、缓存、dispose | 某个对象一定是否释放 |
| AppLifecycleState / iOS app state | App 是否在前台、后台、挂起边缘 | 某个任务一定是否继续执行 |
| isolate / 线程 / 协程 | 任务如何并发执行 | OS 是否授予后台执行窗口 |
对应实验
当前本模块已有实验入口:
| Lab | 说明 | 关键源码 |
|---|---|---|
| android-activity-lifecycle-basic | android-activity-lifecycle-basic | LifecycleLogActivity.kt · ActivityDetailActivity.kt |
| android-activity-lifecycle-compose | android-activity-lifecycle-compose | ComposeLifecycleActivity.kt |
| android-activity-lifecycle-java-xml | android-activity-lifecycle-java-xml | JavaLifecycleActivity.java |
| viewmodel-savedstate-demo | viewmodel-savedstate-demo | ViewModelSavedStateDemoActivity.kt · CounterViewModel.kt |
| flutter-lifecycle-observer | flutter-lifecycle-observer | lifecycle_observer_page.dart |
计划补齐但当前仍缺的实验:
labs/flutter/host/topics/memory-lifecycle/flutter-page-state-restoration/README.md— 验证路由返回、状态保留、恢复边界labs/dart/host/topics/memory-lifecycle/dart-gc-allocation-patterns/README.md— 验证短命对象分配、主 isolate 卡顿与 isolate 拆分边界
复习检查题
为什么这一模块要改成“内存层 → Android 页面层 → Flutter 页面层 → iOS 约束层”的导航树?
答:因为对象是否释放、Android 宿主页面是否重建、Flutter 页面状态为何变化、系统是否允许后台执行分别由不同机制决定。按这条树来读,才能先定位问题层级,再进入对应专题,不会把 GC、页面回调和系统授权混成一团。
为什么 Android / Flutter 页面结束,不等于对象立刻释放?
答:因为页面回调只说明 UI 实例进入某个阶段,真正决定对象能否回收的是引用链是否断开、GC 何时发生,以及框架/缓存/单例是否仍在持有对象。
为什么 Flutter 页面层要单独从 Android 页面层拆出来?
答:因为 Flutter 的 Widget / Element / State 重建模型,与 Android Activity / Fragment 的宿主生命周期不是一一对应关系。如果不单列出来,最容易把
build误当onCreate,或者把宿主前后台变化和页面 State 变化混看。如果需求是“用户离开页面后上传继续,且要跨端可落地”,第一步该问什么?
答:第一步不是选线程、协程还是 isolate,而是先问这是页面离开后的短任务,还是 App 退后台后的可靠任务;只有先澄清系统层要求,后续才知道该放页面外作用域、Android 后台能力,还是干脆改成服务端/机会型同步方案。
后续导航建议
结论:还可以继续补,但优先补“映射页”,不是盲目加主题。
建议后续补两类内容:
模块内跨端映射页(优先级高)
- 建议文件:
docs/02-memory-lifecycle/05-cross-platform-memory-lifecycle-mapping.md - 作用:把 Android Activity / Fragment、Flutter Widget / AppLifecycleState、iOS app state 放在同一张执行顺序表里
- 重点:只做“映射与误判边界”,不重复展开各端完整机制
- 建议文件:
模块间导航页或 README 小节(优先级中)
- 从本模块明确跳到:
03-background-tasks:当问题已经从页面层升级到系统后台可靠执行04-state-routing-architecture:当问题核心变成状态保存、作用域与路由恢复
- 作用:告诉读者什么时候该继续看后台任务,什么时候该继续看状态管理
- 从本模块明确跳到:
速记
- 对象活着:先查引用链,不先怪生命周期
- Android 页面问题:先分 Activity / Fragment / ViewModel 是哪条线
- Flutter 页面问题:先分 Widget / Element / State /
AppLifecycleState是哪条线 - 后台能否跑:先问 OS 是否授权,不先谈线程/协程/isolate
- 跨端收口到 iOS:默认保守设计,接受机会窗,不假设通用保活