本文译自「Inside Jetpack ViewModel: Internal Mechanisms and Multiplatform Design」,原文链接proandroiddev.com/inside-jetp...,由Jaewoong Eum发布于2025年12月7日。

Jetpack 的 ViewModel 已成为现代 Android 开发中不可或缺的组件,它为 UI 相关数据提供了一个生命周期感知容器,即使配置发生更改,数据也能保持不变。虽然其 API 表面上看起来很简单,但其内部机制却展现了围绕生命周期管理、跨平台抽象、资源清理和线程安全缓存等方面的设计决策。了解 ViewModel 的底层工作原理有助于你做出更优的架构决策,并避免一些不易察觉的错误。
本文将深入探讨 Jetpack ViewModel 的内部工作原理,包括 ViewModelStore 如何在配置更改后保留实例、ViewModelProvider 如何协调创建和缓存、工厂模式如何实现灵活的实例化、CreationExtras 如何实现无状态工厂、如何通过 Closeable 模式管理资源清理,以及 viewModelScope 如何将协程与 ViewModel 生命周期集成。
根本问题:配置更改后的保留
配置更改是 Android 开发面临的一项根本性挑战。当用户旋转设备、更改语言设置或触发任何配置更改时,系统会销毁并重新创建 Activity。Activity 中存储的所有数据都会丢失:
kotlin
class MyActivity : ComponentActivity() {
private var userData: User? = null // Lost on rotation!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Must reload data after every rotation
loadUserData()
}
}
一种简单的方法是使用 onSaveInstanceState():
kotlin
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable("user", userData)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userData = savedInstanceState?.getParcelable("user")
}
这种方法适用于小型、可序列化的数据。但是,对于大型数据集、网络连接或无法序列化的对象呢?对于持续进行的网络请求等操作呢?Bundle 方法在这些情况下会失效,原因既有大小限制,也有序列化/反序列化的高昂开销。
ViewModel 通过提供一个生命周期感知容器来解决这个问题,该容器通过保留对象模式(而非序列化)来应对配置更改。
ViewModelStore:保留机制
ViewModel 配置变更后仍能保留的核心是 ViewModelStore,它是一个简单的键值存储,用于保存 ViewModel 实例:
kotlin
public open class ViewModelStore {
private val map = mutableMapOf<String, ViewModel>()
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun put(key: String, viewModel: ViewModel) {
val oldViewModel = map.put(key, viewModel)
oldViewModel?.clear()
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public operator fun get(key: String): ViewModel? {
return map[key]
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun keys(): Set<String> {
return HashSet(map.keys)
}
public fun clear() {
for (vm in map.values) {
vm.clear()
}
map.clear()
}
}
实现非常简单,就是一个 MutableMap<String, ViewModel>。关键不在于存储本身,而在于存储的保留方式。
键替换行为
请注意 put 方法的行为:
kotlin
public fun put(key: String, viewModel: ViewModel) {
val oldViewModel = map.put(key, viewModel)
oldViewModel?.clear()
}
如果已存在具有相同键的 ViewModel,则旧的 ViewModel 会被立即清除。这确保了在替换 ViewModel 时能够正确清理。你可能想知道这种情况何时发生,它发生在你请求具有相同键但类型不同的 ViewModel 时:
kotlin
// First request creates TestViewModel1 with key "my_key"
val vm1: TestViewModel1 = viewModelProvider["my_key", TestViewModel1::class]
// Second request with same key but different type
val vm2: TestViewModel2 = viewModelProvider["my_key", TestViewModel2::class]
// vm1.onCleared() has been called, vm1 is no longer valid
此行为已在测试套件中验证:
kotlin
@Test
fun twoViewModelsWithSameKey() {
val key = "the_key"
val vm1 = viewModelProvider[key, TestViewModel1::class]
assertThat(vm1.cleared).isFalse()
val vw2 = viewModelProvider[key, TestViewModel2::class]
assertThat(vw2).isNotNull()
assertThat(vm1.cleared).isTrue()
}
ViewModelStoreOwner 契约
ViewModelStoreOwner 接口定义了谁拥有该存储:
kotlin
public interface ViewModelStoreOwner {
public val viewModelStore: ViewModelStore
}
ComponentActivity、Fragment 和 NavBackStackEntry 都实现了这个简单的接口。所有者的职责有两方面:
- 在配置更改后保留存储:存储必须在 Activity 重建后仍然存在。
- 在真正完成后清除存储 :当所有者被销毁而未重建时,调用
ViewModelStore.clear()。
对于 Activity 而言,这通常使用 NonConfigurationInstances 来实现,这是一种特殊的机制,允许对象在配置更改后仍然存在。Activity 框架在 onRetainNonConfigurationInstance() 期间保留这些对象,并在 getLastNonConfigurationInstance() 中恢复它们。
为什么简单的映射有效
你可能期望使用复杂的缓存机制,但简单的 MutableMap 就足够了,原因如下:
- 大小有限:每个屏幕上的 ViewModel 数量很少(通常为 1-5 个)。
- 字符串键:键由类名生成,使得查找复杂度为 O(1),并且哈希分布良好。
- 无需驱逐:ViewModel 仅在显式请求或所有者被销毁时才会被清除。
- 线程安全:访问在 ViewModelProvider 级别同步。
ViewModelProvider:编排层
ViewModelProvider 是获取 ViewModel 实例的主要 API。它负责编排 store、factory 和创建 extras 之间的交互:
kotlin
public actual open class ViewModelProvider
private constructor(private val impl: ViewModelProviderImpl) {
public constructor(
store: ViewModelStore,
factory: Factory,
defaultCreationExtras: CreationExtras = CreationExtras.Empty,
) : this(ViewModelProviderImpl(store, factory, defaultCreationExtras))
public constructor(
owner: ViewModelStoreOwner
) : this(
store = owner.viewModelStore,
factory = ViewModelProviders.getDefaultFactory(owner),
defaultCreationExtras = ViewModelProviders.getDefaultCreationExtras(owner),
)
@MainThread
public actual operator fun <T : ViewModel> get(modelClass: KClass<T>): T =
impl.getViewModel(modelClass)
@MainThread
public actual operator fun <T : ViewModel> get(key: String, modelClass: KClass<T>): T =
impl.getViewModel(modelClass, key)
}
多平台抽象
请注意 ViewModelProviderImpl 委托。ViewModel 库是一个 Kotlin 多平台库,支持 JVM、Android、iOS 和其他平台。Kotlin 多平台目前还不支持带有默认实现的 expect 类,因此通用逻辑被提取到内部实现类中:
kotlin
internal class ViewModelProviderImpl(
private val store: ViewModelStore,
private val factory: ViewModelProvider.Factory,
private val defaultExtras: CreationExtras,
) {
private val lock = SynchronizedObject()
@Suppress("UNCHECKED_CAST")
internal fun <T : ViewModel> getViewModel(
modelClass: KClass<T>,
key: String = ViewModelProviders.getDefaultKey(modelClass),
): T {
return synchronized(lock) {
val viewModel = store[key]
if (modelClass.isInstance(viewModel)) {
if (factory is ViewModelProvider.OnRequeryFactory) {
factory.onRequery(viewModel!!)
}
return@synchronized viewModel as T
}
val modelExtras = MutableCreationExtras(defaultExtras)
modelExtras[ViewModelProvider.VIEW_MODEL_KEY] = key
return@synchronized createViewModel(factory, modelClass, modelExtras).also { vm ->
store.put(key, vm)
}
}
}
}
获取或创建模式
getViewModel 方法实现了经典的获取或创建模式:
- 生成键:默认键基于类的规范名称。
- 检查缓存:通过键查找已存在的 ViewModel。
- 类型检查:验证缓存实例的类型是否正确。
- 返回缓存:如果有效,则返回缓存的实例。
- 创建新实例:如果未找到或类型错误,则通过工厂模式创建新实例。
- 存储:将新实例放入存储中。
使用同步访问确保线程安全
synchronized(lock) 代码块确保线程安全访问。虽然 ViewModel 的访问通常来自主线程(如 @MainThread 所示),但同步机制可以防止后台线程访问 ViewModel 等极端情况,尤其是在测试场景或使用 viewModelScope 时。
OnRequeryFactory 回调
OnRequeryFactory 是一种特殊的机制,用于在检索缓存的 ViewModel 时执行操作的工厂:
kotlin
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public actual open class OnRequeryFactory {
public actual open fun onRequery(viewModel: ViewModel) {}
}
SavedStateHandle 内部使用此机制在配置更改后将 ViewModel 与当前的 SavedStateRegistry 重新连接。当从缓存中检索 ViewModel 时,会调用工厂的 onRequery 方法,从而更新可能已更改的引用。
从类名生成键
默认的键生成机制可防止意外冲突:
kotlin
internal fun <T : ViewModel> getDefaultKey(modelClass: KClass<T>): String {
val canonicalName =
requireNotNull(modelClass.canonicalName) {
"Local and anonymous classes can not be ViewModels"
}
return "$VIEW_MODEL_PROVIDER_DEFAULT_KEY:$canonicalName"
}
前缀 "androidx.lifecycle.ViewModelProvider.DefaultKey:" 可确保键不会与用户提供的自定义键冲突。规范名称要求也解释了为什么本地类和匿名类不能是 ViewModel,因为它们没有规范名称:
kotlin
@Test
fun localViewModel() {
class LocalViewModel : ViewModel()
try {
viewModelProvider[LocalViewModel::class]
fail("Expected `IllegalArgumentException`")
} catch (e: IllegalArgumentException) {
assertThat(e)
.hasMessageThat()
.contains("Local and anonymous classes can not be ViewModels")
}
}
工厂模式:灵活实例化
ViewModelProvider.Factory 是创建 ViewModel 实例的机制:
kotlin
public actual interface Factory {
public fun <T : ViewModel> create(modelClass: Class<T>): T =
ViewModelProviders.unsupportedCreateViewModel()
public fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T =
create(modelClass)
public actual fun <T : ViewModel> create(modelClass: KClass<T>, extras: CreationExtras): T =
create(modelClass.java, extras)
}
方法重载链
工厂接口有三个 create 方法变体,形成一个链:
create(KClass<T>, CreationExtras):Kotlin 优先 API,委托给 Java 变体create(Class<T>, CreationExtras):主要实现点,默认为无附加组件的变体create(Class<T>):传统 API,默认抛出异常
此链在保持向后兼容性的同时,也鼓励使用基于 CreationExtras 的现代方法。
NewInstanceFactory:基于反射的创建
最简单的工厂使用反射来创建带有无参构造函数的 ViewModel:
kotlin
public open class NewInstanceFactory : Factory {
public override fun <T : ViewModel> create(modelClass: Class<T>): T =
JvmViewModelProviders.createViewModel(modelClass)
}
JVM 实现使用反射并进行了细致的错误处理:
kotlin
internal object JvmViewModelProviders {
fun <T : ViewModel> createViewModel(modelClass: Class<T>): T {
val constructor =
try {
modelClass.getDeclaredConstructor()
} catch (e: NoSuchMethodException) {
throw RuntimeException("Cannot create an instance of $modelClass", e)
}
// Enforce public constructor for consistent behavior
if (!Modifier.isPublic(constructor.modifiers)) {
throw RuntimeException("Cannot create an instance of $modelClass")
}
return try {
constructor.newInstance()
} catch (e: InstantiationException) {
throw RuntimeException("Cannot create an instance of $modelClass", e)
} catch (e: IllegalAccessException) {
throw RuntimeException("Cannot create an instance of $modelClass", e)
}
}
}
公共修饰符检查对于测试一致性至关重要。在插桩测试中,R8 可能会移除访问修饰符,使私有构造函数可访问。JVM 测试严格执行访问限制。此显式检查可确保跨测试环境的行为一致性。
AndroidViewModelFactory:应用感知创建
AndroidViewModelFactory 继承自 NewInstanceFactory 以支持 AndroidViewModel,后者需要一个 Application 参数。该工厂在保持向后兼容性的同时,也采用了现代的 CreationExtras 方法:
kotlin
public open class AndroidViewModelFactory
private constructor(
private val application: Application?,
@Suppress("UNUSED_PARAMETER") unused: Int,
) : NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
return if (application != null) {
create(modelClass)
} else {
val application = extras[APPLICATION_KEY]
if (application != null) {
create(modelClass, application)
} else {
if (AndroidViewModel::class.java.isAssignableFrom(modelClass)) {
throw IllegalArgumentException(
"CreationExtras must have an application by `APPLICATION_KEY`"
)
}
super.create(modelClass)
}
}
}
private fun <T : ViewModel> create(modelClass: Class<T>, app: Application): T {
return if (AndroidViewModel::class.java.isAssignableFrom(modelClass)) {
try {
modelClass.getConstructor(Application::class.java).newInstance(app)
} catch (e: NoSuchMethodException) {
throw RuntimeException("Cannot create an instance of $modelClass", e)
}
} else super.create(modelClass)
}
}
该工厂实现了级联解析策略:首先检查是否将 Application 传递给了构造函数(传统方法),然后通过 APPLICATION_KEY 在 CreationExtras 中查找(现代无状态方法),最后对于常规 ViewModel 回退到 NewInstanceFactory,或者如果 AndroidViewModel 没有可用的 Application,则抛出异常。
私有构造函数中的 unused: Int 参数是一个巧妙的技巧,用于区分重载构造函数,因为如果使用 null 调用 constructor(),编译器将无法区分 constructor(application: Application?)。
实例化使用了反射(modelClass.getConstructor(Application::class.java).newInstance(app)),这意味着你的 AndroidViewModel 子类必须有一个接受且仅接受一个 Application 参数的构造函数。对于其他依赖项,你需要自定义工厂或依赖注入框架,例如 Hilt。
InitializerViewModelFactory:基于 Lambda 的创建
对于具有自定义依赖项的 ViewModel,InitializerViewModelFactory 提供了一种基于 DSL 的方法,可以减少样板代码:
kotlin
val factory = viewModelFactory {
initializer { MyViewModel(get(MY_KEY)) }
initializer { AnotherViewModel(get(ANOTHER_KEY)) }
}
该 DSL 由一个构建器类驱动,该构建器类收集初始化 Lambda 表达式,其中每个初始化器将一个 KClass 与其创建 Lambda 表达式配对:
kotlin
@ViewModelFactoryDsl
public class InitializerViewModelFactoryBuilder public constructor() {
private val initializers = mutableMapOf<KClass<*>, ViewModelInitializer<*>>()
public fun <T : ViewModel> addInitializer(
clazz: KClass<T>,
initializer: CreationExtras.() -> T,
) {
require(clazz !in initializers) {
"A `initializer` with the same `clazz` has already been added: ${clazz.canonicalName}."
}
initializers[clazz] = ViewModelInitializer(clazz, initializer)
}
public fun build(): ViewModelProvider.Factory =
ViewModelProviders.createInitializerFactory(initializers.values)
}
@ViewModelFactoryDsl 注解是一个 DSL 标记,可防止嵌套的构建器作用域意外访问外部作用域的方法。初始化 Lambda 表达式接收 CreationExtras 作为其接收器,允许通过 get() 直接访问额外内容。
在创建时,工厂会通过初始化器进行线性搜索,以找到匹配的类:
kotlin
internal fun <VM : ViewModel> createViewModelFromInitializers(
modelClass: KClass<VM>,
extras: CreationExtras,
vararg initializers: ViewModelInitializer<*>,
): VM {
val viewModel =
initializers.firstOrNull { it.clazz == modelClass }?.initializer?.invoke(extras) as VM?
return requireNotNull(viewModel) {
"No initializer set for given class ${modelClass.canonicalName}"
}
}
线性搜索是可以接受的,因为每个工厂创建的 ViewModel 数量通常很少(1-5 个),而且 ViewModel 的创建频率很低(每个生命周期一次)。
CreationExtras:无状态工厂配置
CreationExtras 是一个类型安全的键值容器,用于在不使工厂成为有状态的情况下向其传递配置。工厂不再通过构造函数注入来持有依赖项,而是在创建时传递依赖项:
kotlin
public abstract class CreationExtras internal constructor() {
internal val extras: MutableMap<Key<*>, Any?> = mutableMapOf()
public interface Key<T>
public abstract operator fun <T> get(key: Key<T>): T?
public object Empty : CreationExtras() {
override fun <T> get(key: Key<T>): T? = null
}
}
类型安全的键
每个键都由其关联的值类型参数化,从而提供编译时类型安全。当你使用 extras[APPLICATION_KEY] 获取值时,返回类型会自动为 Application?:
kotlin
val APPLICATION_KEY: Key<Application> = CreationExtras.Companion.Key()
val VIEW_MODEL_KEY: Key<String> = CreationExtras.Companion.Key()
键的创建使用了一个内联函数,该函数创建一个实现 Key 接口的新匿名对象。由于每个键都是一个唯一的对象实例,因此键之间通过标识(===)进行比较,即使两个键具有相同的类型参数,也不会发生意外冲突:
kotlin
@JvmStatic
public inline fun <reified T> Key(): Key<T> = object : Key<T> {}
用于修改的 MutableCreationExtras
基类 CreationExtras 是只读的,而 MutableCreationExtras 允许添加条目。这种分离遵循 Kotlin 的集合设计理念(例如 List 与 MutableList),防止工厂意外修改共享的 extras:
kotlin
public class MutableCreationExtras
public constructor(initialExtras: CreationExtras = Empty) : CreationExtras() {
init {
extras += initialExtras.extras
}
public operator fun <T> set(key: Key<T>, t: T) {
extras[key] = t
}
@Suppress("UNCHECKED_CAST")
public override fun <T> get(key: Key<T>): T? = extras[key] as T?
}
与 ViewModelProvider 集成
ViewModelProvider 会在调用工厂之前自动将 ViewModel 的键添加到 extras 中,这对于像 SavedStateHandle 这样需要键来正确限定持久化范围的功能至关重要:
kotlin
val modelExtras = MutableCreationExtras(defaultExtras)
modelExtras[ViewModelProvider.VIEW_MODEL_KEY] = key
return@synchronized createViewModel(factory, modelClass, modelExtras)
HasDefaultViewModelProviderFactory
ViewModelStoreOwner 的实现可以通过 HasDefaultViewModelProviderFactory 接口提供默认的工厂和 extras。 ComponentActivity 和 Fragment 都实现了此接口,并提供了支持 SavedStateHandle 且预先填充了 APPLICATION_KEY 的工厂:
kotlin
public interface HasDefaultViewModelProviderFactory {
public val defaultViewModelProviderFactory: ViewModelProvider.Factory
public val defaultViewModelCreationExtras: CreationExtras
get() = CreationExtras.Empty
}
ComponentActivity 和 Fragment 都实现了此接口,并提供了支持 SavedStateHandle 和正确的 CreationExtras 且预先填充了 APPLICATION_KEY 的工厂。
从 ViewModelStoreOwner 创建 ViewModelProvider 时,会自动使用以下默认值:
kotlin
internal fun getDefaultFactory(owner: ViewModelStoreOwner): ViewModelProvider.Factory =
if (owner is HasDefaultViewModelProviderFactory) {
owner.defaultViewModelProviderFactory
} else {
DefaultViewModelProviderFactory
}
internal fun getDefaultCreationExtras(owner: ViewModelStoreOwner): CreationExtras =
if (owner is HasDefaultViewModelProviderFactory) {
owner.defaultViewModelCreationExtras
} else {
CreationExtras.Empty
}
这种模式支持渐进增强:简单的 ViewModel 使用默认工厂,而复杂的 ViewModel 可以通过 extras 获取丰富的配置,而无需更改获取 ViewModelProvider 的方式。
ViewModel 类:资源管理
ViewModel 类通过 AutoCloseable 模式管理资源生命周期。expect 关键字表明这是一个 Kotlin 多平台声明,每个平台都提供自己的 actual 实现:
kotlin
public expect abstract class ViewModel {
public constructor()
public constructor(viewModelScope: CoroutineScope)
public constructor(vararg closeables: AutoCloseable)
public constructor(viewModelScope: CoroutineScope, vararg closeables: AutoCloseable)
protected open fun onCleared()
@MainThread internal fun clear()
public fun addCloseable(key: String, closeable: AutoCloseable)
public open fun addCloseable(closeable: AutoCloseable)
public fun <T : AutoCloseable> getCloseable(key: String): T?
}
ViewModelImpl 内部实现
实际实现委托给 ViewModelImpl,遵循多平台模式,将通用逻辑提取到内部类中。isCleared 上的 @Volatile 注解确保跨线程可见,这对于清除后的保护机制至关重要:
kotlin
internal class ViewModelImpl {
private val lock = SynchronizedObject()
private val keyToCloseables = mutableMapOf<String, AutoCloseable>()
private val closeables = mutableSetOf<AutoCloseable>()
@Volatile private var isCleared = false
constructor()
constructor(viewModelScope: CoroutineScope) {
addCloseable(VIEW_MODEL_SCOPE_KEY, viewModelScope.asCloseable())
}
constructor(vararg closeables: AutoCloseable) {
this.closeables += closeables
}
constructor(viewModelScope: CoroutineScope, vararg closeables: AutoCloseable) {
addCloseable(VIEW_MODEL_SCOPE_KEY, viewModelScope.asCloseable())
this.closeables += closeables
}
}
两层可关闭存储
该实现维护两个集合:keyToCloseables 用于注册后需要检索的资源(例如 viewModelScope),以及 closeables 用于即用即弃的清理。使用 Set 来管理匿名可关闭资源可以防止重复注册导致双重关闭。
清除顺序
当 ViewModel 被清除时,资源会按照特定的顺序进行清理。isCleared = true 标志会在同步代码块之前设置,以防止并发的 addCloseable 调用添加会被遗漏的资源:
kotlin
@MainThread
fun clear() {
if (isCleared) return
isCleared = true
synchronized(lock) {
for (closeable in keyToCloseables.values) {
closeWithRuntimeException(closeable)
}
for (closeable in closeables) {
closeWithRuntimeException(closeable)
}
// Clear only resources without keys to prevent accidental recreation
closeables.clear()
}
}
匿名 closeables 集合会被清除,但 keyToCloseables 会被有意保留。这可以防止在清除后代码访问 viewModelScope 等资源时意外地重新创建它们。
清除后保护
在清除 ViewModel 后添加可关闭对象会立即将其关闭,从而防止在协程仍在运行时 ViewModel 被清除时出现资源泄漏:
kotlin
fun addCloseable(key: String, closeable: AutoCloseable) {
if (isCleared) {
closeWithRuntimeException(closeable)
return
}
val oldCloseable = synchronized(lock) { keyToCloseables.put(key, closeable) }
closeWithRuntimeException(oldCloseable)
}
此外,请注意,添加键值可关闭对象时,任何具有相同键值的现有可关闭对象都会自动关闭,从而支持替换模式。
用于模拟的可空实现
JVM 实现提供了一个可空的 impl 对象,以支持模拟框架。当你模拟 ViewModel 时,模拟对象不会调用真正的构造函数,因此 impl 对象永远不会被初始化。如果没有可空类型和安全调用(impl?.clear()),测试会因 NullPointerException 而崩溃:
kotlin
public actual abstract class ViewModel {
private val impl: ViewModelImpl?
public actual constructor() {
impl = ViewModelImpl()
}
@MainThread
internal actual fun clear() {
impl?.clear()
onCleared()
}
}
viewModelScope:协程集成
viewModelScope 扩展属性提供了一个生命周期感知的 CoroutineScope,当 ViewModel 被清除时,该作用域会自动取消:
kotlin
public val ViewModel.viewModelScope: CoroutineScope
get() =
synchronized(VIEW_MODEL_SCOPE_LOCK) {
getCloseable(VIEW_MODEL_SCOPE_KEY)
?: createViewModelScope().also { scope ->
addCloseable(VIEW_MODEL_SCOPE_KEY, scope)
}
}
使用键控存储的延迟创建
作用域在首次访问时延迟创建,并使用键控可关闭机制进行存储。键名故意设置得非常详细,以避免与用户自定义的键发生冲突:
kotlin
internal const val VIEW_MODEL_SCOPE_KEY =
"androidx.lifecycle.viewmodel.internal.ViewModelCoroutineScope.JOB_KEY"
CloseableCoroutineScope
协程和可关闭系统之间的桥梁是 CloseableCoroutineScope,它同时实现了 CoroutineScope 和 AutoCloseable 接口。在 ViewModel 清空期间调用 close() 时,所有正在运行的协程都会被取消:
kotlin
internal class CloseableCoroutineScope(override val coroutineContext: CoroutineContext) :
AutoCloseable, CoroutineScope {
constructor(coroutineScope: CoroutineScope) : this(coroutineScope.coroutineContext)
override fun close() = coroutineContext.cancel()
}
平台感知调度器选择
作为 Kotlin 多平台库,ViewModel 在没有主线程概念的平台上也能工作,因为它会回退到 EmptyCoroutineContext。请注意,这里使用的是 Dispatchers.Main.immediate 而不是 Dispatchers.Main,这样可以避免在已经在主线程上运行时进行不必要的重新调度:
kotlin
internal fun createViewModelScope(): CloseableCoroutineScope {
val dispatcher =
try {
Dispatchers.Main.immediate
} catch (_: NotImplementedError) {
// In Native environments where `Dispatchers.Main` might not exist (e.g., Linux)
EmptyCoroutineContext
} catch (_: IllegalStateException) {
// In JVM Desktop environments where `Dispatchers.Main` might not exist (e.g., Swing)
EmptyCoroutineContext
}
return CloseableCoroutineScope(coroutineContext = dispatcher + SupervisorJob())
}
SupervisorJob 实现子协程独立失败
该作用域使用 SupervisorJob(),允许子协程独立失败。而使用普通的 Job 时,如果一个子协程失败,它会取消所有同级协程。这种设计符合 UI 应用程序的预期,即失败的网络请求不应该取消正在进行的数据库操作。
ViewModelLazy:Kotlin 属性委托
viewModels() 委托使用了 ViewModelLazy:
kotlin
public class ViewModelLazy<VM : ViewModel>
@JvmOverloads
constructor(
private val viewModelClass: KClass<VM>,
private val storeProducer: () -> ViewModelStore,
private val factoryProducer: () -> ViewModelProvider.Factory,
private val extrasProducer: () -> CreationExtras = { CreationExtras.Empty },
) : Lazy<VM> {
private var cached: VM? = null
override val value: VM
get() {
val viewModel = cached
return if (viewModel == null) {
val store = storeProducer()
val factory = factoryProducer()
val extras = extrasProducer()
ViewModelProvider.create(store, factory, extras).get(viewModelClass).also {
cached = it
}
} else {
viewModel
}
}
override fun isInitialized(): Boolean = cached != null
}
双重缓存架构
惰性委托会在首次访问后将 ViewModel 引用缓存到本地。这是一种双重缓存模式:
- ViewModelStore 缓存:规范缓存,不受配置更改的影响。
- ViewModelLazy 缓存:本地缓存,用于避免重复创建 ViewModelProvider。
本地缓存是一种优化,创建 ViewModelProvider 并查找 ViewModel 的开销很小,但缓存可以避免后续访问中哪怕是这种微小的开销。
用于延迟访问的 Lambda 生产者
所有三个生产者(store、factory、extras)都是 Lambda 表达式,允许延迟求值:
kotlin
private val viewModel by viewModels { MyViewModelFactory() }
// Factory is only created when viewModel is first accessed
这对于 Fragment 尤其重要,因为在属性初始化期间 Activity 可能不可用。
Android 特有:AGP 代码脱糖兼容性
Android 有一个特殊的兼容层,用于处理混合使用针对不同 ViewModel 版本编译的库时出现的 AGP(Android Gradle 插件)代码脱糖问题:
kotlin
internal actual fun <VM : ViewModel> createViewModel(
factory: ViewModelProvider.Factory,
modelClass: KClass<VM>,
extras: CreationExtras,
): VM {
return try {
factory.create(modelClass, extras)
} catch (e: AbstractMethodError) {
try {
factory.create(modelClass.java, extras)
} catch (e: AbstractMethodError) {
factory.create(modelClass.java)
}
}
}
这一系列 try-catch 代码块用于处理使用旧版本 ViewModel 编译的工厂缺少较新版本的 create 方法的情况。当运行时调用编译后的字节码中不存在的方法时,就会出现 AbstractMethodError 错误,这种情况通常发生在旧工厂只有 create(Class<T>) 方法,而运行时需要 create(Class<T>, CreationExtras) 方法时。这种防御性级联机制能够优雅地降级到方法变体,确保 ViewModel 创建功能不受库版本不匹配的影响。
性能特性和设计权衡
ViewModel 系统在设计上做出了一些权衡,以平衡正确性、灵活性和性能。
同步开销
ViewModelProviderImpl 和 ViewModelImpl 都使用同步块来实现线程安全访问:
kotlin
synchronized(lock) {
val viewModel = store[key]
// ...
}
由于 ViewModel 访问通常发生在主线程上,临界区很短(简单的 map 操作),并且锁争用很少发生,因此这种同步开销非常小。该设计优先考虑正确性而非微优化,因为罕见的竞态条件代价远高于几乎察觉不到的同步开销。
反射开销
NewInstanceFactory 使用 Java 反射来动态实例化 ViewModel 类:
kotlin
modelClass.getDeclaredConstructor().newInstance()
由于运行时类型检查和动态方法解析,反射比直接构造函数调用要慢。但是,这种开销可以忽略不计,因为 ViewModel 的创建频率很低(每个生命周期一次),而且无需编译时信息即可实例化任何 ViewModel 子类的灵活性远远超过了这点微小的开销。
基于映射的存储
使用 MutableMap 作为 ViewModelStore 可以提供 O(1) 的查找时间,但清除操作的时间复杂度为 O(n),因为每个 ViewModel 都必须单独清除:
kotlin
public fun clear() {
for (vm in map.values) {
vm.clear()
}
map.clear()
}
由于 ViewModelStore 通常只包含 1-5 个 ViewModel,因此线性时间的清除操作实际上并无影响。使用标准 HashMap 的简洁性使得代码更易于理解和维护,对于这种有限的用例而言,其价值远大于理论上的优化。
用于模拟的可空实现
JVM ViewModel 中的可空 impl 会在每个操作中添加空值检查:
kotlin
impl?.clear()
这种微小的开销可以实现正确的模拟行为。当模拟框架创建 ViewModel 模拟对象时,它们会绕过真正的构造函数,导致 impl 未初始化。可空类型可以防止测试中出现 NullPointerException,这是一个值得的权衡。
实际应用模式:理解 ViewModel 的使用
理解其内部机制有助于你在实际应用中识别模式和反模式。
模式 1:作用域 ViewModel 共享
可以使用 Activity 的 ViewModelStore 而不是每个 Fragment 自己的存储来在 Fragment 之间共享 ViewModel:
kotlin
class FragmentA : Fragment() {
private val sharedViewModel: SharedViewModel by activityViewModels()
}
class FragmentB : Fragment() {
private val sharedViewModel: SharedViewModel by activityViewModels()
}
两个 Fragment 会获得相同的 ViewModel 实例,因为它们使用相同的 ViewModelStore(Activity 的)。这使得兄弟 Fragment 可以通过共享状态进行通信。内部机制:
kotlin
// activityViewModels() uses:
ViewModelProvider(
store = requireActivity().viewModelStore, // Same store for both fragments
factory = ...,
)
模式 2:自定义键用于多个实例
你可以通过提供自定义键来拥有同一个 ViewModel 类的多个实例:
kotlin
val viewModel1: MyViewModel = viewModelProvider["user_1", MyViewModel::class]
val viewModel2: MyViewModel = viewModelProvider["user_2", MyViewModel::class]
该键包含自定义字符串,从而在 ViewModelStore 中创建单独的缓存条目。这对于同时管理多个用户配置文件或聊天对话等场景非常有用。
模式 3:使用 CreationExtras 进行工厂注入
现代工厂是无状态的,在创建时通过 CreationExtras 接收所有依赖项:
kotlin
class MyViewModelFactory : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: KClass<T>, extras: CreationExtras): T {
val repository = extras[REPOSITORY_KEY]!!
val userId = extras[USER_ID_KEY]!!
return MyViewModel(repository, userId) as T
}
}
这使得工厂可以作为单例,同时每次调用配置都不同,从而避免为每个具有不同依赖项的 ViewModel 创建新的工厂实例。
反模式:在 ViewModel 中访问 Context
kotlin
// Bad: Leaks Activity context
class MyViewModel(private val context: Context) : ViewModel()
// Good: Use Application context via AndroidViewModel
class MyViewModel(application: Application) : AndroidViewModel(application)
// Better: Don't hold Context at all, pass it to methods
class MyViewModel : ViewModel() {
fun loadData(context: Context) { ... }
}
第一种方法可能会导致 Activity 泄漏,因为 ViewModel 在持有已销毁 Activity 的引用时,配置更改仍然存在。请使用 AndroidViewModel 来获取 Application 上下文,或者在需要时将 Context 传递给各个方法。
反模式:在 onCleared 中阻塞
你知道 onCleared() 是在主线程上调用的吗?诸如网络关闭或数据库清理之类的阻塞操作应该使用协程的 addCloseable 方法卸载到后台线程:
kotlin
// Bad: Blocking call in onCleared
override fun onCleared() {
networkClient.shutdown() // May block the main thread until completing the shutdown process!
}
// Good: Use addCloseable for managed cleanup
init {
addCloseable {
scope.launch { networkClient.shutdown() }
}
}
结论
Jetpack ViewModel 的内部机制展现了一个精心设计的生命周期感知状态管理系统。ViewModelStore 通过保留映射提供简单而有效的缓存,而 ViewModelProvider 则通过线程安全的访问和灵活的工厂支持来协调创建过程。CreationExtras 通过外部化配置来实现无状态工厂,而 AutoCloseable 集成则确保了资源的正确清理。
多平台架构将通用逻辑提取到内部实现类中,使该库能够支持 JVM、Android、iOS 和其他平台。viewModelScope 集成通过 closeable 机制提供自动协程取消功能,并为没有主线程的环境提供平台感知的调度器选择。
如果你想掌握最新的技能、新闻、技术文章、面试题和实用代码技巧,请查看Dove Letter。想要更深入地了解面试准备,千万不要错过终极 Android 面试指南:Manifest Android Interview。
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!