kotlin by lazy详解

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) 常见坑与最佳实践

  1. 避免捕获短生命周期对象导致泄漏****

    • 在单例或长生命周期对象里 lazy { somethingUsing(activity) } 会把 Activity 引用进来导致泄漏。
    • 解决:用 applicationContext,或不要在长生命周期对象里懒持有短生命周期引用。
  2. 与展示端一致性(Android 图像/缓存相关)

    • 如果懒初始化结果跟 UI 配置强相关(尺寸/模式),确保逻辑一致,避免后续重新创建。
  3. PUBLICATION 仅用于"无副作用"初始化****

    • 因为可能运行多次,初始化代码不能写文件、打点一次性逻辑等。
  4. 需要"重置"的场景****

    • 官方 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()
}
  1. 与 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 等)。
相关推荐
xiaopengbc3 分钟前
火狐(Mozilla Firefox)浏览器离线安装包下载
前端·javascript·firefox
用户0165238444124 分钟前
Webpack5 入门与实战,前端开发必备技能无密
前端
小高00724 分钟前
🔥🔥🔥前端性能优化实战手册:从网络到运行时,一套可复制落地的清单
前端·javascript·面试
古夕27 分钟前
my-first-ai-web_问题记录01:Next.js的App Router架构下的布局(Layout)使用
前端·javascript·react.js
杨超越luckly32 分钟前
HTML应用指南:利用POST请求获取上海黄金交易所金价数据
前端·信息可视化·金融·html·黄金价格
Jerry37 分钟前
Compose 中的基本布局
前端
Hilaku43 分钟前
深入WeakMap和WeakSet:管理数据和防止内存泄漏
前端·javascript·性能优化
Juchecar1 小时前
常见的 HTML 标签及 CSS 选择器速查表
前端
前端程序猿i1 小时前
用本地代理 + ZIP 打包 + Excel 命名,优雅批量下载跨域 PDF
前端·javascript·vue.js·html
绝无仅有1 小时前
编写 Go 项目的 Dockerfile 文件及生成 Docker 镜像
后端·面试·github