kotlin by lazy 原理

当我们定义一个类的属性的时候,不希望直接初始化,而是在用的时候再初始化,示例如下:

kotlin 复制代码
class MainActivity {

    // 1️⃣ 真正保存对象的地方
    private var _binding: ActivityMainBinding? = null

    // 2️⃣ 对外访问的 getter
    private val binding: ActivityMainBinding
        get() {
            if (_binding == null) {
                _binding = ActivityMainBinding.inflate(layoutInflater)
            }
            return _binding!!
        }
}

如上代码,我们声明了一个binding属性,为了实现懒加载,我们需要再声明一个_binding属性,以实现第一次使用binding属性时才开始初始化的效果,感觉就是使用binding属性,且默认没直接初始化,在使用的时候才初始化,其实底层我们真正使用的是_binding属性。

由于懒加载比较常用,每次要写这样的模板代码挺烦人的,所以可以抽取一下:

kotlin 复制代码
class SimpleLazy<T>(private val initializer: () -> T) {

    private var value: T? = null

    fun get(): T {
        if (value === null) {
            value = initializer()
        }        
        return value!!
    }
    
}

可以看到,SimpleLazy把之前的懒加载逻辑封装起来了,以后要用懒加载的时候代码就会简单一些了,如下:

kotlin 复制代码
class MainActivity {

    private val bindingLazy = SimpleLazy { ActivityMainBinding.inflate(layoutInflater) }

    private val binding: ActivityMainBinding
        get() = bindingLazy.get()
        
}

可以看到代码比之前简单了,只是简单把属性初始化的代码封装起来而已,使用时仍然需要先声明一个bindingLazy属性来保存真正的属性值,就类似之前的_binding属性,只不过这次的SimpleLazy 把属性的初始化逻辑封装了起来,每次都可以复用,不需要每次都重复写了。

所以,为了更加简单,kotlin出了by lazy语法,如下:

kotlin 复制代码
class MainActivity {

    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

}

有了语法糖,使用起来就很简单了,但是了解原理很重要,当这个代码编译为字节码并反编译为java代码时,看它的实现原理就是前面写的SimpleLazy,示例如下:

kotlin 复制代码
class Hello {

    private val name by lazy { "Even" }

}

然后在IntelliJAndroid Studio中执行:Tools > Kotlin > Show Kotlin Bytecode,显示Kotlin字节码后,再点击 Decompile 按钮反编译为Java代码,如下:

java 复制代码
public final class Hello {

   private final Lazy name$delegate = LazyKt.lazy(Hello::name_delegate$lambda$0);

   private final String getName() {
      Lazy var1 = this.name$delegate;
      return (String)var1.getValue();
   }

   private static final String name_delegate$lambda$0() {
      return "Even";
   }
   
}

简化一下为:

java 复制代码
public final class Hello {

   private final Lazy lazy = LazyKt.lazy(Hello::initializer);

	 private static final String initializer() {
      return "Even";
   }

   private final String getName() {      
      return (String)lazy.getValue();
   }
   
}

此时,我们再看这个代码就会觉得比较简单了。而且Kotlin的懒 by lazy还做了同步处理,在Kotlin代码中,按住Ctrl再点击 lazy查看其源码,发现lazy是其实是一个顶层函数,所以在调用lazy函数时不需要加类名:

声明在LazyJVM.kt文件中的顶层函数:

kotlin 复制代码
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

SynchronizedLazyImpl的实现我就不再贴源代码了,需要时再点击去看,一看就懂了,这里只看它的构造函:

kotlin 复制代码
SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null)

可以看到,对于同步,可以使用自己的锁对象,如果此参数为null,则使用this做为锁对象。在前面的lazy函数中创建SynchronizedLazyImpl时并没有传lock参数。当我们需要使用自己的锁对象时,那我们如何传入自己的锁对象呢?在LazyJVM.kt文件中,还有另外两个重载函数:

kotlin 复制代码
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

public actual fun <T> lazy(lock: Any?, initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer, lock)

可以看到,其中一个重载函数是可以传入锁对象的,而另一个是传入LazyThreadSafetyMode,有三种模式:

  • SYNCHRONIZED - 对应Lazy实现类:SynchronizedLazyImpl
  • PUBLICATION - 对应Lazy实现类:SafePublicationLazyImpl
  • NONE - 对应Lazy实现类:UnsafeLazyImpl

看类名就能很容易理解其中两个的功能:

  • SynchronizedLazyImpl 同步的,线程安全。
  • UnsafeLazyImpl 没有同步的,多线程下使用时不安全。

它们的父类为:

kotlin 复制代码
public interface Lazy<out T> {

    public val value: T

    public fun isInitialized(): Boolean
    
}

所以,SynchronizedLazyImpl类会把valuegetter函数增加同步处理,而UnsafeLazyImplvaluegetter函数则没有同步处理,所以多线程调用时可能会导致懒加载的属性被创建两次,导致得到两个不一样的属性值。对于SynchronizedLazyImpl,我们也不需要担心因为加了锁会影响性能,因为它只在初始化时加了锁了,一但初始化完成,下一次再访问时就可以直接返回对象了,所以,除非你是在一开始就开很多线程同时访问它,导致初始化函数被多个线程同时访问了,此时除了一个线程进入锁对象外另外的线程会阻塞,此时才会影响性能。所以平时使用肯定是使用SynchronizedLazyImpl,即使我们没有多线程访问,因为性能影响微呼其呼。

