美文网首页
如何写出高效Kotlin代码

如何写出高效Kotlin代码

作者: Jaking | 来源:发表于2019-07-12 15:10 被阅读0次

前言

Kotlin语法特性有哪些会造成额外的性能开销呢?我们在开发过程中如何去避开这些问题,写出高效Kotlin代码,这就是这篇文章的目的。

开发环境

Kotlin版本:1.3.20
平台:在JVM / Android上的实现
分析工具:Kotlin字节码分析工具

一、inline关键字

class FunctionTest {

    private fun upload(filePath: String, onSuccess: () -> Unit) {
        print("start")
        onSuccess()
    }

    @Test
    fun testUpload() {
        val filePath = "/test.mp4"
        upload(filePath) {
            print("success")
        }
    }
}

转化成同等的java代码

public final class FunctionTest {
   private final void upload(String filePath, Function0 onSuccess) {
      String var3 = "start";
      System.out.print(var3);
      onSuccess.invoke();
   }

   @Test
   public final void testUpload() {
      String filePath = "/test.mp4";
      this.upload(filePath, (Function0)null.INSTANCE);
   }
}

可以看到我们使用高阶函数时,实际上会把lambda块封装成Function0对象,最后invoke()才调用结束,多出了运行时的损耗。通过对高阶函数添加inline,在编译时会把高函数复制到调用的地方,减少了运行时的开销。

class FunctionTest {

    private inline fun upload(filePath: String, onSuccess: () -> Unit) {
        print("start")
        onSuccess()
    }

    @Test
    fun testUpload() {
        val filePath = "/test.mp4"
        upload(filePath) {
            print("success")
        }
    }
}

同等的java代码,使用inline效果如下。

public final class FunctionTest {
//编译时upload()的函数体会被替换到调用的地方
   private final void upload(String filePath, Function0 onSuccess) {
      System.out.print("start");
      onSuccess.invoke();
   }

   @Test
   public final void testUpload() {
      String filePath = "/test.mp4";
      //来自upload()函数体
      System.out.print("start");
      System.out.print("success");
   }
}

注意:

  1. 如果高阶函数lambda参数是变量时,即使声明inline的函数也会失效,如下
class FunctionTest {

    private inline fun upload(filePath: String, onSuccess: () -> Unit) {
        print("start")
        onSuccess()
    }

    @Test
    fun testUpload() {
        val filePath = "/test.mp4"
        val a = { print("success") }
        upload(filePath, a)
    }
}

同等java代码

public final class FunctionTest {
   private final void upload(String filePath, Function0 onSuccess) {
      System.out.print("start");
      onSuccess.invoke();
   }

   @Test
   public final void testUpload() {
      String filePath = "/test.mp4";
      Function0 a = (Function0)null.INSTANCE;
      //替换了部分,还是需要调用invoke()
      System.out.print("start");
      a.invoke();
   }
}
  1. inline函数会中的lambda块内的return被复制过去,从而导致整个函数的返回。应该使用return@高阶函数名,指明是在lambda块内返回。
class FunctionTest {

    private inline fun upload(filePath: String, onSuccess: () -> Unit) {
        print("start")
        onSuccess()
    }

    @Test
    fun testUpload() {
        val filePath = "/test.mp4"
        upload(filePath) {
            print("success")
            // return会导致整个外部函数的返回,应该使用return@高阶函数名,指明是在lambda块内返回。
            return@upload
        }
        print("end")
    }
}

二、Foreach性能开销

fun forEach() {
        (1..10).forEach {
            print(it)
        }
 }

同等java代码

public final void forEach() {
      byte var1 = 1;
      Iterable $receiver$iv = (Iterable)(new IntRange(var1, 10));
      Iterator var2 = $receiver$iv.iterator();

      while(var2.hasNext()) {
         int element$iv = ((IntIterator)var2).nextInt();
         int var5 = false;
         System.out.print(element$iv);
      }

   }

从上面代码可以看到,创建 IntRange 对象外,还有IntIterator 的开销。所以对于一个范围使用一个简单的 for 循环,而不是forEach,来减少迭代器的开销。

三、集合链式操作使用Sqeuence来提高集合处理速度

