1) 它到底做了什么?
kotlin
val imageLoader by lazy { HeavyImageLoader(context) }
-
by lazy { ... } 是 属性委托。编译器会生成一个 Lazy 对象保存到一个隐藏字段里(例如 imageLoader$delegate)。
-
第一次读取该属性时才执行大括号里的 初始化逻辑 ,并把结果缓存;之后每次读取都直接返回缓存,不会再次计算。
-
只能用于 val(只读)属性;不能用于 var。
等价伪代码(简化):
csharp
private val imageLoader$delegate = lazy { HeavyImageLoader(context) }
val imageLoader: HeavyImageLoader
get() = imageLoader$delegate.value
2) 线程安全模式(LazyThreadSafetyMode)
kotlin
val a by lazy { ... } // 默认 SYNCHRONIZED
val b by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ... }
val c by lazy(LazyThreadSafetyMode.PUBLICATION) { ... }
val d by lazy(LazyThreadSafetyMode.NONE) { ... }
-
SYNCHRONIZED(默认)****
- 内部用同步锁保证仅初始化一次;对多线程最安全。
- 代价:第一次初始化有同步开销。
-
PUBLICATION****
- 多线程并发读取时,初始化代码可能被执行多次,但只有一个结果会被保存("先发布者胜出")。
- 适合初始化无副作用的计算(幂等/可重复)。
-
NONE****
-
不加锁 ,只适合单线程场景(如 Android 主线程使用)。
-
优点:最省开销;缺点:多线程下不安全,可能读到半初始化状态或多次初始化。
-
Android 常见推荐:在主线程上使用的懒属性可用 NONE 提升性能:
scss
val viewBinding by lazy(LazyThreadSafetyMode.NONE) { ActivityMainBinding.inflate(layoutInflater) }
3) 生命周期与可见性细节
-
只初始化一次:初始化成功后,结果持久缓存到 Lazy;除非你手动实现"可重置的 lazy"(见下方)。
-
递归访问会抛错:如果在初始化块内再次访问同一个 lazy 属性,会抛出 IllegalStateException("Recursive call in a lazy value initialization")。
-
可见性/发布:
-
SYNCHRONIZED / PUBLICATION 提供正确的发布语义(其他线程看到的是完全构造好的对象)。
-
NONE 不保证跨线程可见性(不要在多线程读写)。
-
4) 与
lateinit
的区别
对比项 | by lazy | lateinit |
---|---|---|
适用修饰 | val(只读) | var(可变),且非空引用类型 |
何时赋值 | 第一次读取时自动初始化 | 你自己在某处显式赋值 |
线程安全 | 可选 3 种模式 | 取决于你何时/如何赋值 |
未赋值访问 | 不存在(第一次读取即赋值) | 抛 UninitializedPropertyAccessException |
用途 | 惰性构造昂贵对象 | 框架/DI/视图绑定等需要稍后注入 |
简而言之:需要"读到时才建"选 lazy,需要"之后我自己再设值"选 lateinit。
5) 典型用法示例
5.1 重对象/单例
scss
object ApiHolder {
val retrofit by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
val service by lazy { retrofit.create(ApiService::class.java) }
}
5.2 Android 主线程场景(无锁)
kotlin
class MainActivity : AppCompatActivity() {
private val binding by lazy(LazyThreadSafetyMode.NONE) {
ActivityMainBinding.inflate(layoutInflater)
}
}
5.3 与函数引用
kotlin
val db by lazy(::createDatabase) // 等价 lazy { createDatabase() }
6) 常见坑与最佳实践
-
避免捕获短生命周期对象导致泄漏****
- 在单例或长生命周期对象里 lazy { somethingUsing(activity) } 会把 Activity 引用进来导致泄漏。
- 解决:用 applicationContext,或不要在长生命周期对象里懒持有短生命周期引用。
-
与展示端一致性(Android 图像/缓存相关)
- 如果懒初始化结果跟 UI 配置强相关(尺寸/模式),确保逻辑一致,避免后续重新创建。
-
PUBLICATION 仅用于"无副作用"初始化****
- 因为可能运行多次,初始化代码不能写文件、打点一次性逻辑等。
-
需要"重置"的场景****
- 官方 lazy 不支持重置。可用自定义委托:
kotlin
class ResettableLazy<T>(private val initializer: () -> T) {
@Volatile private var _value: Any? = UNINITIALIZED
fun get(): T {
val v1 = _value
if (v1 !== UNINITIALIZED) @Suppress("UNCHECKED_CAST") return v1 as T
return synchronized(this) {
val v2 = _value
if (v2 !== UNINITIALIZED) @Suppress("UNCHECKED_CAST") v2 as T
else initializer().also { _value = it }
}
}
fun reset() { synchronized(this) { _value = UNINITIALIZED } }
private object UNINITIALIZED
}
// 用法
class Repo {
private val _client = ResettableLazy { createClient() }
val client get() = _client.get()
fun resetClient() = _client.reset()
}
-
与 Compose 的关系****
- Compose 里UI 状态应使用 remember/rememberSaveable 而非 by lazy。lazy 不会触发重组,也不会随生命周期自动清理。
7) 性能与实现细节(简述)
-
默认 SYNCHRONIZED 的懒实现是"双重检查+锁"风格,JVM 上安全发布;PUBLICATION 用原子引用/自旋允许多次计算;NONE 就是朴素字段判断。
-
读取后是普通字段访问,极快;初始化那一次的成本由你初始化逻辑决定。
8) 小结与选型建议
- 大多数多线程环境:默认 by lazy { ... }(= SYNCHRONIZED)就对了。
- Android 主线程懒加载(如绑定/适配器/资源)用 NONE 去掉锁开销。
- 无副作用初始化且可能多线程读:PUBLICATION。
- 需要可变/稍后注入:用 lateinit var 而不是 lazy。
- 避免在 lazy 捕获易泄漏的上下文(Activity、View 等)。