kotlin入门潜修之类和对象篇—委托及其原理(二)

本文收录于 kotlin入门潜修专题系列,欢迎学习交流。

创作不易,如有转载,还请备注。

写在前面

天生人而使有贪有欲,欲有情,情有节。圣人修节以止欲,故不过行其情也。——与君共勉。

委托方法实现原理

本篇文章将阐述委托实现的背后原理。

照例,先上我们要分析的源代码:

interface IWork {//IWork接口
    fun doWork()
}
//被委托者Boss类,实现了IWork接口
class Boss(val time: String) : IWork {
    override fun doWork() {
        println(time)
    }
}
//委托者Secretary类,实现了IWork接口,同时委托了自己的实现给work对象
class Secretary(work: IWork) : IWork by work
//测试类Main
class Main {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val boss = Boss("today")//生成一个Boss类对象boss
            val secretary = Secretary(boss)//boss对象作为Secretary的委托实现,即boss是被委托者
            secretary.doWork()//调用secretary的doWork方法,实际上是调用了boss的doWork方法。
        }
    }
}

接下来我们把上述源代码的字节码全部粘贴上来(这里测试类Main对应的字节码暂时不粘贴),可能有点长,可以不必先全部看完,下面会一段段分析,这里先给个整体的印象:

// ================Boss.class =================
// class version 50.0 (50)
// access flags 0x31
public final class Boss implements IWork  {
  // access flags 0x1
  public doWork()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    GETFIELD Boss.time : Ljava/lang/String;
    ASTORE 1
   L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L2
   L3
    LINENUMBER 8 L3
    RETURN
   L4
    LOCALVARIABLE this LBoss; L0 L4 0
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x12
  private final Ljava/lang/String; time
  @Lorg/jetbrains/annotations/NotNull;() // invisible

  // access flags 0x11
  public final getTime()Ljava/lang/String;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    LINENUMBER 5 L0
    ALOAD 0
    GETFIELD Boss.time : Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE this LBoss; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public <init>(Ljava/lang/String;)V
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 1
    LDC "time"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 5 L1
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    ALOAD 0
    ALOAD 1
    PUTFIELD Boss.time : Ljava/lang/String;
    RETURN
   L2
    LOCALVARIABLE this LBoss; L0 L2 0
    LOCALVARIABLE time Ljava/lang/String; L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2
// ================Secretary.class =================
// class version 50.0 (50)
// access flags 0x31
public final class Secretary implements IWork  {
  // access flags 0x1
  public <init>(LIWork;)V
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 1
    LDC "work"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 12 L1
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    ALOAD 0
    ALOAD 1
    PUTFIELD Secretary.$$delegate_0 : LIWork;
    RETURN
   L2
    LOCALVARIABLE this LSecretary; L0 L2 0
    LOCALVARIABLE work LIWork; L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1012
  private final synthetic LIWork; $$delegate_0

  // access flags 0x1
  public doWork()V
   L0
    ALOAD 0
    GETFIELD Secretary.$$delegate_0 : LIWork;
    INVOKEINTERFACE IWork.doWork ()V
    RETURN
   L1
    LOCALVARIABLE this LSecretary; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
  // compiled from: Main.kt
}
// ================IWork.class =================
// class version 50.0 (50)
// access flags 0x601
public abstract interface IWork {
  // access flags 0x401
  public abstract doWork()V
    LOCALVARIABLE this LIWork; L0 L1 0
  // compiled from: Main.kt
}

上面就是我们要分析的全部字节码,这里主要看class Secretary(work: IWork) : IWork by work
这句源码对应的字节码,因为这个是委托的真正实现。分析总结如下:

