lateinit与by lazy的区别?从源码角度研究它们各自的原理

本篇博客主要通过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相较常规的非空类型属性主要有两处不同:

  1.  成员变量或者说字段,没有初始值也没有@NotNull注解。这意味着初始化时,字段会因为没有初始值而默认为null(后续讨论lateinit不可修饰原生类型的问题),但它也不会受到编译器的非空检查,即该非空属性在声明与构造阶段可以为空(至于为什么变量不是private而是public,不太了解原因就不讨论了);
  2. 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的一些基础知识的巩固。

  1. lateinit 是一个关键字,用于声明一个非空的var类型属性,其原理是保证属性构造时为空,访问时非空;
  2. by lazy 中的 by 是用于属性委托的关键字,lazy 是生产val属性委托Lazy的工厂方法,因此by lazy 就是将val属性的访问委托给一个Lazy对象;
  3. Lazy工厂主要有3种生产模式,正好对应单例模式的 3 种实现方式:线程不安全的懒汉模式、volatile + CAS 的线程安全模式、volatile + Synchronized 的双重检查模式。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值