本篇博客主要通过Kotlin的源码、字节码和反编译JAVA来了解这两种延迟加载机制的原理。
(因为基础知识早就忘得差不多了,看代码常常会觉得一知半解,所以会随时穿插一些很碎的小知识点。也会有很多个人理解,如有错误,期盼指正。)
一、lateinit 关键字
一句话描述:lateinit 允许一个非空属性在构造之外进行初始化。
1、为什么要使用lateinit
对Kotlin来说,类的属性需要在或构造函数、或声明时、或init代码块中进行初始化。如果初始化时属性值尚为空,那可能就需要将其声明为可空类型?,并在后续使用时进行非空判断。
但在实际开发中,常常会有属性的值在类初始化阶段无法确定、但可以保证其在具体使用时非空的场景。例如比较常见的,由Activity的加载流程引入的延迟初始化的需求:Activity一般在创建完成后再调用Activity.attach()方法来关联上下文,进行mApplication、mWindow、mWindowManager等成员变量的初始化,并在Activity.onCreate()方法中加载布局并绑定,这就意味着那些基于mApplication等上下文或指向布局中具体View的属性无法在Activity构造时完成初始化;但同时,只要它们在onCreate或其它恰当的阶段完成设置,就可以保证后续实际访问时非空,即无需再时时进行非空判断。
为了免除在这样的初始化延迟的场景中对属性进行大量非空判断,Kotlin提出了lateinit这一关键字。
2、lateinit的基本原理
lateinit允许声明一个未即时初始化的非空类型的属性。
尝试定义一个lateinit属性和一个普通的非空类型属性,如下是相应的JAVA代码。
class LateinitAndByLazy{
lateinit var lateStr: String
var noLatestr: String = "No Late"
}
public final class LateinitAndByLazy {
// 一、 没有初始值与非空注解 @NotNull
public String lateStr;
@NotNull
private String noLatestr = "No Late";
@NotNull
public final String getLateStr() {
String var1 = this.lateStr;
// 二、进行非空检查
if (var1 != null) {
return var1;
} else {
Intrinsics.throwUninitializedPropertyAccessException("lateStr");
return null;
}
}
public final void setLateStr(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.lateStr = var1;
}
@NotNull
public final String getNoLatestr() {
return this.noLatestr;
}
public final void setNoLatestr(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.noLatestr = var1;
}
}
知识点补充(个人理解,不太确定):Kotlin中定义的类的属性,实际包含了一个幕后字段和它相应的Getter、Setter访问器,对应到JAVA中便是一个成员变量(幕后字段)和它的get、set方法(访问器)。
对比发现,lateinit相较常规的非空类型属性主要有两处不同:
- 成员变量或者说字段,没有初始值也没有@NotNull注解。这意味着初始化时,字段会因为没有初始值而默认为null(后续讨论lateinit不可修饰原生类型的问题),但它也不会受到编译器的非空检查,即该非空属性在声明与构造阶段可以为空(至于为什么变量不是private而是public,不太了解原因就不讨论了);
- Getter内部进行非空检查。对于普通的非空类型属性,编译器会检查并确保字段非空,所以get访问时直接返回即可;但lateinit因为告知了编译器不做字段的非空检查,所以需要在get内部自主进行额外的非空判断。同时可以发现,lateinit并不是“确保”而只是“相信”属性非空,如果属性真的为空,它只会throw exception。
所以简单推测并总结lateinit的工作原理,就是实现属性的构造时为空、访问时非空。
3、lateinit的几个使用禁忌
由原理就自然而然地引出了lateinit的几个使用禁忌:
1. 只能修饰非空类型(可空就与lateinit的设计根本背道而驰了);
2. 只能修饰var而不能修饰val,lateinit的属性是需要使用Setter设置其真正的初始值的,所以即便是初始化后就不再改变的场景也不适用没有Setter的val变量;
3. 不能使用自定义的Getter与Setter,个人理解,lateinit主要是因为Getter需要自主进行属性字段的非空检查,所以不宜自定义修改;
4. 不能修饰原生类型。以下以数字类型Int为例。
参考文档 数字在JVM上的表示 可知,kotlin的Int、Double等非空数字类型在JVM中一般会存储为int、double等原生类型,在可空引用如Int?的场景中,数字则会被装箱(boxed)为Integer、Double等Java类。
var notNullInt: Int = 0
var nullableInt: Int? = null
// 非空Int 对应 int原生 (不需要@NotNull注解)
private int notNullInt;
// 可空Int 对应 Integer类
@Nullable
private Integer nullableInt;
而int原生是不可能为空的,如果一个int没有初始化,JVM会自动将其设置为默认初始值0。所以个人理解,lateinit不支持原生类型,是因为它基于null与非null的设计已经足够简单有效,没必要解决原生类型在"非空->原生"和"可空->装箱"上的冲突。就像要声明一个Int类型的延迟初始化属性,那直接使用Integer类型即可。
4、补充:isInitialized
// 来自 Lateinit.kt
@SinceKotlin("1.2")
@InlineOnly
inline val @receiver:AccessibleLateinitPropertyLiteral KProperty0<*>.isInitialized: Boolean
get() = throw NotImplementedError("Implementation is intrinsic")
lateinit提供了一个isInitialized变量来判断属性是否进行了初始化。从定义来看,它是一个inline内联属性,兼KProperty0的扩展属性。
从KProperty扩展属性的角度来看,它使用了位置注解@receiver,对接收者(也就是被扩展类的实例/要访问isInitialized的Property对象)进行了限制——AccessibleLateinitPropertyLiteral,即接收者需要是一个Literal字面的lateinit属性的引用,并且lateinit属性的幕后字段在调用位置是accessible的。
因此使用isInitialized变量时,要先用 属性引用 操作符::获取lateinit属性对应的KProperty0对象,然后就可以像访问一个一等对象一样访问lateinit属性。
lateinit var lateStr: String
fun check(): Boolean {
// this::lateStr,this可省略
return ::lateStr.isInitialized
}
"Literal字面的",则是意味着如下的使用方法是错误的:
fun check(): Boolean {
val prop = ::lateStr
return prop.isInitialized // 编译报错
}
无论是扩展属性还是内联属性,其实都是没有幕后字段的,它们的访问由GETTER/SETTER来控制。而内联inline属性的独特一点是,它的访问器会像内联函数一样在调用点展开,所以查看::lateStr.isInitialized对应的JAVA代码与字节码,会发现没有isInitialized相关的变量或方法的身影,只有原地展开的一段非空判断。
public final boolean check() {
return this.lateStr != null;
}
public final check()Z
L0
LINENUMBER 11 L0
ALOAD 0
GETFIELD com/example/juejin/LateinitAndByLazy.lateStr : Ljava/lang/String;
IFNULL L1 // 直接做非空判断
ICONST_1
GOTO L2
L1
FRAME SAME
ICONST_0
L2
FRAME SAME1 I
IRETURN
L3
LOCALVARIABLE this Lcom/example/juejin/LateinitAndByLazy; L0 L3 0
MAXSTACK = 1
MAXLOCALS = 1
(至于为什么Lateinit.kt中定义get直接throw NotImplementationError实际却是进行非空判断,我搜了一些资料依然没太懂,所以lateinit就先看到这里了。)
二、by lazy
1、关键字 by 与 属性委托
by是Kotlin提供的一个实现委托机制的软关键字,主要用于类委托(将接口的实现委托给另一个对象)与属性委托(将属性的访问器的实现委托给另一个对象),后者便是这次讨论的重点,它的基础语法如下:
val/var <property name>: <Type> by <expression>
by关键字意味着,将前面属性的Getter/Setter访问器的实现委托给后面expression的getValue/setValue方法。而根据官方文档中的 属性委托要求,expression要成为一个合法有效的委托delegate,只需在成员函数或扩展函数中提供相应的operator fun操作符函数getValue/setValue即可。
如下是一个var属性的委托示例,delegate实现了两个操作符函数getValue与setValue;如果是val属性,那么只提供getValue即可。
class Owner { // 属性所有者
var delegatedStr: String by Delegate() // 被委托属性
}
class Delegate { // 委托
var delegateStr: String = "DELEGATE"
operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
// thisRef, 与属性所有者类型相同或者是其超类型
// property, 必须是类型KProperty<*>或其超类型
// 返回与属性同类型或其子类型
return delegateStr
}
operator fun setValue(thisRef: Owner, property: KProperty<*>, value: Any?) {
// value,与属性同类型或其超类型
if (value is String){
delegateStr = value
}
}
}
(operator修饰符,将一个函数标记为重载一个操作符或者实现一个约定,provideDelegate、getValue和setValue是属于属性委托机制的3个操作符函数。)
2、延迟属性 与 lazy()
Kotlin提出了几种常用的属性委托的应用场景,例如延迟属性Lazy Properties(只在首次访问时计算属性值),和可观察属性Observable properties(属性值更改时通知监听器),并在标准库中为它们提供了相应的工厂方法。
而lazy(),便是延迟属性的工厂方法,它负责生成实现了延迟属性委托的Lazy<T>实例。
Lazy接口
// 来自 Lazy.Kt
public interface Lazy<out T> {
public val value: T
public fun isInitialized(): Boolean
}
@kotlin.internal.InlineOnly
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
首先查看Lazy的源码。它是一个接口,定义了一个val接口属性和一个isInitialized()接口方法,还定义了一个扩展的操作符函数getValue()。
根据1中的属性委托要求可知,这个扩展函数意味着Lazy的实例可以成为一个val属性的合法委托。同时因为它也是一个内联函数,所以它会原地展开为对value属性的访问。
默认Lazy实例
接着查看lazy()源码,看它是如何返回一个Lazy实例,以及Lazy实例是如何实现对value的访问的。
// 来自 LazyJVM.kt
// 参数initializer,一个返回结果为T的函数
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
// lazy() 默认返回一个线程安全模式的Lazy实例
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
// initializer,指定的初始化函数
private var initializer: (() -> T)? = initializer
// private var,幕后属性,有字段但没有Getter与Setter
// UNINITIALIZED_VALUE,Lazy.kt中定义的一个internal object,用于标识未初始化的状态
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// 默认使用本身(即要返回的Lazy实例)进行同步,所以要注意避免在外部对返回实例进行同步,可能会造成死锁
private val lock = lock ?: this
// 实现接口属性value的Getter
override val value: T
get() {
val _v1 = _value
// 一次判空
if (_v1 !== UNINITIALIZED_VALUE) {
// @Suppress注解,用于消除编译时警告
// “UNCHECKED_CAST”,类型转换警告
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
// 二次判空
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}
//实现接口方法isInitialized()
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
}
可以看到,线程安全的SynchronizedLazyImpl实例作为一个属性委托,它的内联getValue(thisRef, property)操作符函数会原地展开为value属性的访问器Getter(一个无参的getValue()),并按照一种线程安全的单例模式,使用initializer函数构建并返回幕后属性_value(也就是单例对象)。
这种单例模式采用 volatile修饰单例对象 + 一次判空后synchronized 的经典双重检查锁方案,保证_value只使用initializer进行一次初始化。
(知识点补充:为什么需要volatile修饰单例对象?
一般常说,volatile可以保证有序性、可见性,synchronized可以保证原子性、有序性、可见性,但其实二者的“有序性”并不尽相同:volatile是禁止指令重排的有序,synchronized则更多的是强调多个线程之间执行顺序的有序,而从单个线程的角度来看,当前代码块的指令重排并不会影响执行结果,是一种as-if-serial('如'有序,类似如来的一种概念),synchronized并不禁止。
但实际上,单个线程的指令重排是有可能会破坏多线程的线程安全的,一个典型案例便是在双重检查锁的单例模式中,创建单例对象过程的指令重排。
JVM要创建一个对象,大致可分为三步:1、分配内存;2、初始化;3、指向内存地址。对单个线程来说,2与3的重排不会影响执行结果,但在双重检查锁的单例模式中,如果有其它线程恰好在步骤3完成后进行了第一次判空,那么它便会认为单例存在,并可能在第2步初始化尚未完成时返回结果。
所以在这类单例模式中,需要使用volatile修饰,以禁止单例对象创建时的指令重排。)
剩余两种Lazy实例
Lazy定义了三种线程安全模式LazyThreadSafetyMode。lazy()工厂默认使用上面介绍过的SYNCHRONIZED模式,也支持显式地指定其它线程安全模式。
public enum class LazyThreadSafetyMode {
SYNCHRONIZED,
PUBLICATION,
NONE,
}
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T>
接下来简单介绍一下剩余两种安全模式。
首先是LazyThreadSafetyMode.NONE,顾名思义,它没有使用任何同步机制,因此是线程不安全的。源码如下,getValue()的思路很直接。
// 来自 Lazy.kt
internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
private var _value: Any? = UNINITIALIZED_VALUE
override val value: T
get() {
if (_value === UNINITIALIZED_VALUE) {
_value = initializer!!()
initializer = null
}
@Suppress("UNCHECKED_CAST")
return _value as T
}
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
}
然后是LazyThreadSafetyMode.PUBLICATION,它主要采用了volatile + CAS机制 来实现无锁的线程安全。
// 来自 LazyJVM.kt
private class SafePublicationLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
// volatile 可见性+有序性
@Volatile private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
private val final: Any = UNINITIALIZED_VALUE
override val value: T
get() {
val value = _value
if (value !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return value as T
}
val initializerValue = initializer
if (initializerValue != null) {
// 使用initializerValue而不是initializer,因为此时initializer可能已被其它线程置空
val newValue = initializerValue()
// CAS 原子性
if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) {
// volatile + CAS 确保只有一个线程可以到达这里
initializer = null
return newValue
}
}
@Suppress("UNCHECKED_CAST")
return _value as T
}
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
companion object {
// valueUpdater,newUpdater返回的一个AtomicReferenceFieldUpdaterImpl实例
// companion object中定义的private val相当于类的静态变量,也就是所有实例共享这个valueUpdater
private val valueUpdater = java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater(
// 持有字段的对象的类
SafePublicationLazyImpl::class.java,
// 字段的类
Any::class.java,
// 字段的名字
"_value"
)
}
}
查看源码可知,该模式下_value的初始化借助AtomicReferenceFieldUpdater实现,这是一个基于反射的工具类,允许对指定类的指定volatile字段进行原子性的更新操作。
如下是它的实例的compareAndSet()方法,只有_value的值是UNINITIALIZED_VALUE,即未初始化状态时,才能进行更新并返回true,所以该模式可能同时有多个线程调用initializer函数,但只有第一个更改_value的线程可以成功设置初始值。
// AtomicReferenceFieldUpdaterImpl
public final boolean compareAndSet(T obj, V expect, V update) {
// 校验对象的类型
accessCheck(obj);
// 校验更新值的类型
valueCheck(update);
// 通过UnSafe进行CAS
return U.compareAndSetReference(obj, offset, expect, update);
}
3、by lazy
基于前面对by属性委托和lazy源码的了解,现在可以将by lazy延迟初始化机制的原理简单总结为 委托 + 单例 。本小节就继续从字节码和JAVA代码的角度入手,了解by lazy的一些实现细节。
如下是by lazy一个简单的使用示例,传入一个lambda表达式,最后一行返回被委托属性的初始值。
(因为之前介绍过,Lazy接口实现的是val属性的委托,所以by lazy只能实现val属性的延迟初始化。)
class LateinitAndByLazy{
val lazyStr: String by lazy {
"LAZY STR"
}
}
查看反编译的JAVA代码,发现持有被委托属性的类在构造时,会初始化一个Lazy实例作为被委托属性的delegate,而被委托属性则没有了对应的幕后字段,只有对应的get方法。
这里可以再回忆一下之前介绍的属性委托和Lazy的内容:委托之后,该被委托属性的Getter访问器会交由delegate的getValue(thisRef, property)函数实现,同时,又因为Lazy的这个函数是一个内联函数,所以它会原地展开为对delegate的value属性的访问,也就一个无参的getValue(),也就是说,在被委托属性的get方法中调用delegate的getValue()。
public final class LateinitAndByLazy {
// 没有lazyStr的幕后字段
// 但有一个lazyStr的委托实例 lazyStr$delegate
@NotNull
private final Lazy lazyStr$delegate;
public LateinitAndByLazy() {
// Lazy创建实例时,传入了一个Function0类型的参数
this.lazyStr$delegate = LazyKt.lazy((Function0)null.INSTANCE);
}
// lazyStr的Getter由lazyStr$delegate实现
@NotNull
public final String getLazyStr() {
Lazy var1 = this.lazyStr$delegate;
Object var2 = null;
// 原地展开的Lazy value属性Getter
return (String)var1.getValue();
}
}
JAVA代码中,构造Lazy实例时会传入一个(Function0)null.INSTANCE,这是lambda表达式没有持有外部引用时的编译方式。查看字节码可知,lambda对应着一个内部类,这个内部类持有一个静态成员变量INSTANCE,它是类加载<clinit>时就生成的一个内部类实例。
// 一个内部类
final static INNERCLASS com/example/juejin/LateinitAndByLazy$lazyStr$2 null null
。。。
//外部类初始化时,读取内部类的INSTANCE静态变量作为Lazy的参数Function0
GETSTATIC com/example/juejin/LateinitAndByLazy$lazyStr$2.INSTANCE : Lcom/example/juejin/LateinitAndByLazy$lazyStr$2;
CHECKCAST kotlin/jvm/functions/Function0
INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
PUTFIELD com/example/juejin/LateinitAndByLazy.lazyStr$delegate : Lkotlin/Lazy;
。。。
//内部类的定义
final class com/example/juejin/LateinitAndByLazy$lazyStr$2 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {
//内部类有一个静态成员变量INSTANCE
public final static Lcom/example/juejin/LateinitAndByLazy$lazyStr$2; INSTANCE
//类加载时init一个实例并PUTSTATIC作为INSTANCE
static <clinit>()V
NEW com/example/juejin/LateinitAndByLazy$lazyStr$2
DUP
INVOKESPECIAL com/example/juejin/LateinitAndByLazy$lazyStr$2.<init> ()V
PUTSTATIC com/example/juejin/LateinitAndByLazy$lazyStr$2.INSTANCE : Lcom/example/juejin/LateinitAndByLazy$lazyStr$2;
当lambda表达式中持有对外部的引用时,反编译的JAVA代码会更直观一些,它不再是一个内部类的静态变量,而是新建一个Function0实例。但是这块知识点好像挺复杂的,会有点跑题,这里就不再讨论了。总之,就是lambda表达式会处理为一个Function0实例,作为initializer传入Lazy。
(后续补充:因为恰好看到了Kotlin的嵌套类的区别,)
class LateinitAndByLazy{
val outterStr = "Outter"
val outLazyStr: String by lazy {
outterStr
}
}
@NotNull
private final Lazy outLazyStr$delegate = LazyKt.lazy((Function0)(new Function0() {
public final String invoke() {
return LateinitAndByLazy.this.getOutterStr();
}
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}
}));
final static INNERCLASS com/example/juejin/LateinitAndByLazy$outLazyStr$2 null null
。。。
public <init>()V
。。。
L2
LINENUMBER 8 L2
ALOAD 0
NEW com/example/juejin/LateinitAndByLazy$outLazyStr$2
DUP
ALOAD 0
// 不是直接读取静态INSTANCE,而是新建一个实例
INVOKESPECIAL com/example/juejin/LateinitAndByLazy$outLazyStr$2.<init> (Lcom/example/juejin/LateinitAndByLazy;)V
CHECKCAST kotlin/jvm/functions/Function0
INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
PUTFIELD com/example/juejin/LateinitAndByLazy.outLazyStr$delegate : Lkotlin/Lazy;
总结
大概就是练习看看源码、字节码这些东西,穿插着对Kotlin的一些基础知识的巩固。
- lateinit 是一个关键字,用于声明一个非空的var类型属性,其原理是保证属性构造时为空,访问时非空;
- by lazy 中的 by 是用于属性委托的关键字,lazy 是生产val属性委托Lazy的工厂方法,因此by lazy 就是将val属性的访问委托给一个Lazy对象;
- Lazy工厂主要有3种生产模式,正好对应单例模式的 3 种实现方式:线程不安全的懒汉模式、volatile + CAS 的线程安全模式、volatile + Synchronized 的双重检查模式。