云计算百科
云计算领域专业知识百科平台

拆解 ComponentActivity.viewModels()

一、先给结论

这十行代码看起来平平无奇,实际上把 Kotlin 里三个最容易混淆的语法点一次性用全了:

语法点表面作用真正解决的问题
inline 消除 lambda 对象的分配开销 让 reified 成为可能
reified 拿到"真实"的泛型类型 JVM 类型擦除下取到 VM::class
noinline 参数不参与内联 让 lambda 能存进变量、延后调用
两层 lambda 把表达式包成 () -> T 拖到"真正需要"时才求值
返回 Lazy<VM> 延迟初始化 只创建一次 + 天然支持 by 委托

一句话概括:它在"调用时"什么都不做,只造一个 Lazy 壳子;等第一次访问 vm 时,才去拿 store、造 factory、创建 ViewModel。

二、逐行拆解

public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
// 没传工厂就用默认工厂;注意这里返回的是一个 lambda,不是 Factory 实例
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}

// 只构造壳子:类型、store 的获取方式、工厂的获取方式
return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}

签名本身信息量就很大:

  • ComponentActivity. — 扩展函数,只有 Activity 及其子类(AppCompatActivity 等)能调用
  • <reified VM : ViewModel> — 上界约束,保证类型安全
  • noinline factoryProducer — 可选的自定义工厂,默认 null
  • : Lazy<VM> — 返回的不是 ViewModel,而是一个"等到用时才创建 ViewModel"的容器

三、reified:为什么必须配 inline

3.1 没有 reified 会怎样

JVM 泛型是类型擦除的。普通泛型函数编译后,VM 这个类型参数就消失了:

// 编译后签名近似为 Object get()
fun <VM : ViewModel> get(): VM { ... }

函数体内根本不知道 VM 到底是 MyViewModel 还是 OtherViewModel,所以下面这行代码连编译都过不了:

fun <VM : ViewModel> get() = VM::class // 编译错误:Cannot use 'VM' as reified type parameter

而 ViewModelProvider 必须拿着 KClass 才能按类名去 store 里查、去 factory 里建。拿不到类型,整条链路就断了。

3.2 reified 做了什么

inline + reified 的组合,本质是编译器在调用点做文本替换:

// 源码
private val vm: MyViewModel by viewModels()

// 编译器在调用点展开后大致等价于
ViewModelLazy(MyViewModel::class, { viewModelStore }, { defaultViewModelProviderFactory })

VM::class 被原地替换成 MyViewModel::class —— 一个编译期就确定的常量。类型参数不再需要"运行时存在",自然也就绕开了擦除。

所以 reified 只是表象,inline 才是实现手段:函数体必须能搬到调用点,才可能带着具体类型一起搬。

四、noinline:一个"必须能被存起来"的 lambda

inline 函数的 lambda 参数默认也会被内联——不生成 Function0 对象,直接展开成代码块。代价是它不再是一个值:不能赋给变量,不能返回,不能传给非内联函数。

而这行代码恰恰需要它是个值:

val factoryPromise = factoryProducer ?: { defaultViewModelProviderFactory }

factoryPromise 要被赋给局部变量,再作为构造参数传给 ViewModelLazy,最后在 ViewModelLazy.value 里被延迟调用。这些操作都要求它是一个实实在在的 () -> Factory 对象。

加上 noinline,Kotlin 就会为它生成真正的函数对象(Function0 实例),而不是把它内联展开。

一句话记住区别:

  • 默认内联的 lambda:快,但"用完即焚"
  • noinline 的 lambda:可以当值传来传去,代价是多一次对象分配

五、为什么要再包一层 { defaultViewModelProviderFactory }

factoryPromise 的类型是 () -> Factory,所以这里必须传 lambda 而不是 defaultViewModelProviderFactory 本身。但"类型对不上"只是表层原因,真正有价值的是延迟求值:

第一,工厂只在真的需要创建 ViewModel 时才构造。
defaultViewModelProviderFactory 内部会创建 SavedStateViewModelFactory,需要 Application、SavedStateRegistryOwner、Fragment 的 arguments 等一堆东西。如果 viewModels() 一调用就把工厂造出来,那么"声明了但这次没用上"的情况就白付了这份开销。

第二,保证拿到的是"当前对象"的实现。
属性是在 lambda 里读的,读的时机是 Lazy 首次 get()。FragmentActivity 覆写了 defaultViewModelProviderFactory 返回带参数支持的工厂——延迟读取让多态在任何时候都成立。

同样的道理,{ viewModelStore } 这层 lambda 更是关键的保命设计:

private val vm: MyViewModel by viewModels() // 写在字段初始化处

