美文网首页
如何写出高效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