一、生命周期回调全景
Android 的组件生命周期本质上是一份状态机契约:系统在特定时刻调用特定回调,开发者只能在这些回调里做与之匹配的事。违背契约的典型后果是资源泄漏、崩溃(如在 onSaveInstanceState 之后提交事务)或界面状态丢失。
Activity 的完整回调顺序如下:
| 阶段 | 回调 | 可做与不可做 |
|---|---|---|
| 创建 | onCreate | 初始化,不要做耗时操作 |
| 启动 | onStart | 界面可见,注册可见性相关资源 |
| 恢复 | onResume | 可交互,启动动画与传感器 |
| 暂停 | onPause | 停止动画,提交轻量数据 |
| 停止 | onStop | 释放重量资源,取消订阅 |
| 销毁 | onDestroy | 释放一切引用 |
Fragment 在此基础上多出一组与视图相关的回调:onCreateView、onViewCreated、onViewStateRestored、onDestroyView。onDestroyView 是 Fragment 最容易出错的地方:视图被销毁但 Fragment 实例仍然存活,任何持有视图引用的字段都必须在此清空。
class ProfileFragment : Fragment() {
private var _binding: FragmentProfileBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, saved: Bundle?): View {
_binding = FragmentProfileBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null // 必须置空,否则 View 无法回收
}
}
onSaveInstanceState 的调用时机不固定——它可能在 onStop 之前或之后被调用,因此不要在该回调里提交 FragmentTransaction,否则会抛 IllegalStateException: Can not perform this action after onSaveInstanceState。
一句话总结:生命周期回调不是"可选的通知",而是一份必须在正确时点做正确事情的契约,越界操作几乎都会以泄漏或崩溃的形式暴露。
二、Lifecycle 与 LifecycleOwner
直接在各回调里写资源管理逻辑会导致代码分散且难以复用。androidx.lifecycle 把这套回调抽象成可观察的对象:
Lifecycle:持有状态(INITIALIZED、CREATED、STARTED、RESUMED、DESTROYED)与事件(ON_CREATE、ON_START、ON_RESUME、ON_PAUSE、ON_STOP、ON_DESTROY)。LifecycleOwner:任何"拥有生命周期"的对象,通过lifecycle属性暴露。LifecycleRegistry:Lifecycle的默认实现,负责状态推进与事件分发。
ComponentActivity 与 Fragment 都实现了 LifecycleOwner,因此可以直接拿到 lifecycle。
Lifecycle.State 之间的偏序关系是判断"当前是否可执行某操作"的基础:currentState.isAtLeast(Lifecycle.State.STARTED) 是官方推荐的安全判断,比直接比较 == RESUMED 更健壮。
// 依赖 lifecycle-runtime-ktx 2.8.x
class MyLocationListener : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) = startListening()
override fun onStop(owner: LifecycleOwner) = stopListening()
}
版本建议:androidx.lifecycle:lifecycle-runtime-ktx:2.8.7、androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7、androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7、androidx.activity:activity-compose:1.9.3、Kotlin 2.0.21 / 2.1.x、AGP 8.5+。
三、LifecycleObserver 与 DefaultLifecycleObserver
LifecycleObserver 是空接口,仅作标记;实际使用时继承 DefaultLifecycleObserver,它用默认方法提供了全部回调,只需覆写关心的事件。
class AnalyticsObserver : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
Analytics.pageView(owner.javaClass.simpleName)
}
}
// 注册与反注册
lifecycle.addObserver(AnalyticsObserver())
LifecycleEventObserver 是另一条路线,它把所有事件收敛到 onStateChanged(source, event) 一个方法里,适合需要按事件做统一分发的场景:
| 接口 | 覆写方式 | 适用场景 |
|---|---|---|
DefaultLifecycleObserver | 按事件覆写独立方法 | 逻辑分散、可读性好,推荐 |
LifecycleEventObserver | 单一 onStateChanged | 统一分发、日志与埋点 |
LifecycleObserver | 空接口 | 仅作标记,需配合注解反射 |
注册必须与反注册配对。addObserver 会持有观察者引用,如果观察者是匿名内部类且持有 Activity 引用,未 removeObserver 就会泄漏。lifecycle 在 DESTROYED 状态会自动清理观察者,但生命周期更短的观察者(如绑定到某个 View)仍应显式移除。
四、repeatOnLifecycle 与 flowWithLifecycle
协程与生命周期的结合是最容易出错的环节。常见错误写法是在 lifecycleScope.launch 里直接 collect:
// 错误:Activity 进入后台后仍在收集,浪费资源甚至崩溃
lifecycleScope.launch {
viewModel.uiState.collect { render(it) }
}
lifecycleScope 只在 DESTROYED 时取消,因此 STOPPED 期间收集仍在进行。正确做法是 repeatOnLifecycle:
// 正确:STARTED 时启动,STOPPED 时取消,回到前台自动重启
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state -> render(state) }
}
}
repeatOnLifecycle 的语义是:每当生命周期达到目标状态就启动协程块,低于该状态就取消并等待下次达标时重新执行。这保证了 UI 收集只在界面可见时进行。
如果只需要收集单个 Flow,用 flowWithLifecycle 更简洁:
viewModel.uiState
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.onEach { state -> render(state) }
.launchIn(lifecycleScope)
两者对比:
| API | 适用场景 | 行为 |
|---|---|---|
repeatOnLifecycle | 多个 Flow、复杂逻辑 | 生命周期达标时重启整个块 |
flowWithLifecycle | 单个 Flow | 本质是 repeatOnLifecycle 的单 Flow 封装 |
lifecycleScope.launch | 仅需随销毁取消 | 后台仍执行,慎用 |
在 Fragment 中应使用 viewLifecycleOwner.lifecycleScope 而非 lifecycleScope,因为前者的生命周期与视图绑定,会在 onDestroyView 时取消,避免视图销毁后仍更新界面。协程作用域与取消传播的更多细节可参考 Kotlin 协程与并发编程
。
一句话总结:界面数据的收集必须用
repeatOnLifecycle或flowWithLifecycle,lifecycleScope.launch直接 collect 是后台耗电与崩溃的常见源头。
五、ViewModel 的职责与边界
ViewModel 的设计目的是跨配置变更(如旋转屏幕)保留数据,并承载与界面无关的业务逻辑。它的生命周期独立于 Activity:Activity 重建时 ViewModel 实例被保留,只有真正 finish 时才调用 onCleared。
| 职责 | 应放在 | 不应放在 |
|---|---|---|
| 界面状态持有 | ViewModel | View / Activity 字段 |
| 业务逻辑与数据加工 | ViewModel / Repository | Composable |
| 网络与数据库请求 | Repository(由 VM 调用) | ViewModel 内直写 |
| Context 相关操作 | 传入 Application Context | 持有 Activity Context |
| 一次性事件分发 | Channel / SharedFlow | LiveData 直接暴露 |
ViewModel 绝不能持有 Activity、Fragment、View 的引用。如果确实需要 Context,使用 AndroidViewModel 注入 Application:
class SettingsViewModel(app: Application) : AndroidViewModel(app) {
private val repo = SettingsRepository(app) // Application 引用安全
}
状态暴露推荐用 StateFlow 或 mutableStateOf,而不是 LiveData——前者与 Compose 及协程生态更契合:
class LoginViewModel(private val repo: AuthRepository) : ViewModel() {
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
fun onEmailChange(value: String) {
_uiState.update { it.copy(email = value) }
}
}
用 asStateFlow() 暴露只读视图,避免外部直接修改内部状态,是 ViewModel 封装的基本纪律。
六、viewModelScope 与协程取消
viewModelScope 是绑定到 ViewModel 生命周期的 CoroutineScope,在 onCleared 时自动取消。这意味着在 viewModelScope 中启动的请求无需手动取消:
class ArticleViewModel(private val repo: ArticleRepository) : ViewModel() {
val article = repo.observeArticle().stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null
)
}
SharingStarted.WhileSubscribed(5_000) 中的 5 秒是保留期:界面短暂不可见(如旋转)时不立即停止上游,避免重新订阅造成的网络请求抖动。这个参数是 stateIn 最重要的调优点。
需要区分三类作用域:
| 作用域 | 取消时机 | 适用 |
|---|---|---|
viewModelScope | ViewModel 销毁 | 业务请求、状态派生 |
lifecycleScope | Activity/Fragment 销毁 | 界面相关收集 |
repeatOnLifecycle 块 | 生命周期低于阈值 | 仅前台执行的收集 |
一个常见误区是在 viewModelScope 里启动"必须执行完"的任务(如保存草稿)。ViewModel 销毁会取消它们,这类任务应使用 NonCancellable 或放入 WorkManager。
七、ViewModelProvider.Factory 与依赖注入
ViewModel 不能有带参构造函数由框架直接实例化,需要通过 Factory 提供依赖。现代写法是 viewModelFactory DSL:
class LoginViewModel(private val repo: AuthRepository) : ViewModel() {
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer { LoginViewModel(AuthRepository()) }
}
}
}
initializer 块内可以通过 createSavedStateHandle() 拿到 SavedStateHandle,这是把参数与状态注入 ViewModel 的关键,例如 initializer { DetailViewModel(createSavedStateHandle()) }。
手工管理 Factory 在依赖变多后迅速失控。生产项目通常交给 Hilt 处理,@HiltViewModel 与 @Inject constructor 组合后无需手写 Factory,具体装配方式见 Android Hilt 依赖注入
。
| 方式 | 代码量 | 适用规模 |
|---|---|---|
ViewModelProvider.Factory 手写 | 高 | 单 ViewModel、无 DI |
viewModelFactory DSL | 中 | 少量依赖 |
Hilt @HiltViewModel | 低 | 中大型项目 |
八、SavedStateHandle 与进程死亡恢复
ViewModel 能跨配置变更存活,但无法跨进程死亡存活。系统在内存压力下杀掉后台进程后,重新回到前台会重建 Activity 与 ViewModel,此前状态全部丢失。SavedStateHandle 就是为此设计的:它把状态写入 SavedStateRegistry,由系统在进程重建时恢复。
class SearchViewModel(private val handle: SavedStateHandle) : ViewModel() {
val query: StateFlow<String> = handle.getStateFlow("query", "") // 自动保存与恢复
fun onQueryChange(value: String) {
handle["query"] = value
}
}
SavedStateHandle 底层是 Bundle,因此只适合保存轻量、可序列化的数据:字符串、基本类型、Parcelable、Serializable。不适合放大对象、图片、列表全集——TransactionTooLargeException 是这类误用的典型后果。
SavedStateHandle 与 onSaveInstanceState 的关系是:前者由 SavedStateRegistry 统一管理,后者是 Activity 层面的直接入口。二者共享同一份 Bundle 配额(约 1MB 上限,且是所有状态之和)。
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("draft", draftText)
}
| 机制 | 存活范围 | 容量 | 推荐用途 |
|---|---|---|---|
| ViewModel 字段 | 配置变更 | 内存 | 大数据、临时状态 |
SavedStateHandle | 进程死亡 | Bundle 配额 | 关键轻量状态 |
onSaveInstanceState | 进程死亡 | Bundle 配额 | 视图层状态 |
九、onSaveInstanceState 与 Compose rememberSaveable
在 Compose 中,rememberSaveable 是 onSaveInstanceState 的声明式等价物:它把状态写入 SavedStateRegistry,在配置变更与进程重建后恢复。
@Composable
fun SearchBar() {
// 进程死亡恢复后仍保留输入内容
var query by rememberSaveable { mutableStateOf("") }
OutlinedTextField(value = query, onValueChange = { query = it })
}
关键区别在于存活边界:
remember只在组合存活期间保留,Activity 重建即丢失。rememberSaveable跨配置变更与进程死亡保留。ViewModel跨配置变更保留,但不跨进程死亡。
因此选择策略是:临时 UI 状态用 remember;需要跨进程恢复的输入与选择用 rememberSaveable;跨配置变更的业务数据用 ViewModel;两者都要保的用 ViewModel + SavedStateHandle。
在 Compose 中获取 ViewModel 用 viewModel() 组合函数(来自 androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7):
@Composable
fun LoginScreen(viewModel: LoginViewModel = viewModel(factory = LoginViewModel.Factory)) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
// ...
}
collectAsStateWithLifecycle() 是 collectAsState() 的生命周期感知版本,等价于在 Composable 中做了 repeatOnLifecycle(STARTED)。Compose 中收集 StateFlow 应优先使用它,避免界面不可见时仍在重组。这与 Jetpack Compose 声明式 UI 开发
中状态管理的讨论一脉相承。
| 场景 | 推荐 API |
|---|---|
Compose 中收集 StateFlow | collectAsStateWithLifecycle() |
| Compose 中获取 ViewModel | viewModel() |
| 跨进程恢复的输入框 | rememberSaveable |
| ViewModel 内的轻量状态 | SavedStateHandle |
十、Compose 中的 viewModel 组合函数
viewModel() 的作用域默认取自最近的 ViewModelStoreOwner,在 Activity 中即 Activity 本身,在 Navigation 的 destination 中即该目的地。理解作用域是避免"多个页面共享同一个 ViewModel 实例"或"每次重组新建实例"的关键。
// 作用域为当前 Activity:多个 Composable 拿到同一个实例
val vm: LoginViewModel = viewModel()
// 显式指定 owner,用于跨 Composable 共享
val shared: SharedViewModel = viewModel(viewModelStoreOwner = LocalActivity.current as ComponentActivity)
viewModel() 返回的实例在组合中被 remember 缓存,因此重复调用不会新建。但不要在 Composable 中手动 remember { LoginViewModel() },那样会绕过 ViewModelStore,既不跨配置变更保留,也不会调用 onCleared。
导航场景中,如果希望多个页面共享 ViewModel,需要把 viewModelStoreOwner 指向父级 NavBackStackEntry,这是 Compose Navigation 的常见模式。
十一、常见内存泄漏
生命周期管理不当的直接后果是内存泄漏。以下是四类高频场景:
1. 非静态内部类 Handler 或 Runnable
非静态内部类隐式持有外部类引用。Handler 的 MessageQueue 持有 Message,若 Message 指向延迟执行的 Runnable,Activity 在延迟期间无法回收。
private val handler = Handler(Looper.getMainLooper()) { true } // 泄漏:隐式持有 Activity
private class SafeHandler(activity: MainActivity) : Handler(Looper.getMainLooper()) {
private val ref = WeakReference(activity) // 修复一:静态类 + 弱引用
override fun handleMessage(msg: Message) { ref.get()?.doSomething() }
}
lifecycleScope.launch { delay(3000); doSomething() } // 修复二:改用协程
2. 未取消的观察者与监听器
addObserver、registerReceiver、addOnGlobalLayoutListener 都必须配对移除。Lifecycle 能自动清理注册到它自身的观察者,但系统级监听器不会。
3. 静态字段持有 Context
companion object 中的 lateinit var context: Context 是经典泄漏。静态字段与进程同寿命,持有 Activity 会让整个 Activity 树无法回收。如需静态 Context,只能持有 applicationContext。
4. 协程与 Flow 未绑定生命周期
在 GlobalScope 中启动的协程不会随界面销毁取消,回调里更新已销毁的 View 会崩溃或泄漏。
| 泄漏源 | 检测手段 | 修复 |
|---|---|---|
| 非静态 Handler | LeakCanary | 静态类 + WeakReference |
| 未移除监听器 | LeakCanary | 生命周期回调中移除 |
| 静态 Context | 代码审查 | 只用 applicationContext |
GlobalScope 协程 | 静态分析 | 改用 viewModelScope |
| Fragment 持有 View | LeakCanary | onDestroyView 置空 |
集成 LeakCanary(debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14"))可以在开发阶段自动捕获泄漏并给出引用链,是成本最低的防护手段。
十二、常见坑清单
| 坑位 | 现象 | 修正 |
|---|---|---|
lifecycleScope.launch 直接 collect | 后台仍在收集 | 用 repeatOnLifecycle |
Fragment 用 lifecycleScope | 视图销毁后仍更新 | 改用 viewLifecycleOwner.lifecycleScope |
| ViewModel 持有 Activity | 内存泄漏 | 改用 Application 或 AndroidViewModel |
SavedStateHandle 存大对象 | TransactionTooLargeException | 只存轻量可序列化数据 |
onSaveInstanceState 后提交事务 | IllegalStateException | 移到 onCreate 或提前提交 |
Compose 中 remember { ViewModel() } | 不跨配置变更、不 onCleared | 用 viewModel() |
用 collectAsState 收集 StateFlow | 后台仍重组 | 用 collectAsStateWithLifecycle |
stateIn 未设 WhileSubscribed | 旋转后重新请求 | 设 5 秒保留期 |
addObserver 未配对移除 | 观察者泄漏 | removeObserver 或依赖自动清理 |
| 非静态 Handler 延时任务 | Activity 泄漏 | 静态类 + WeakReference |
生命周期与状态管理是 Android 架构的地基。把 ViewModel 的边界划清、把协程绑定到正确的生命周期、把跨进程状态交给 SavedStateHandle,大部分"偶发崩溃"与"内存增长"都会自然消失。
小结
本章的核心结论可以压缩成一张选择表:
- 界面可见才收集:
repeatOnLifecycle/flowWithLifecycle/collectAsStateWithLifecycle三者是同一原则的不同封装。 - 配置变更用 ViewModel:业务状态放 ViewModel,视图状态放 Fragment 视图层并在
onDestroyView清空。 - 进程死亡用 SavedStateHandle:只存轻量可序列化数据,大对象留给 ViewModel 字段。
- 作用域决定取消时机:
viewModelScope、lifecycleScope、viewLifecycleOwner.lifecycleScope三者的边界必须分清。
一句话总结: 生命周期管理的本质是"在正确的时点持有正确的引用"——引用对了,泄漏与崩溃自然就没有了。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。