by viewModels() 在字段初始化阶段执行,此时 Activity 还没 attach 到 Application。而 getViewModelStore() 在未 attach 时会直接抛异常:

IllegalStateException: Your activity is not yet attached to the Application instance.
You can't request ViewModel before onCreate call.

因为 lambda 被延迟到首次访问 vm 时才执行,而那时 Activity 早已进入 onCreate,所以这段代码安全。把 viewModelStore 直接当参数传,这行代码在字段初始化处就会崩。

六、ViewModelLazy:真正的创建发生在第一次 get()

ViewModelLazy 是一个 Lazy<VM> 的实现,简化后大致长这样:

public class ViewModelLazy<VM : ViewModel>(
private val viewModelClass: KClass<VM>,
private val storeProducer: () -> ViewModelStore,
private val factoryProducer: () -> ViewModelProvider.Factory
) : Lazy<VM> {

private var cached: VM? = null

override val value: VM
get() {
val viewModel = cached
if (viewModel != null) return viewModel // 命中缓存,直接返回

// 首次访问:此时才去碰 store 和 factory
val factory = factoryProducer()
val store = storeProducer()
return ViewModelProvider(store, factory)
.get(viewModelClass)
.also { cached = it }
}

override fun isInitialized(): Boolean = cached != null
}

注意两个细节:

  • 它是自己实现的缓存,不依赖 lazy {}。 没有 synchronized,也没有 LazyThreadSafetyMode——因为它约定只在主线程访问,加锁纯属浪费。
  • cached 一旦赋值就永久持有。 所以同一个 Activity 里反复访问 vm,拿到的始终是同一个实例。
  • 往下再走一层,ViewModelProvider.get(KClass) 最终落到 get(key):

    // 简化的核心逻辑
    val viewModel = store[key] // 先从 store 里找
    return viewModel ?: factory.create(modelClass) // 找不到才创建
    .also { store.put(key, it) } // 创建后存回 store

    其中 key 默认就是 VM::class.java.canonicalName。这解释了为什么同一个 Activity 里声明两个不同类型的 ViewModel 不会互相覆盖——类名不同,key 就不同。

    七、完整链路:by viewModels() 到底走到哪

    by 委托只要求被委托对象提供 getValue 操作符,而 Lazy 恰好有,所以 Lazy<VM> 天生可以 by:

    private val vm: MyViewModel by viewModels()

    // 等价于
    private val vm$delegate = viewModels<MyViewModel>()
    private val vm: MyViewModel get() = vm$delegate.value

    整条链路的时序:

    时机发生了什么
    Activity 字段初始化 只创建 ViewModelLazy 对象,store 和 factory 都还是待执行的 lambda
    onCreate 及之后首次访问 vm 触发 value getter:factoryProducer() → storeProducer() → ViewModelProvider.get()
    之后每次访问 vm cached 命中,直接返回,无任何开销
    配置变更(旋转屏幕) Activity 重建,ViewModelStore 被 NonConfigurationInstances 保留下来,新 Activity 从同一个 store 里取回旧实例

    八、几个容易误解的点

    1. 传自定义 Factory,构造需要参数的 ViewModel

    private val vm: DetailViewModel by viewModels {
    object : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
    @Suppress("UNCHECKED_CAST")
    return DetailViewModel(repository) as T
    }
    }
    }

    这里的 lambda 就是 factoryProducer,因为标了 noinline 才允许写成尾随 lambda 形式。

    2. viewModels() 与 activityViewModels() 不是一回事

    • by viewModels() —— 作用域是当前组件自己的 ViewModelStore
    • Fragment 里想要 Activity 的作用域,用 by activityViewModels()(来自 androidx.fragment)

    3. ViewModel 不是"什么都不持有"

    它不持有 Activity/Fragment,但可以持有 Application。这也解释了为什么 AndroidViewModel 需要传 Application。

    4. 别在 Lazy 还没初始化时就去摸 store

    任何在 onCreate 之前访问 viewModelStore 的代码都会踩到上面那个 IllegalStateException。viewModels() 靠 lambda 把这一步推迟了,但这不代表可以自己在构造函数里直接写 viewModelStore。

    九、小结

    • inline 不是为了性能,而是为了让 reified 可行——函数体必须能在调用点展开,VM::class 才能被替换成具体类
    • noinline 是为了让 lambda 能当值存起来——它要被赋给变量、延迟到 Lazy 内部调用,不能"用完即焚"
    • 两层 lambda 是延迟求值的载体——既省掉了不必要的工厂构造,也避免在 Activity attach 之前访问 viewModelStore 抛异常
    • Lazy 是缓存 + 委托的双重收益——同一个 Activity 内只创建一次,同时天然支持 by 语法

    十行代码,没有一行是多余的。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 拆解 ComponentActivity.viewModels()
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!