Kotlin标准库函数源码浅析

作者: 飘飘然的影子 | 来源:发表于2020-08-12 16:54 被阅读0次

    Kotlin标准库函数

    Kotlin提供了一个标准函数库,例如run, with, let, also, apply等函数,开发中使用十分方便。

    我们可以通过源码来观察学习这些函数

    先来看看run函数

    @kotlin.internal.InlineOnly
    public inline fun <T, R> T.run(block: T.() -> R): R {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        return block()
    }
    

    run 函数使用泛型通配符 T 和 R ,对 T 进行方法扩展,传入一个 block 函数 ,返回 R 。

    逐步分析代码

     contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
    

    contract是Kotlin1.3的新特性,目前还是处于实验性阶段,即API在稳定版之前可能会发生变动。

    它叫做Kotlin的契约,是面向编译器的,为编译器提供稳定的方法行为,告诉编译器我要做什么。

    Kotlin 编译器会做大量的静态分析工作,以提供警告并减少模版代码。其中最显著的特性之一就是智能转换——能够根据类型检测自动转换类型。

    fun foo(s: String?) {
        if (s != null) s.length // 编译器自动将“s”转换为“String”,而不是String?
    }
    

    然而,一旦将这些检测提取到单独的函数中,所有智能转换都立即消失了:

    fun String?.isNotNull(): Boolean = this != null
    
    fun foo(s: String?) {
        if (s.isNotNull()) s.length // 没有智能转换,这行代码会编译不过
                                    //因为s不确定是不是String
    }
    

    所以为了改善在此类场景中的行为,Kotlin 引入了契约这个概念。

    run函数里面的契约代码的含义是指会在这里调用 block 函数,而且只调用一次。

    contract有多个描述符:

    • returns(): 描述函数正常返回(无返回值)但没有抛出任何异常的情况
    • returns(value: Any?): 描述函数以指定的return [value]正常返回的情况
    • returnsNotNull(): 描述函数正常返回任何非“null”值的情况
    • callsInPlace: 用于在适当的位置调用函数参数[lambda],而且可以指定调用次数

    函数调用次数也有相关参数:

    InvocationKind
    • AT_MOST_ONCE: 函数参数将被调用一次或根本不被调用
    • AT_LEAST_ONCE: 函数参数将被调用一次或多次
    • EXACTLY_ONCE: 函数参数将被调用一次
    • UNKNOWN: 函数参数就地调用,但不知道可以调用多少次

    我们也可以通过 contract 进行自定义契约,可以为自己的函数声明契约

    通过调用标准库(stdlib)函数 contract 来引入自定义契约,该函数提供了 DSL 作用域:

    fun String?.isNullOrEmpty(): Boolean {
        contract {
            returns(false) implies (this@isNullOrEmpty != null)
        }
        return this == null || isEmpty()
    }
    

    了解了Kotlin的契约之后,我们再来看看一开始的run函数

    @kotlin.internal.InlineOnly
    public inline fun <T, R> T.run(block: T.() -> R): R {
       //契约代码,不影响业务逻辑
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        return block()
    }
    

    我们跳过契约代码,可以发现run函数其实就是返回了 block 函数,即是返回了 block 函数的返回值。

    看一下示例代码

        var  text =  activityMainBinding.run {
                this.rvContent.invalidate()
                this.tvTitle.text = "abc"
                this.tvTitle.text
            }
    

    因为 block 本身就是 T.() 扩展函数,所以可以拿到 T 的对象

    let 函数

    @kotlin.internal.InlineOnly
    public inline fun <T, R> T.let(block: (T) -> R): R {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        return block(this)
    }
    

    可以看见let函数跟run函数不一样的地方在于,block函数是传入了调用者 T 的对象

    看一下示例代码

          var test =  activityMainBinding.let {
                it.rvContent.invalidate()
                it.tvTitle.text = "abc"
                it.tvTitle.text
            }
    

    这个 it 则是 block 函数传入的 T 对象,it 只是个别名,是可以自己修改的

    apply函数

    @kotlin.internal.InlineOnly
    public inline fun <T> T.apply(block: T.() -> Unit): T {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        block()
        return this
    }
    

    apply 函数是调用了一个没有返回值的 block 函数,而且返回了当前对象。

    看一下示例代码

        activityMainBinding.apply {
                this.rvContent.invalidate()
                this.tvTitle.text = "abc"
            }.tvTitle.text = "abc"
    

    also函数

    @kotlin.internal.InlineOnly
    @SinceKotlin("1.1")
    public inline fun <T> T.also(block: (T) -> Unit): T {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        block(this)
        return this
    }
    

    also函数跟apply函数类似,唯一不同的是传入的 block 函数是带有调用对象的。

    看一下示例代码

      activityMainBinding.also {
               it.rvContent.invalidate()
               it.tvTitle.text = "abc"
            }.tvTitle.text = "abc"
    

    it 同样也是一个别名

    with函数

    @kotlin.internal.InlineOnly
    public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        return receiver.block()
    }
    

    可以看见with函数不是扩展函数,所以它会接收一个调用者的对象和 block 函数,实质是用这个对象调用 block 函数,有点像代理模式。

    inline关键字

    我们会发现,上面的方法前面都有个关键字 inline ,它属于 Kotlin 的高级特性——内联函数。

    调用一个方法其实就是一个方法压栈和出栈的过程,调用方法时将栈帧压入方法栈,然后执行方法体,方法结束时将栈帧出栈,这个压栈和出栈的过程是一个耗费资源的过程,而且如果调用过多次,方法栈空间被耗尽,没有足够资源分配给新创建的栈帧,就会抛出 java.lang.StackOverflowError 错误。

    为了避免遇到这种情况,所以 Kotlin 提出了内联函数的使用。

    内联函数是指被inline标记的函数,其原理就是:在编译时期,把调用这个函数的地方用这个函数的方法体进行替换。

    举个例子:

    我们先定义了一个普通方法

        fun show() {
            val a = "shadow:"
            val b = "hhhh"
            println(a + b)
        }
    

    然后在 Activity onCreate()调用

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val activityMainBinding =                             ActivityMainBinding.inflate(LayoutInflater.from(this))
            setContentView(activityMainBinding.root)
            show()
        }
    

    通过 Kotlin 编译成字节码,再反编译,看看其.java文件

      protected void onCreate(@Nullable Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          ActivityMainBinding activityMainBinding = ActivityMainBinding.inflate(LayoutInflater.from((Context)this));
          Intrinsics.checkExpressionValueIsNotNull(activityMainBinding, "activityMainBinding");
          this.setContentView(activityMainBinding.getRoot());
          this.show();
       }
    
       public final void show() {
          String a = "shadow:";
          String b = "hhhh";
          String var3 = a + b;
          boolean var4 = false;
          System.out.println(var3);
       }
    

    发现就是正常调用了 show()

    那我们再来看看加了关键字 inline 的 show()

    inline fun show() {
            val a = "shadow:"
            val b = "hhhh"
            println(a + b)
        }
    

    进行反编译

       protected void onCreate(@Nullable Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          ActivityMainBinding activityMainBinding = ActivityMainBinding.inflate(LayoutInflater.from((Context)this));
          Intrinsics.checkExpressionValueIsNotNull(activityMainBinding, "activityMainBinding");
          this.setContentView(activityMainBinding.getRoot());
          int $i$f$show = false;
          String a$iv = "shadow:";
          String b$iv = "hhhh";
          String var7 = a$iv + b$iv;
          boolean var8 = false;
          System.out.println(var7);
       }
    
       public final void show() {
          int $i$f$show = 0;
          String a = "shadow:";
          String b = "hhhh";
          String var4 = a + b;
          boolean var5 = false;
          System.out.println(var4);
       }
    

    可以发现在编译时期就会把方法内容替换到调用该方法的地方,这样就会减少方法压栈,出栈,进而减少资源消耗。

    这就是内联函数的作用,inline 关键字实际上增加了代码量,但是提升了性能,而且增加的代码量是在编译期执行的,对程序可读性不会造成影响。

    相关文章

      网友评论

        本文标题:Kotlin标准库函数源码浅析

        本文链接:https://www.haomeiwen.com/subject/gddvrktx.html