背景

kotlin中的语法糖by lazy相信都有用过,但是这里面的秘密却很少有人深究下去,还有网上充斥着大量的文章,却很少能说到本质的点上,所以本jetbrains是什么软件文以字节码的视角,揭开by lazy的秘密。

一个例子

class LazyClassTest {
    val lazyTest :Test by lazy {
        Log.i("hello","初始化") 1
        Test()
    }
    fun test(){
        Log.i("hello","$lazyTest")
        Log.i("hello","$lazyTest")
    }
}

如果执行test方法,请问代号为1的log会输出几次呢?答案是1次,明明我们在test方法中执行了两次lazyTest的获取,这其中有什么不为人知的事情吗!?其实这是kotlin在编译的时候给我们施加了魔法。

编译器背后的事情

为了看清楚编译器的事情,我们直接查看编译后的字节码,这里贴出来,后面解释

删除不必要的信息
 // access flags 0x18
  final static INNERCLASS com/example/newtestproject/LazyClassTest$lazyTest$2 null null
  // access flags 0x12
  private final Lkotlin/Lazy; lazyTest$delegate
  @Lorg/jetbrains/annotations/NotNull;() // invisible
  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 7 L1
    ALOAD 0
    GETSTATIC com/example/newtestproject/LazyClassTest$lazyTest$2.INSTANCE : Lcom/example/newtestproject/LazyClassTest$lazyTest$2;
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    PUTFIELD com/example/newtestproject/LazyClassTest.lazyTest$delegate : Lkotlin/Lazy;
   L2
    LINENUMBER 5 L2
    RETURN
   L3
    LOCALVARIABLE this Lcom/example/newtestproject/LazyClassTest; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 1
  // access flags 0x11
  public final getLazyTest()Lcom/example/newtestproject/Test;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    LINENUMBER 7 L0
    ALOAD 0
    GETFIELD com/example/newtestproject/LazyClassTest.lazyTest$delegate : Lkotlin/Lazy;
    ASTORE 1
    ALOAD 1
    INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object; (itf)
    CHECKCAST com/example/newtestproject/Test
   L1
    LINENUMBER 7 L1
    ARETURN
   L2
    LOCALVARIABLE this Lcom/example/newtestproject/LazyClassTest; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 2
  // access flags 0x11
  public final test()V
   L0
    LINENUMBER 13 L0
    LDC "hello"
    ALOAD 0
    INVOKEVIRTUAL com/example/newtestproject/LazyClassTest.getLazyTest ()Lcom/example/newtestproject/Test;
    INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
    INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L1
    LINENUMBER 14 L1
    LDC "hello"
    ALOAD 0
    INVOKEVIRTUAL com/example/newtestproject/LazyClassTest.getLazyTest ()Lcom/example/newtestproject/Test;
    INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
    INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L2
    LINENUMBER 15 L2
    RETURN
   L3
    LOCALVARIABLE this Lcom/example/newtestproject/LazyClassTest; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 1

我们初始化磁盘惊讶的发现,原本的类中居然多出了一个内部类com/example/newtestprojeandroid/harmonyosct/LazyClassTestlazyTestlazyTest2,命名这么长!没错,它就是编译的时候生成的“魔法的种子”,那么这里内部类有什么特别的android下载安装地方吗?字节码层java怎么读面是看不出来初始化电脑的,因为这个这只是编初始化电脑译时期的内容,我们在虚拟机运行的时候来看,它初始化电脑时出现问题未进行更改其实是一个实现了一个接口是Lazy的内部类

public interface Lazy<out T> {
    public abstract val value: T
    public abstract fun isInitialized(): kotlin.Boolean
}

lazy背后的延时加载

为什么用了lazy就有懒加载的效果呢?其实关键就是这个,我们在iandroid是什么手机牌子ni初始化游戏启动器失败t阶段可以看到

    getstatic 'com/example/newtestproject/LazyClassTest$lazyTest$2.INSTANCE','Lcom/example/newtestproject/LazyClassTest$lazyTest$2;'
    checkcast 'kotlin/jvm/functions/Function0'
    INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    putfield 'com/example/newtestproject/LazyClassTest.lazyTest$delegate','Lkotlin/Lazy;'

在初始化的时候,只是调用了kotlin/LazyKt.lazy类的一个静态方法,针对属性复制的putfield指令,也只是对LazyClasjava是什么意思sTest.lazyTest$delegatejava培训这个内部类的一Android个Lkotlin/Lazy对象进行赋值,看起来其实跟我们的lazyTest变量毫无关系。真相是lazyTest具体的赋值操作被隐藏了而已。从这里就可以看到,为什么android的drawable类lazy是如何实现延时加载的!本质就是在初始化的时候只是生成一个内部类,不进行任何对目标对象进行赋值操作罢了!

获取操作

android平板电脑价格们再观察一下对于lazyTest变量的访问操作,从字节码看到,每次对变量的获初始化sdk什么意思取都调用了LazyClassTest的getLazyTest方法android是什么手机牌子!这个也是编译器生成的方法,具体可以看到

  public final com.example.newtestproject.Test getLazyTest() {
    aload 0
    getfield 'com/example/newtestproject/LazyClassTest.lazyTest$delegate','Lkotlin/Lazy;'
    astore 1
    aload 1
    INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object; (itf)
    checkcast 'com/example/newtestproject/Test'
    areturn
  }

天呐!我们越来越接近终点了,首先是通android什么意思过getfield指令获取了一个Lkotlin/Lazy变量,这个不就是上面我们赋值的初始化游戏启动器失败东西吗!然后调用了一个普通的方法getVa字节码是什么意思lue就结束java模拟器了,也就是说,每次对lazyTest变量的访问,都间接转发到了一个编译时生成的内部类中的一个特殊属性所调java培训用的字节码文件方法!看到这个,读者可能会思考,既然每次访问都是调用同一个方法,为什么我们by lazy时声明的lambad会只执行一次呢?编译时的字节码已经不能给我们带来答案了,这个因为像java虚拟机这种,关于具体类的调用会在运行时确定这个特性所带来的(区别于c/cpp)。

运行时的魔法

那好,我们还有最后一个神奇,就是debug,我们最终会发现初始化磁盘,在运行时by lazy的调用,其实最终都会转到如下代码的执行

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this
    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @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
                }
            }
        }

这个类位于LazyJVM中(kotlin1.5.10),我们就找到最终的秘密了,原来一开始的时候变量就是UNINITIAjava培训LIZED_VALUE,经过一次赋值操作后,就会变成实际的T所指代的类型,下次再访问的时候,就直接满足if条件返回了!所以这就是一初始化sdk什么意思次赋值的秘密!还有我们可以看到,默认的by lazy操作第一次赋值时,是采用了syncJavahronized进行了加锁操作!

总结

我们已经全方位揭秘了by lazy的魔法面android是什么手机牌子Java,相信也初始化电脑对这个语法糖有了自己更深的理解,之所以写这篇字节码是什么意思文,是因为好多网上资料要么是含糊不清要么是无法解释本质,这里作为一个记录分享

往期更精彩:

想要实现JetBrains一个hook库吗,快看过来/post/710008…