SafePublicationLazyImpl,它使用了原子类,所以多线程访问也是安全的,但效率比同步锁高。简单理解这个类的功能为:允许多个线程调用初始化函数,但最终只会使用其中一个初始化函数产生的结果。所以它也有可能会产生Bug,因为初始化函数会被调用多次。如果初始化函数只允许调用一次,则使用SynchronizedLazyImpl。至于原子类如何实现无锁但又是线程安全的,可以问AI,比如我们最常使用的AtomicInteger,以前我也以为是有锁的,但它是无锁的,原子类大多都是无锁的,所以平时使用原子类时不用担心性能,因为它们是无锁的,快的很。

总结就是使用默认的就行了(即SynchronizedLazyImpl),基本能满足我们的开发需求了,极少数特殊的需要使用别的,比如,如果你有高并发使用(比如网站购物百万级访问的的初始化),为了初始化更快,则可以考虑使用SafePublicationLazyImpl,这还有一个要求,就是初始化函数不能是耗时的,则可以使用SafePublicationLazyImpl,否则结果就会适得其反,比如数据库连接、读写文件等。

另外,Lazy的getValue是一个扩展函数,而不是成员函数,getValue 之所以是"扩展函数",是为了让"被委托的类"不需要知道"属性委托语法"的存在,这是 Kotlin 为了"解耦 + 开放性 + 低侵入"做的刻意设计。

这里有个疑问,懒加载使用lazy不就够了吗,为什么还要加by?原因也很简单,代码如下:

kotlin 复制代码
val binding = lazy { ActivityMainBinding.inflate(layoutInflater) }

此时binding的类型为Lazy<ActivityMainBinding>,所以在使用时是这样的:

kotlin 复制代码
binding.value.loginButton.setOnClickListener { }

by的作用:这个属性的 getter / setter 交给 delegate 去处理,所以对于 by lazy { },则此时的委托对象就是lazy函数返回的Lazy对象。by并不是专门为lazy设计的,by用于执行委托,对于val类型,只要求委托对象有getValue即可,而对于var类型,则要求委托对象必须同时有getValuesetValue,方法原型如下:

kotlin 复制代码
operator fun getValue(thisRef, property)
operator fun setValue(thisRef, property, value)

所以by并不总是和lazy一起使用的,示例如下:

kotlin 复制代码
class ReadOnlyDelegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "hello"
    }
}

class Demo {
    val text by ReadOnlyDelegate()
}

对于getValue和setValue方法中的两个参数:

  • thisRef:告诉你"这个属性属于谁"
  • property:告诉你"你访问的是哪个属性"

使用示例如下:

kotlin 复制代码
class PreferenceDelegate<T>(
    private val default: T
) {

    operator fun getValue(thisRef: Context, property: KProperty<*>): T {
        val sp = thisRef.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
        val key = property.name

        @Suppress("UNCHECKED_CAST")
        return when (default) {
            is Int -> sp.getInt(key, default)
            is Boolean -> sp.getBoolean(key, default)
            is String -> sp.getString(key, default) as T
            else -> error("Unsupported type")
        }
    }

    operator fun setValue(thisRef: Context, property: KProperty<*>, value: T) {
        val sp = thisRef.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
        val key = property.name

        with(sp.edit()) {
            when (value) {
                is Int -> putInt(key, value)
                is Boolean -> putBoolean(key, value)
                is String -> putString(key, value)
                else -> error("Unsupported type")
            }
            apply()
        }
    }
}

在 Activity 中像普通属性一样用:

kotlin 复制代码
class SettingsActivity : AppCompatActivity() {

    var darkMode by PreferenceDelegate(false)
    var userName by PreferenceDelegate("")

    fun test() {
        darkMode = true
        println(userName)
    }
}

最后顺便提一个Android开发中的建议:在Fragment中不建议使用lazy来初始化ViewBinding,因为Fragment可能会被销毁视图,然后再次恢复,如果使用lazy初始化ViewBinding,则它永远指向第一次初始化的视图。

复制代码
onAttach
onCreate
onCreateView
onViewCreated
...
onDestroyView   ← 视图被销毁
...
onDestroy       ← Fragment 才销毁

简单理解就是onDestroyView 执行了,但onDestroy没执行,稍后onViewCreated可能会再次执行,此时应该使用新的视图,而不是旧的视图。

相关推荐
SmoothSailingT5 天前
C#——Lazy<T>懒加载机制
开发语言·单例模式·c#·懒加载
Irene19911 个月前
JavaScript 懒加载全面总结
懒加载
Sheldon一蓑烟雨任平生2 个月前
Vue3 异步组件(懒加载组件)
懒加载·vue3 异步组件·懒加载组件
cat10month2 个月前
react-loadable懒加载使用
懒加载
linweidong7 个月前
汇量科技前端面试题及参考答案
webpack·vue3·react·前端面试·hooks·懒加载·flex布局
Samdy_Chan8 个月前
同时支持Vue2/Vue3的图片懒加载组件(支持懒加载 v-html 指令梆定的 html 内容)
前端·vue·vue3·vue2·懒加载·图片懒加载·图像懒加载
大模型铲屎官1 年前
【HTML性能优化】提升网站加载速度:GZIP、懒加载与资源合并
前端·性能优化·html·gzip·懒加载·网站加载·资源合并
GJWeigege1 年前
如何实现图片懒加载,原生 + React 实现方式
前端·react.js·前端框架·懒加载·图片懒加载优化·列表优化
Play_Sai1 年前
鸿蒙ArkTS实用开发技巧: 提高效率的关键知识点
网络请求·harmonyos·arkts·懒加载·钩子函数