Kotlin使用优化(二)

作者: 小小小小小粽子 | 来源:发表于2019-03-24 21:47 被阅读34次

上一节我们聊到Kotlin集合,以及它提供给我们的一些集合的扩展方法,我们主要探讨了filter跟map方法的实现,开销以及优化。那除了集合之外,Kotlin提供的lambda跟closure也很强大,今天我们来看看它们的使用对性能有什么影响。

首先来看一个lambda的例子:

fun main(args: Array<String>) {
    val write = {
  val ints = listOf(1, 2, 3, 4, 5)
        File("somefile.txt")
            .writeText(ints.joinToString("\n"))
    }
  invokeBlock(write)
    invokeBlock(write)
}

fun invokeBlock(body: () -> Unit) {
    try {
        body()
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

write是一个lambda,我们知道在Kotlin里函数虽然是一级公民,但是编译器在编译时会把它是现成一个Function对象。我们来看看反编译成Java代码什么样子:

public final void main(@NotNull String[] args) {
   Intrinsics.checkParameterIsNotNull(args, "args");
  Function0 write = (Function0)null.INSTANCE;
 this.invokeBlock(write);
 this.invokeBlock(write); }

两次调用方法居然只生成了一个对象,神奇不神奇?于是我深入到字节码里面看到:

 // access flags 0x19
  public final static LMain$main$write$1; INSTANCE

  // access flags 0x8
  static <clinit>()V
    NEW Main$main$write$1
    DUP
    INVOKESPECIAL Main$main$write$1.<init> ()V
    PUTSTATIC Main$main$write$1.INSTANCE : LMain$main$write$1;
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0

在字节码里每个实现了Function接口的类都有一个静态字段保存了实例,提供了实例复用的能力,也就是我们所说的单例!一切编译器都给我们做好了。

再看一个例子:

fun test() {
    val literals = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
    var total = 0
  literals.forEach {
  total += it
 } }

又是熟悉的集合操作,但是写完之后我总觉得哪里不对劲,依稀记得当初学Java8的lambda的时候,这么写编译器报错了困扰了好久,但是时间太久我不敢确定,于是赶紧用java写了一遍:

Error:(19, 49) java: local variables referenced from a lambda expression must be final or effectively final                                                                     

果然,大脑给我的反馈还是对的,那为什么都是lambda行为却差这么多呢?

我们知道在Java里lambda跟匿名内部类在某种意义上是等价的,而Java里他们可以可以传递给其他线程,这就导致了Java如果允许lambda操作本地变量,可能会有一个并发问题,所以编译器不允许我们对局部变量进行修改。那Kotlin是怎么做到的呢?

我们看一下上面的方法反编译成Java代码:

public final void test() {
   List literals = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9});
 int total = 0;
  Iterable $receiver$iv = (Iterable)literals;   int it;
 for(Iterator var4 = $receiver$iv.iterator(); var4.hasNext(); total += it) {
      Object element$iv = var4.next();
  it = ((Number)element$iv).intValue();
 int var7 = false;
  }

}

原来,编译器给优化成了循环。编译器还真是为了方便我们默默干了不少事啊!

好了,不扯题外话了,我们回到主题,除了lambda之外,Kotlin还有closure的概念,closure呢,其实就是一种访问了定义在函数外部的变量的函数,我们上面的lambda就可以说是一种closure。再确切一点:

val inc = {        counter ++    }

我们这就定义了一个closure,我们可以把它当方法调用:

fun main(args: Array<String>) {
    var counter = 0   
    val inc = { 
      counter ++
    }   
    inc()
}

这里跟上面描述的情况有些类似,操作了外部定义的局部变量,但是有少许不同,在这里closure是被当成方法调用的,让我们来看看反编译后的Java代码:

public final void main(@NotNull String[] args) {
   Intrinsics.checkParameterIsNotNull(args, "args");
 final IntRef counter = new IntRef();
  counter.element = 0;
  Function0 inc = (Function0)(new Function0() {
      // $FF: synthetic method
 // $FF: bridge method  public Object invoke() {
         return this.invoke();
  }

      public final int invoke() {
         IntRef var10000 = counter;
 int var1;
  var10000.element = (var1 = var10000.element) + 1;
 return var1;
  }
   });
  inc.invoke(); }

出现了一个很有意思的东西IntRef,这里声明了一个final的IntRef对象,然后这个对象持有了实际会被修改的变量,我猜这也是编译器为了防止并发调用方法所做的措施,这里就不继续深究了。

我们来看一下生成的字节码:

 // access flags 0x0
  <init>(Lkotlin/jvm/internal/Ref$IntRef;)V
    ALOAD 0
    ALOAD 1
    PUTFIELD Main$main$inc$1.$counter : Lkotlin/jvm/internal/Ref$IntRef;
    ALOAD 0
    ICONST_0
    INVOKESPECIAL kotlin/jvm/internal/Lambda.<init> (I)V
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1010
  final synthetic Lkotlin/jvm/internal/Ref$IntRef; $counter

这个类生成了一个构造器,需要传入一个IntRef对象,类里面有个final字段引用这个对象,这导致了我们每次使用这些修改外部变量的lambda时,总会创建新的对象,编译器不会给我们缓存创建的实例,这对性能有不小的影响,建议大家在平时写代码时尽量避免这样使用lambda。

好了,这次更多的是lambda跟closure使用时的一些特性跟注意事项,倒没有涉及多少优化的层面,下回,我们来聊聊Kotlin属性访问可能存在的优化点。

快来关注我吧!

相关文章

网友评论

    本文标题:Kotlin使用优化(二)

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