当我们定义一个类的属性的时候,不希望直接初始化,而是在用的时候再初始化,示例如下:
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" }
}
然后在IntelliJ或Android 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实现类:SynchronizedLazyImplPUBLICATION- 对应Lazy实现类:SafePublicationLazyImplNONE- 对应Lazy实现类:UnsafeLazyImpl
看类名就能很容易理解其中两个的功能:
SynchronizedLazyImpl同步的,线程安全。UnsafeLazyImpl没有同步的,多线程下使用时不安全。
它们的父类为:
kotlin
public interface Lazy<out T> {
public val value: T
public fun isInitialized(): Boolean
}
所以,SynchronizedLazyImpl类会把value的getter函数增加同步处理,而UnsafeLazyImpl对value的getter函数则没有同步处理,所以多线程调用时可能会导致懒加载的属性被创建两次,导致得到两个不一样的属性值。对于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类型,则要求委托对象必须同时有getValue和setValue,方法原型如下:
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可能会再次执行,此时应该使用新的视图,而不是旧的视图。