对一个User集合先做map操作最后做filter操作,代码如下

class SqeuenceTest {

    fun test() {
        val list = listOf(1, 2, 3, 4, 5, 6)
        list.map{ it * 2 }.filter { it % 3  == 0 }.average()
    }

}

程序处理过程:users调用map会产生中间List<String>集合,接着调用filter,产生中间List<String>集合,最后调用average来产生我们需要的结果List<String>集合,如图:


image.png

通过将集合转化成Sequence后,能够解决产生中间集合问题。
优化后代码如下

class SqeuenceTest {

    fun test() {
        val list = listOf(1, 2, 3, 4, 5, 6)
        list.asSequence().map{ it * 2 }.filter { it % 3  == 0 }.average()
    }

}

Sequence过程:Sequence先对单个元素进行一系列的整体操作(简单理解为合并map和filter操作),然后再对下一个元素做进行一系列的整体操作,直到处理完集合中所有元素为止,最后获取Sequence<T>结果集合。


image.png

结论:对于集合链式操作时,我们需要把集合先转化成Sequence,再进行操作,从而减少中间集合和循环次数,最终提高集合处理速度。

四、const关键字

伴生对象通过在类中使用companion object来创建,用来替代静态成员,类似于Java中的静态内部类。所以在伴生对象中声明常量是很常见的做法,但如果写法不对,可能就会产生额外开销。比如下面这段声明TAG常量的代码:

class CompanionTest {
    companion object {
        private const val TAG = "CompanionTest"
        private val tag = "CompanionTest"
    }

    @Test
    fun print() {
        print(TAG)
        print(tag)
    }
}

将这段Kotlin代码转化成等同的Java代码后

public final class CompanionTest {
   private static final String TAG = "CompanionTest";
   private static final String tag = "CompanionTest";
   public static final CompanionTest.Companion Companion = new CompanionTest.Companion((DefaultConstructorMarker)null);

   @Test
   public final void print() {
      String var1 = "CompanionTest";
      System.out.print(var1);
      var1 = tag;
      System.out.print(var1);
   }
   public static final class Companion {
      private Companion() {
      }
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

从上面代码可以看到伴生对象常量使用const,在编译时会替换成常量值,不需要进行赋值。

五、lazy()

lazy()委托属性可以用于只读属性的惰性加载,但我们经常忽略lazy的可选的model参数,lazy(mode)的源码如下:

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = 
SynchronizedLazyImpl(initializer)

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

我们先了解三种mode的作用:

  • LazyThreadSafetyMode.SYNCHRONIZED:初始化属性时会有双重锁检查,保证该值只在一个线程中计算,并且所有线程会得到相同的值。
  • LazyThreadSafetyMode.PUBLICATION:多个线程会同时执行,初始化属性的函数会被多次调用,但是只有第一个返回的值被当做委托属性的值。
  • LazyThreadSafetyMode.NONE:没有双重锁检查,不应该用在多线程下。

从源码中我们可以看到lazy()默认的是LazyThreadSafetyMode.SYNCHRONIZED,在不需要线程安全的场景下,会造成不必要的线程安全的开销,比如Android中预知只在主线程,我们可以指定LazyThreadSafetyMode.NONE,来避免不必要的损耗。

六、基本类型数组

class BasicTypeArray {
    val intArray = intArrayOf(1)
    val array = arrayOf(1)
    val nullArray = arrayOf<Int?>(null)
}

将这段Kotlin代码转化成等同的Java代码后

public final class BasicTypeArray {
   @NotNull
   private final int[] intArray = new int[]{1};
   @NotNull
   private final Integer[] array = new Integer[]{1};
   @NotNull
   private final Integer[] nullArray = new Integer[]{(Integer)null};
}

我们可以看到后面两种方式对基本类型做了装箱处理,产生了额外的开销。
所以我们使用非空的数组时,应该使用XXXArray,避免自动装箱。

参考文章:
https://tech.meituan.com/2018/07/05/kotlin-code-inspect.html
https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62
https://juejin.im/post/5b28f4946fb9a00e3a5a9b8c

相关文章

网友评论

      本文标题:如何写出高效Kotlin代码

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