Jetpack ViewModel内幕:内部机制与跨平台设计

本文译自「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
}

ComponentActivityFragmentNavBackStackEntry 都实现了这个简单的接口。所有者的职责有两方面:

  1. 在配置更改后保留存储:存储必须在 Activity 重建后仍然存在。
  2. 在真正完成后清除存储 :当所有者被销毁而未重建时,调用 ViewModelStore.clear()

对于 Activity 而言,这通常使用 NonConfigurationInstances 来实现,这是一种特殊的机制,允许对象在配置更改后仍然存在。Activity 框架在 onRetainNonConfigurationInstance() 期间保留这些对象,并在 getLastNonConfigurationInstance() 中恢复它们。

为什么简单的映射有效

你可能期望使用复杂的缓存机制,但简单的 MutableMap 就足够了,原因如下:

  1. 大小有限:每个屏幕上的 ViewModel 数量很少(通常为 1-5 个)。
  2. 字符串键:键由类名生成,使得查找复杂度为 O(1),并且哈希分布良好。
  3. 无需驱逐:ViewModel 仅在显式请求或所有者被销毁时才会被清除。
  4. 线程安全:访问在 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 方法实现了经典的获取或创建模式:

  1. 生成键:默认键基于类的规范名称。
  2. 检查缓存:通过键查找已存在的 ViewModel。
  3. 类型检查:验证缓存实例的类型是否正确。
  4. 返回缓存:如果有效,则返回缓存的实例。
  5. 创建新实例:如果未找到或类型错误,则通过工厂模式创建新实例。
  6. 存储:将新实例放入存储中。

使用同步访问确保线程安全

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 方法变体,形成一个链:

  1. create(KClass<T>, CreationExtras):Kotlin 优先 API,委托给 Java 变体
  2. create(Class<T>, CreationExtras):主要实现点,默认为无附加组件的变体
  3. 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_KEYCreationExtras 中查找(现代无状态方法),最后对于常规 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 的集合设计理念(例如 ListMutableList),防止工厂意外修改共享的 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。 ComponentActivityFragment 都实现了此接口,并提供了支持 SavedStateHandle 且预先填充了 APPLICATION_KEY 的工厂:

kotlin 复制代码
public interface HasDefaultViewModelProviderFactory {
    public val defaultViewModelProviderFactory: ViewModelProvider.Factory

    public val defaultViewModelCreationExtras: CreationExtras
        get() = CreationExtras.Empty
}

ComponentActivityFragment 都实现了此接口,并提供了支持 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,它同时实现了 CoroutineScopeAutoCloseable 接口。在 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 引用缓存到本地。这是一种双重缓存模式:

  1. ViewModelStore 缓存:规范缓存,不受配置更改的影响。
  2. 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 系统在设计上做出了一些权衡,以平衡正确性、灵活性和性能。

同步开销

ViewModelProviderImplViewModelImpl 都使用同步块来实现线程安全访问:

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

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
_李小白4 小时前
【Android FrameWork】延伸阅读: Android应用安装过程
android
光头闪亮亮5 小时前
Android手持机扫码出入库的开发详解-6.APP下载更新
android
光头闪亮亮5 小时前
Android手持机扫码出入库的开发详解-7.SQLite CRUD操作
android
键来大师5 小时前
Android16 设置壁纸出现APK重启问题和悬浮控件等图标变成黑色图框
android·framework·rk3576
_李小白5 小时前
【Android FrameWork】第四十二天:PMS main函数
android
BoomHe6 小时前
Android LMK(Low Memory Killer)机制
android
时光呀时光慢慢走6 小时前
MAUI 开发安卓 MQTT 客户端:实现远程控制 (完整源码 + 避坑指南)
android·物联网·mqtt·c#
成都大菠萝7 小时前
2-2-44 快速掌握Kotlin-函数类型操作
android
有位神秘人7 小时前
Android中获取设备里面的音频文件
android