// MyViewModel.kt
class MyViewModel : ViewModel() {
// 使用 viewModelScope 启动协程
fun loadData() {
viewModelScope.launch {
// 模拟网络请求
val result = repository.fetchData()
_data.value = result
}
}
// 自定义协程作用域(不推荐,除非需要更精细控制)
private val customScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun longRunningTask() {
customScope.launch {
// 该协程不会随 ViewModel 清理自动取消,需手动处理
processLargeDataSet()
}
}
override fun onCleared() {
super.onCleared()
// 手动取消自定义作用域
customScope.cancel()
// 释放其他资源
repository.close()
}
}
#### 3.3.2 onSaveInstanceState 保存 UI 状态
// MainActivity.kt
class MainActivity : AppCompatActivity() {
private val viewModel: MyViewModel by viewModels()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 恢复 UI 状态
savedInstanceState?.let { bundle ->
val savedPosition = bundle.getInt("scroll_position", 0)
binding.recyclerView.scrollToPosition(savedPosition)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// 保存 UI 瞬态数据
outState.putInt("scroll_position", binding.recyclerView.computeVerticalScrollOffset())
outState.putString("input_text", binding.editText.text.toString())
outState.putParcelable("selected_item", binding.listView.selectedItem)
}
}
#### 3.3.3 ViewModel 中的状态恢复(配合 SavedStateHandle)
// ProfileViewModel.kt
class ProfileViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// SavedStateHandle 自动处理配置变更和进程杀死
val userId: MutableLiveData = savedStateHandle.getLiveData("userId", "")
fun updateUserId(newId: String) {
savedStateHandle["userId"] = newId
}
override fun onCleared() {
super.onCleared()
// 清理工作,不要在此处保存数据
analyticsTracker.flush()
}
}
### 3.4 三个机制对比表
| 特性 | ViewModelScope | onCleared() | onSaveInstanceState |
|------|---------------|-------------|---------------------|
| 触发时机 | ViewModel 初始化时创建 | ViewModel 被清理前 | Activity 进入后台或销毁前 |
| 作用域 | 跟随 ViewModel 生命周期 | 仅调用一次 | 每次可能销毁时 |
| 数据类型 | 协程任务 | 资源释放(非序列化) | 可序列化数据(Bundle) |
| 容量限制 | 无 | 无 | ~1MB (Binder 事务) |
| 适用场景 | 网络请求、数据库操作 | 关闭数据库/文件、释放锁 | 滚动位置、输入框内容、选中项 |
| 进程杀死后恢复 | 否(新 ViewModel 实例) | 否 | 是(从 Bundle 恢复) |
| 配置变更后恢复 | 是(相同 ViewModel 实例) | 否 | 是 |
### 3.5 调试与常见错误
#### 3.5.1 调试技巧
// 在 ViewModel 中添加日志来观察生命周期
class DebugViewModel : ViewModel() {
init {
Log.d("ViewModelLifecycle", "init: ${this.hashCode()}")
}
override fun onCleared() {
super.onCleared()
Log.d("ViewModelLifecycle", "onCleared: ${this.hashCode()}")
}
}
// 在 Activity 中检查 ViewModel 是否复用
val prevHash = viewModel.hashCode()
// 旋转屏幕后,hashCode() 应该相同
Log.d("ViewModelTest", "ViewModel hash: ${viewModel.hashCode()}")
#### 3.5.2 常见错误与避坑
| 错误类型 | 问题描述 | 解决方案 |
|---------|---------|---------|
| 在 onCleared() 中保存数据 | ViewModel 清理后数据丢失 | 使用 SavedStateHandle 或持久化存储 |
| viewModelScope 未正确处理异常 | 协程未捕获的异常导致 ViewModel 泄漏 | 添加 try-catch 或使用 `SupervisorJob` |
| onSaveInstanceState 存储大对象 | Bundle 过大导致 TransactionTooLargeException | 仅保存轻量状态,大数据存入 ViewModel |
| 自定义 CoroutineScope 未取消 | 导致协程泄漏和内存泄漏 | 在 onCleared() 中 `scope.cancel()` |
| 在 ViewModel 中持有 Activity 上下文 | 内存泄漏 | 使用 Application 上下文或 ViewModel 专用类 |
⚠️ Warning:ViewModel 在配置变更时存活,但 不会保存 UI 状态到磁盘。如果用户切换到其他应用且进程被系统杀死,重新启动 Activity 后 ViewModel 将被重新创建,此时必须依赖 onSaveInstanceState 或 SavedStateHandle 恢复 UI 状态。
### 3.6 性能与稳定性优化
- **优先使用 SavedStateHandle**:替代手动 onSaveInstanceState 实现,支持自动序列化,减少样板代码。
- **合理划分数据职责**:业务数据(用户信息、列表数据)放入 ViewModel;UI 瞬态(输入框文本、滚动位置)通过 onSaveInstanceState 或 SavedStateHandle 处理。
- **避免在 ViewModel 中持有 Activity/Fragment 引用**:使用 LiveData、StateFlow 等响应式组件进行通信。
- **监控 ViewModel 泄漏**:使用 LeakCanary 检测异常,确保 onCleared() 不会因为持有外部引用而未被调用。
💡 最佳实践:在 Activity `onCreate()` 中通过 `viewModels()` 获取 ViewModel,配合 `SavedStateHandle` 可实现无痛的状态保持;仅在必要时手动调用 `onSaveInstanceState` 存储 UI 辅助状态。
// 推荐组合使用 SavedStateHandle + ViewModel
class RecommendationViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// 自动保存/恢复用户输入的搜索词
val searchQuery: LiveData = savedStateHandle.getLiveData("search_query", "")
// ViewModelScope 处理异步请求
fun performSearch(query: String) {
savedStateHandle["search_query"] = query
viewModelScope.launch {
val results = repository.search(query)
_searchResults.value = results
}
}
override fun onCleared() {
super.onCleared()
// 清理资源,但不应保存数据
searchRepository.close()
}
}