一、先给结论
这十行代码看起来平平无奇,实际上把 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
}
注意两个细节:
往下再走一层,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 语法
十行代码,没有一行是多余的。
网硕互联帮助中心


评论前必须登录!
注册