浅析一下:kotlin委托背后的实现机制

6,818 阅读2分钟

大家好,kotlin的属性委托、类委托、lazy等委托在日常的开发中,给我们提供了很大的帮助,我之前的文章也是有实战过几种委托。不过对比委托实现的背后机制一直都没有分析过,所以本篇文章主要是带领大家分析下委托的实现原理,加深对kotlin的理解。

一. lazy委托

这里我们不说用法,直接说背后的实现原理。

先看一段代码:

val content: String by lazy {
    "oiuytrewq"
}

fun main() {
    println(content.length)
}

我们看下反编译后的java代码:

  1. 首先会通过DelegateDemoKt静态代码块饿汉式的方式创建一个Lazy类型的变量content$delegate,命名的规则即代码中定义的原始变量值拼接上$delegate,我们原始定义的content变量就会从属性定义上消失,但会生成对应的get方法,即getContent()
  1. 当我们在main方法中调用content.length时,其实就是调用getContent().length(),而getContent()最终是调用了content$delegate.getValue方法;
  1. 这个lazy类型的变量是调用了LazyKt.lazy()方法创建,而真正的核心逻辑——该方法具体参数的传入,在反编译的java代码中并没有体现;

java代码既然看不到,我们退一步看下字节码:

上面是DelegateDemoKt类构造器对应的字节码,其中就是获取了DelegateDemoKt$content$2作为参数传入了LazyKt.lazy()方法。

我们看下DelegateDemoKt$content$2类的实现字节码:

DelegateDemoKt$content$2类实现了Function0接口,所以上面lazy的真正实现逻辑就是DelegateDemoKt$content$2类的invoke方法中,上图的字节码红框圈出的地方就很直观的看出来了。

二. 属性委托

属性委托的委托类就是指实现了ReadWritePropertyReadOnlyProperty接口的类,像官方提供的Delegates.observable()Delegates.vetoable()这两个api也是借助前面两个接口实现的。这里我们就以支持读写的ReadWriteProperty委托接口进行举例分析。

先看一段例子代码:

var age: Int by object : ReadWriteProperty<Any?, Int> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return 10
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        val v = value * value
        println("setValue: $v")
    }
}

fun main() {
    age = 4
    println(age)
}

我们看下反编译的java代码:

public final class DelegateDemoKt {
   // $FF: synthetic field
   static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.mutableProperty0(new MutablePropertyReference0Impl(DelegateDemoKt.class, "age", "getAge()I", 1))};
   @NotNull
   private static final <undefinedtype> age$delegate = new ReadWriteProperty() {
      @NotNull
      public Integer getValue(@Nullable Object thisRef, @NotNull KProperty property) {
         Intrinsics.checkNotNullParameter(property, "property");
         return 10;
      }

      public void setValue(@Nullable Object thisRef, @NotNull KProperty property, int value) {
         Intrinsics.checkNotNullParameter(property, "property");
         int v = value * value;
         String var5 = "setValue: " + v;
         System.out.println(var5);
      }
   };

   public static final int getAge() {
      return age$delegate.getValue((Object)null, $$delegatedProperties[0]);
   }

   public static final void setAge(int var0) {
      age$delegate.setValue((Object)null, $$delegatedProperties[0], var0);
   }

   public static final void main() {
      setAge(4);
      int var0 = getAge();
      System.out.println(var0);
   }
}
  1. 和lazy有些类似,会生成一个实现了ReadWriteProperty接口的匿名类变量age$delegate,命名规则和lazy相同,通过还帮助我们生成了对应的getAgesetAge方法;
  1. 当我们在代码中执行age = 4就会调用setAge(4)方法,最终会调用age$delegate.setValue()方法;类似的调用age就会调用getAge(),最终调用到age$delegate.getValue()方法;
  1. 编译器还通过反射帮助我们生成了一个KProperty类型的$$delegatedProperties变量,主要是ReadWritePropertysetValuegetValue方法都需要传入这样一个类型的对象,通过$$delegatedProperties变量我们可以访问到具体的变量名等信息;

类似的还有一种属性委托,我们看下代码:

val map = mutableMapOf<String, Int>()

val name: Int by map

上面代码的意思是:当访问name时,就会从map这个散列表中获取key为"name"的value值并返回,不存在就直接抛异常,接下来我们看下反编译后的java代码:

public final class DelegateDemoKt {
   // $FF: synthetic field
   static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.property0(new PropertyReference0Impl(DelegateDemoKt.class, "name", "getName()I", 1))};
   @NotNull
   private static final Map map = (Map)(new LinkedHashMap());
   @NotNull
   private static final Map name$delegate;

   static {
      name$delegate = map;
   }

   public static final int getName() {
      Map var0 = name$delegate;
      Object var1 = null;
      KProperty var2 = $$delegatedProperties[0];
      return ((Number)MapsKt.getOrImplicitDefaultNullable(var0, var2.getName())).intValue();
   }
}
  1. 生成一个Map类型的name$delegate变量,这个变量其实就是我们定义的map散列表;
  1. 通过反射生成了一个KProperty类型对象变量$$delegatedProperties,通过这个对象的getName()我们就能拿到变量名称,比如这里的"name"变量名;
  1. 最终调用了MapsKt.getOrImplicitDefaultNullable方法,去map散列表去查找"name"这个key对应的value;

PS:记得kotlin1.6还是1.7的插件版本对应委托进行了优化,这个后续的文章会再进行讲解。

三. 类委托

类委托实现就比较简单了,这里我们看下样例代码:

fun interface Fruit {
    fun type(): Int
}

class FruitProxy(private val model: Fruit) : Fruit by model

fun main() {
    val proxy: FruitProxy = FruitProxy {
        -1
    }
    println(proxy.type())
}

反编译成java代码看下:

首先我们看下FruitProxy这个类,其实现了Fruit接口,借助属性委托特性,编译器会自动帮助我们生成type() 接口方法的实现,并再其中调用构造方法传入的委托类对象modeltype()方法,类委托的核心逻辑就这些。

再main()方法中构造FruitProxy时,我们也无法知晓具体的构造参数对象是啥,和上面的lazy一样,我们看下字节码:

其实FruitProxy方法就传入了一个DelegateDemoKt$main$proxy$1类型的对象,并实现了Fruit接口重写了type方法。

总结

本篇文章主要是讲解了三种委托背后的实现原理,有时候反编译字节码看不出来原理的,可以从字节码中寻找答案,希望本篇文章能对你有所帮助。

历史文章

这里是我整理的过往kotlin特性介绍的历史文章,大家感兴趣可以阅读下:

Kotlin1.9.0-Beta,它来了!!

聊聊Kotlin1.7.0版本提供的一些特性

聊聊kotlin1.5和1.6版本提供的一些新特性

kotlin密封sealed class/interface的迭代之旅

优化@BuilderInference注解,Kotlin高版本下了这些“毒手”!

@JvmDefaultWithCompatibility优化小技巧,了解一下~