  1. 对于by关键字,kotlin编译器会为我们生成一个同实现接口类型(这里即是IWork类型)一致的private final的属性成员,其名字为$$delegate_0,字节码对应如下:
  private final synthetic LIWork; $$delegate_0
  1. $$delegate_0属性会在Secretary实例构造方法中完成初始化,其值正是我们通过Secretary构造方法传入的boss对象,字节码如下所示:
// access flags 0x1
  public <init>(LIWork;)V//Secretary的实例构造方法
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 1
    LDC "work"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1//重点在L1这部分内容
    LINENUMBER 12 L1
    ALOAD 0//实际上将this装载到了当前操作数栈中
    INVOKESPECIAL java/lang/Object.<init> ()V//调用了父类构造方法
    ALOAD 0
    ALOAD 1//这里实际上是将构造方法中的work对象装载到操作栈顶,这个work对象实际上是外部传入的boss类对象
    PUTFIELD Secretary.$$delegate_0 : LIWork;//将装入的work对象赋值给$$delegate_0
    RETURN
   L2
    LOCALVARIABLE this LSecretary; L0 L2 0
    LOCALVARIABLE work LIWork; L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2
  1. 通过 secretary.doWork()来调用doWork方法的时候,实际上是通过 $$delegate_0属性成员来完成调用的,要确认这一点,只需要找到Secretary类中生成的doWork方法对应的字节码即可,如下所示:
  public doWork()V//kotlin编译器为委托类Secretary生成的doWork方法
   L0
    ALOAD 0
    GETFIELD Secretary.$$delegate_0 : LIWork;//这里实际上就是上面kotlin为我们生成的IWork类型的对象
    INVOKEINTERFACE IWork.doWork ()V//INVOKEINTERFACE指令实际上是调用接口方法的指令,他会搜索实现了IWork接口对象的doWork方法,
//这里正是$$delegate_0对象
    RETURN
   L1
    LOCALVARIABLE this LSecretary; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

至此关于委托方法阐述完毕。

委托属性实现原理

本小节来看下委托属性的实现原理,这里主要以lazy委托为例进行分析。

照例先上我们要分析的源代码,如下所示:

class Test {
    val lazyVal: String by lazy {
        "hello"
    }

    fun test(){
        print(lazyVal)
    }
}

源代码及其简单,但是其生成的字节码却很复杂,这里照例先把上面代码对应的字节码粘贴如下:

// ================Test.class =================
// class version 50.0 (50)
// access flags 0x31
public final class Test {
  // access flags 0x12
  private final Lkotlin/Lazy; lazyVal$delegate
  @Lorg/jetbrains/annotations/NotNull;() // invisible

