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
}

ComponentActivity、Fragment 和 NavBackStackEntry 都实现了这个简单的接口。所有者的职责有两方面:

  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_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 引用缓存到本地。这是一种双重缓存模式:

  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 系统在设计上做出了一些权衡,以平衡正确性、灵活性和性能。

同步开销

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。

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

保护原创,请勿转载!

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work4 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work4 天前
ch21 签名、校验与发版
kotlin
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui