注册

kotlin - 你真的了解 by lazy吗

背景


kotlin中的语法糖by lazy相信都有用过,但是这里面的秘密却很少有人深究下去,还有网上充斥着大量的文章,却很少能说到本质的点上,所以本文以字节码的视角,揭开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/newtestproject/LazyClassTestlazyTestlazyTestlazyTest2,命名这么长!没错,它就是编译的时候生成的“魔法的种子”,那么这里内部类有什么特别的地方吗?字节码层面是看不出来的,因为这个这只是编译时期的内容,我们在虚拟机运行的时候来看,它其实是一个实现了一个接口是Lazy的内部类


public interface Lazy<out T> {
public abstract val value: T

public abstract fun isInitialized(): kotlin.Boolean
}

lazy背后的延时加载


为什么用了lazy就有懒加载的效果呢?其实关键就是这个,我们在init阶段可以看到


    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指令,也只是对LazyClassTest.lazyTest$delegate这个内部类的一个Lkotlin/Lazy对象进行赋值,看起来其实跟我们的lazyTest变量毫无关系。真相是lazyTest具体的赋值操作被隐藏了而已。从这里就可以看到,为什么lazy是如何实现延时加载的!本质就是在初始化的时候只是生成一个内部类,不进行任何对目标对象进行赋值操作罢了!


获取操作


我们再观察一下对于lazyTest变量的访问操作,从字节码看到,每次对变量的获取都调用了LazyClassTest的getLazyTest方法!这个也是编译器生成的方法,具体可以看到


  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
}

天呐!我们越来越接近终点了,首先是通过getfield指令获取了一个Lkotlin/Lazy变量,这个不就是上面我们赋值的东西吗!然后调用了一个普通的方法getValue就结束了,也就是说,每次对lazyTest变量的访问,都间接转发到了一个编译时生成的内部类中的一个特殊属性所调用的方法!看到这个,读者可能会思考,既然每次访问都是调用同一个方法,为什么我们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),我们就找到最终的秘密了,原来一开始的时候变量就是UNINITIALIZED_VALUE,经过一次赋值操作后,就会变成实际的T所指代的类型,下次再访问的时候,就直接满足if条件返回了!所以这就是一次赋值的秘密!还有我们可以看到,默认的by lazy操作第一次赋值时,是采用了synchronized进行了加锁操作!


总结


我们已经全方位揭秘了by lazy的魔法面纱,相信也对这个语法糖有了自己更深的理解,之所以写这篇文,是因为好多网上资料要么是含糊不清要么是无法解释本质,这里作为一个记录分享


作者:Pika
链接:https://juejin.cn/post/7106320684275482631
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册