  // access flags 0x11
  public final getLazyVal()Ljava/lang/String;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    ALOAD 0
    GETFIELD Test.lazyVal$delegate : Lkotlin/Lazy;
    ASTORE 1
    ALOAD 0
    ASTORE 2
    GETSTATIC Test.$$delegatedProperties : [Lkotlin/reflect/KProperty;
    ICONST_0
    AALOAD
    ASTORE 3
   L1
    ALOAD 1
    INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object;
   L2
    CHECKCAST java/lang/String
    ARETURN
   L3
    LOCALVARIABLE this LTest; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 4

  public final test()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKEVIRTUAL Test.getLazyVal ()Ljava/lang/String;
    ASTORE 1
   L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.print (Ljava/lang/Object;)V
   L2
   L3
    LINENUMBER 8 L3
    RETURN
   L4
    LOCALVARIABLE this LTest; L0 L4 0
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 2 L1
    ALOAD 0
    GETSTATIC Test$lazyVal$2.INSTANCE : LTest$lazyVal$2;
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    PUTFIELD Test.lazyVal$delegate : Lkotlin/Lazy;
    RETURN
   L2
    LOCALVARIABLE this LTest; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1018
  final static synthetic [Lkotlin/reflect/KProperty; $$delegatedProperties

  // access flags 0x8
  static <clinit>()V
    ICONST_1
    ANEWARRAY kotlin/reflect/KProperty
    DUP
    ICONST_0
    NEW kotlin/jvm/internal/PropertyReference1Impl
    DUP
    LDC LTest;.class
    INVOKESTATIC kotlin/jvm/internal/Reflection.getOrCreateKotlinClass (Ljava/lang/Class;)Lkotlin/reflect/KClass;
    LDC "lazyVal"
    LDC "getLazyVal()Ljava/lang/String;"
    INVOKESPECIAL kotlin/jvm/internal/PropertyReference1Impl.<init> (Lkotlin/reflect/KDeclarationContainer;Ljava/lang/String;Ljava/lang/String;)V
    INVOKESTATIC kotlin/jvm/internal/Reflection.property1 (Lkotlin/jvm/internal/PropertyReference1;)Lkotlin/reflect/KProperty1;
    CHECKCAST kotlin/reflect/KProperty
    AASTORE
    PUTSTATIC Test.$$delegatedProperties : [Lkotlin/reflect/KProperty;
    RETURN
    MAXSTACK = 8
    MAXLOCALS = 0
}


// ================Test$lazyVal$2.class =================
// class version 50.0 (50)
// access flags 0x30
// signature Lkotlin/jvm/internal/Lambda;Lkotlin/jvm/functions/Function0<Ljava/lang/String;>;
// declaration: Test$lazyVal$2 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0<java.lang.String>
final class Test$lazyVal$2 extends kotlin/jvm/internal/Lambda  implements kotlin/jvm/functions/Function0  {


  // access flags 0x1041
  public synthetic bridge invoke()Ljava/lang/Object;
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKEVIRTUAL Test$lazyVal$2.invoke ()Ljava/lang/String;
    ARETURN
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x11
  public final invoke()Ljava/lang/String;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    LINENUMBER 3 L0
    LDC "hello"
   L1
    ARETURN
   L2
    LOCALVARIABLE this LTest$lazyVal$2; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x0
  <init>()V
    ALOAD 0
    ICONST_0
    INVOKESPECIAL kotlin/jvm/internal/Lambda.<init> (I)V
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x19
  public final static LTest$lazyVal$2; INSTANCE

  // access flags 0x8
  static <clinit>()V
    NEW Test$lazyVal$2
    DUP
    INVOKESPECIAL Test$lazyVal$2.<init> ()V
    PUTSTATIC Test$lazyVal$2.INSTANCE : LTest$lazyVal$2;
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0
  OUTERCLASS Test <init> ()V
  // access flags 0x18
  final static INNERCLASS Test$lazyVal$2 null null
  // compiled from: Main.kt
}

万万没有想到,一段简单的代码,kotlin竟然在背后为我们做了这么多工作!这里照例分解总结如下:

  1. kotlin编译器会为委托属性单独生成一个类,该类的命名规则是外部类+"$"+属性名+"$2"(本例中为Test$lazyVal$2),该类继承了Lambda类并实现了Function0接口,如下所示:
final class Test$lazyVal$2 extends kotlin/jvm/internal/Lambda  implements kotlin/jvm/functions/Function0  

这里为什么要实现Function接口呢?先继续往下看,看完自然就明白了。

  1. 在Test$lazyVal$2类中,会暴露出了一个单例对象INSTANCE,这个对象被外部类(即Test)所使用,如下所示:
 // access flags 0x19
  public final static LTest$lazyVal$2; INSTANCE//单例对象
  // access flags 0x8
  static <clinit>()V//在类构造方法中,完成了INSTANCE的初始化
    NEW Test$lazyVal$2
    DUP
    INVOKESPECIAL Test$lazyVal$2.<init> ()V
    PUTSTATIC Test$lazyVal$2.INSTANCE : LTest$lazyVal$2;
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0

上面基本就是委托属性生成类所提供的功能,接下来从Test类中的test方法的调用处来分析上面功能是如何被使用的。

1.在test方法对应的字节码中,重点是其调用了getLazyVal 方法,getLazyVal方法会完成对属性值的获取,如下所示:

//test方法对应的字节码,这里只摘录重要部分,print对应的字节码暂时省略
  public final test()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKEVIRTUAL Test.getLazyVal ()Ljava/lang/String;
    ASTORE 1

//getLazyVal对应的字节码实现如下:
  public final getLazyVal()Ljava/lang/String;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    ALOAD 0
    GETFIELD Test.lazyVal$delegate : Lkotlin/Lazy;//这里生成了一个lazy代理对象
    ASTORE 1//将Test.lazyVal$delegate暂时存储到局部变量1中
    ALOAD 0
    ASTORE 2
    GETSTATIC Test.$$delegatedProperties : [Lkotlin/reflect/KProperty;//这里生成了KProperty类型属性对象
    ICONST_0
    AALOAD
    ASTORE 3
   L1
    ALOAD 1//这里实际上是    ASTORE 1
保存的对象,即就是Test.lazyVal$delegate对象
    INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object;//重点是这里,调用了就是ALOAD 1指令取出来的Test.lazyVal$delegate的getValue方法
   L2
    CHECKCAST java/lang/String
    ARETURN
   L3
    LOCALVARIABLE this LTest; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 4
  1. getLazyVal最终调用了getValue这个方法,上面字节码中已经在注释中说明了getValue的来源,即来自于Test.lazyVal$delegate这个对象,那么这个对象是什么时候生成的?其实就是在Test类中的实例构造方法中生成的,如下所示:
//Test类的实例构造方法
  public <init>()V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 2 L1
    ALOAD 0
//注意这里,终于调用了文章前面提示的Test$lazyVal$2.INSTANCE实例,即kotlin为我们生成的Test$lazyVal$2实例
    GETSTATIC Test$lazyVal$2.INSTANCE : LTest$lazyVal$2;
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
//下面语句完成了对Test.lazyVal$delegate对象的赋值,其值就是Test$lazyVal$2.INSTANCE
    PUTFIELD Test.lazyVal$delegate : Lkotlin/Lazy;
    RETURN
   L2
    LOCALVARIABLE this LTest; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

上面构造方法中有一句静态方法的调用,摘录如下:

INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;

从字节码可以看出这里调用了LazyKt类的静态方法lazy,并且传入了Function0类型的入参。这个入参正是Test$lazyVal$2.INSTANCE,这也是为什么Test$lazyVal$2要实现 kotlin/jvm/functions/Function0接口的原因。

  1. kotlin包kotlin.jvm.functions下的所有Function接口都有一个invoke操作符,当方法被调用的时候实际上就是通过执行该操作符来完成的,这里就涉及到了kotlin中的高阶函数的原理,会有相关文章展开阐述。只需要知道,代码最终会通过调用Test$lazyVal$2类中的 public final invoke()Ljava/lang/String方法来完成lazyVal的赋值即可。

从上述分析可知,lazy实现的主要流程就是,为委托属性生成了一个类,并且提供了该类的一个单例对象,当我们使用到该委托属性的时候,就会通过该单例对象完成属性的计算赋值。

那么lazy是如何保证属性值只计算一次呢?从字节码中可以看出,获取属性值最终都会通过getValue获取,如下所示:

//位于Test类中的字节码
    INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object;

getValue的执行实际上是通过调用invoke来实现的,invoke方法的字节码我们能够看到,但是却依然无法看出调用该方法之前的逻辑是什么。至少在我们这个例子中,字节码层面上没有体现出为什么lazy属性计算只初始化一次!所以通过看字节码显然是得不到答案了,那么是不是就没有方法了?

当然不是,从上面字节码可以看出,最终会调用Lazy类的getValue方法,所以只需要查看Lazy类的getValue方法实现即可。有朋友说getValue最后不是要调用invoke方法吗?为什么不直接看invoke方法的实现?这是因为,invoke方法的调用实际上表示的是其对应方法体的执行,getValue是完全有可能在invoke执行之前做些逻辑判断的,这有可能就是lazy属性没有多次计算的原因。

位于Lazy.kt文件中的getValue的源代码摘录如下:

@kotlin.internal.InlineOnly
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

可以看出,getValue最终获取的是value属性的值,所以我们需要继续找属性value的实现。

通过查找可知,value是定义在Lazy.kt文件中的Lazy接口中的val类型的属性成员,由于是位于接口中,所以需要找到其具体实现。实际上Lazy文件中针对不同环境(java版、js版等)有多个value的实现,最终我们找到了java版的value实现,位于私有类SafePublicationLazyImpl中,如下所示:

    override val value: T
        get() {
            if (_value === UNINITIALIZED_VALUE) {
                val initializerValue = initializer
                // if we see null in initializer here, it means that the value is already set by another thread
                if (initializerValue != null) {
                    val newValue = initializerValue()
                    if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) {
                        initializer = null
                    }
                }
            }
            @Suppress("UNCHECKED_CAST")
            return _value as T
        }

上面代码很显然,当value值为UNINITIALIZED_VALUE会进行第一次赋值,之后再获取时就会直接返回该值。第一次赋值实际上就是通过调用initializer完成了初始化,这个初始化实际上就是执行了lazy{"hello"}这个段代码(对应于字节码中的invoke方法)。所以如果我们在lazy中添加打印语句会发现,该打印语句仅仅会在属性第一次计算的时候执行。

至此,属性相关的原理阐述完毕。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值