Android 生命周期与 ViewModel

生命周期错配是 Android 内存泄漏与崩溃的主要成因,视图模型则是官方给出的隔离方案。本文梳理两者的协作关系:页面回调链路、生命周期与观察者、安全收集流的方式、视图模型作用域的自动取消、状态保存句柄与进程死亡恢复,以及声明式界面中视图模型与可保存状态的配合。文末附常见内存泄漏清单与对应的修复手段,帮你写出生命周期安全的代码。

一、生命周期回调全景

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。

职责应放在不应放在
界面状态持有ViewModelView / Activity 字段
业务逻辑与数据加工ViewModel / RepositoryComposable
网络与数据库请求Repository(由 VM 调用)ViewModel 内直写
Context 相关操作传入 Application Context持有 Activity Context
一次性事件分发Channel / SharedFlowLiveData 直接暴露

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 最重要的调优点。

需要区分三类作用域:

作用域取消时机适用
viewModelScopeViewModel 销毁业务请求、状态派生
lifecycleScopeActivity/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 中收集 StateFlowcollectAsStateWithLifecycle()
Compose 中获取 ViewModelviewModel()
跨进程恢复的输入框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 会崩溃或泄漏。

泄漏源检测手段修复
非静态 HandlerLeakCanary静态类 + WeakReference
未移除监听器LeakCanary生命周期回调中移除
静态 Context代码审查只用 applicationContext
GlobalScope 协程静态分析改用 viewModelScope
Fragment 持有 ViewLeakCanaryonDestroyView 置空

集成 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,大部分"偶发崩溃"与"内存增长"都会自然消失。


小结

本章的核心结论可以压缩成一张选择表:

  1. 界面可见才收集:repeatOnLifecycle / flowWithLifecycle / collectAsStateWithLifecycle 三者是同一原则的不同封装。
  2. 配置变更用 ViewModel:业务状态放 ViewModel,视图状态放 Fragment 视图层并在 onDestroyView 清空。
  3. 进程死亡用 SavedStateHandle:只存轻量可序列化数据,大对象留给 ViewModel 字段。
  4. 作用域决定取消时机:viewModelScope、lifecycleScope、viewLifecycleOwner.lifecycleScope 三者的边界必须分清。

一句话总结: 生命周期管理的本质是"在正确的时点持有正确的引用"——引用对了,泄漏与崩溃自然就没有了。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Android 开发」更多文章

  1. Kotlin Multiplatform 跨平台共享
  2. Android R8 混淆与 Baseline Profile
  3. Android 测试体系:单元测试、Espresso 与 Compose 测试