影响性能的 Kotlin 代码(一)

茗玉
• 阅读 1337

Kotlin 高级函数的特性不仅让代码可读性更强,更加简洁,而且还提高了生产效率,但是简洁的背后是有代价的,隐藏着不能被忽视的成本,特别是在低端机上,这种成本会被放大,因此我们需要去研究 kotlin 语法糖背后的魔法,选择合适的语法糖,尽量避免这些坑。

Lambda 表达式

Lambda 表达式语法简洁,避免了冗长的函数声明,代码如下。

fun requestData(type: Int, call: (code: Int, type: Int) -> Unit) {
    call(200, type)
}

Lambda 表达式语法虽然简洁,但是隐藏着两个性能问题。

  • 每次调用 Lambda 表达式,都会创建一个对象

影响性能的 Kotlin 代码(一)

图中标记 1 所示的地方,涉及一个字节码类型的知识点。

标识符含义
I基本类型 int
L对象类型,以分号结尾,如 Lkotlin/jvm/functions/Function2;

Lambda 表达式 call: (code: Int, type: Int) -> Unit 作为函数参数,传递到函数中,Lambda 表达式会继承 kotlin/jvm/functions/Function2 , 每次调用都会创建一个 Function2 对象,如图中标记 2 所示的地方。

  • Lambda 表达式隐含自动装箱和拆箱过程

影响性能的 Kotlin 代码(一)

正如你所见 lambda 表达式存在装箱和拆箱的开销,会将 int 转成 Integer,之后进行一系列操作,最后会将 Integer 转成 int

如果想要避免 Lambda 表达式函数对象的创建及装箱拆箱开销,可以使用 inline 内联函数,直接执行 lambda 表达式函数体。

Inline 修饰符

Inline 内联函数的作用:提升运行效率,调用被 inline 修饰符标记的函数,会把函数内的代码放到调用的地方。

如果阅读过 Koin 源码的朋友,应该会发现 inline 都是和 lambda 表达式和 reified 修饰符配套在一起使用的,如果只使用 inline 修饰符标记普通函数,Android Studio 也会给一个大大大的警告。

影响性能的 Kotlin 代码(一)

编译器建议我们在含有 lambda 表达式作为形参的函数中使用内联,既然 Inline 修饰符可以提升运行效率,为什么编译器会给我们一个警告? 这是为了防止 inline 操作符滥用而带来的性能损失。

inline 修饰符适用于以下情况

  • inline 修饰符适用于把函数作为另一个函数的参数,例如高阶函数 filter、map、joinToString 或者一些独立的函数 repeat
  • inline 操作符适合和 reified 操作符结合在一起使用
  • 如果函数体很短,使用 inline 操作符可以提高效率

Kotlin 遍历数组

这一小节主要介绍 Kotlin 数组,一起来看一下遍历数组都有几种方式。

  • 通过 forEach 遍历数组
  • 通过区间表达式遍历数组(..downTountil)
  • 通过 indices 遍历数组
  • 通过 withIndex 遍历数组

通过 forEach 遍历数组

先来看看通过 forEach 遍历数组,和其他的遍历数组的方式,有什么不同。

array.forEach { value ->

}

反编译后:

Integer[] var5 = array;
int var6 = array.length;
for(int var7 = 0; var7 < var6; ++var7) {
 Object element$iv = var5[var7];
 int value = ((Number)element$iv).intValue();
 boolean var10 = false;
}

正如你所见通过 forEach 遍历数组的方式,会创建额外的对象,并且存在装箱/拆箱开销,会占用更多的内存。

通过区间表达式遍历数组

在 Kotlin 中区间表达式有三种 ..downTountil

  • .. 关键字,表示左闭右闭区间
  • downTo 关键字,实现降序循环
  • until 关键字,表示左闭右开区间

.. 、downTo 、until

for (value in 0..size - 1) {
    // case 1
}

for (value in size downTo 0) {
    // case 2
}

for (value in 0 until  size) {
    // case 3
}

反编译后

// case 1 
if (value <= var4) {
 while(value != var4) {
    ++value;
 }
}

// case 2
for(boolean var5 = false; value >= 0; --value) {
}

// case 3
for(var4 = size; value < var4; ++value) {
}

如上所示 区间表达式 ( ..downTountil) 除了创建一些临时变量之外,不会创建额外的对象,但是区间表达式 和 step 关键字结合起来一起使用,就会存在内存问题。

区间表达式 和 step 关键字

step 操作的 ..downTountil, 编译之后如下所示。

for (value in 0..size - 1 step 2) {
    // case 1
}

for (value in 0 downTo size step 2) {
    // case 2
}

反编译后:

// case 1
var10000 = RangesKt.step((IntProgression)(new IntRange(var6, size - 1)), 2);
while(value != var4) {
    value += var5;
}

// case 2
 var10000 = RangesKt.step(RangesKt.downTo(0, size), 2);
 while(value != var4) {
    value += var5;
 }

step 操作的 ..downTountil 除了创建一些临时变量之外,还会创建 IntRangeIntProgression 对象,会占用更多的内存。

通过 indices 遍历数组

indices 通过索引的方式遍历数组,每次遍历的时候通过索引获取数组里面的元素,如下所示。

for (index in array.indices) {
}

反编译后:

for(int var4 = array.length; var3 < var4; ++var3) {
}

通过 indices 遍历数组, 编译之后的代码 ,除了创建了一些临时变量,并没有创建额外的对象。

通过 withIndex 遍历数组

withIndexindices 遍历数组的方式相似,通过 withIndex 遍历数组,不仅可以获取的数组索引,同时还可以获取到每一个元素。

for ((index, value) in array.withIndex()) {

}

反编译后:

Integer[] var5 = array;
int var6 = array.length;
for(int var3 = 0; var3 < var6; ++var3) {
 int value = var5[var3];
}

正如你所看到的,通过 withIndex 方式遍历数组,虽然不会创建额外的对象,但是存在装箱/拆箱的开销

总结:

  • 通过 forEach 遍历数组的方式,会创建额外的对象,占用内存,并且存在装箱 / 拆箱开销
  • 通过 indices 和区间表达式 ( ..downTountil) 都不会创建额外的对象
  • 区间表达式 和 step 关键字结合一起使用, 会有创建额外的对象的开销,占用更多的内存
  • 通过 withIndex 方式遍历数组,不会创建额外的对象,但是存在装箱/拆箱的开销

尽量少使用 toLowerCase 和 toUpperCase 方法

这一小节内容,在我之前的文章中分享过,但是这也是很多小伙伴,遇到最多的问题,所以单独拿出来在分析一次

当我们比较两个字符串,需要忽略大小写的时候,通常的写法是调用 toLowerCase() 方法或者 toUpperCase() 方法转换成大写或者小写,然后在进行比较,但是这样的话有一个不好的地方,每次调用 toLowerCase() 方法或者 toUpperCase() 方法会创建一个新的字符串,然后在进行比较。

调用 toLowerCase() 方法

fun main(args: Array<String>) {
//    use toLowerCase()
    val oldName = "Hi dHL"
    val newName = "hi Dhl"
    val result = oldName.toLowerCase() == newName.toLowerCase()

//    or use toUpperCase()
//    val result = oldName.toUpperCase() == newName.toUpperCase()
}

toLowerCase() 编译之后的 Java 代码

影响性能的 Kotlin 代码(一)

如上图所示首先会生成一个新的字符串,然后在进行字符串比较,那么 toUpperCase() 方法也是一样的如下图所示。

toUpperCase() 编译之后的 Java 代码

影响性能的 Kotlin 代码(一)

这里有一个更好的解决方案,使用 equals 方法来比较两个字符串,添加可选参数 ignoreCase 来忽略大小写,这样就不需要分配任何新的字符串来进行比较了。

fun main(args: Array<String>) {
    val oldName = "hi DHL"
    val newName = "hi dhl"
    val result = oldName.equals(newName, ignoreCase = true)
}

equals 编译之后的 Java 代码

影响性能的 Kotlin 代码(一)

使用 equals 方法并没有创建额外的对象,如果遇到需要比较字符串的时候,可以使用这种方法,减少额外的对象创建。

by lazy

by lazy 作用是懒加载,保证首次访问的时候才初始化 lambda 表达式中的代码, by lazy 有三种模式。

  • LazyThreadSafetyMode.NONE 仅仅在单线程
  • LazyThreadSafetyMode.SYNCHRONIZED 在多线程中使用
  • LazyThreadSafetyMode.PUBLICATION 不常用

LazyThreadSafetyMode.SYNCHRONIZED 是默认的模式,多线程中使用,可以保证线程安全,但是会有 double check + lock 性能开销,代码如下图所示。

影响性能的 Kotlin 代码(一)

如果是在主线程中使用,和初始化相关的逻辑,建议使用 LazyThreadSafetyMode.NONE 模式,减少不必要的开销。

学习资源推荐👉

《Kotlin 入门教程指南》,篇幅有限,下方有免费领取方式!

影响性能的 Kotlin 代码(一)

这份完整版的《Kotlin 入门教程指南》PDF版电子书,点这里可以看到全部内容。或者点击 【这里】 查看获取方式。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Stella981 Stella981
3年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
Stella981 Stella981
3年前
Kotlin代码检查在美团的探索与实践
背景Kotlin有着诸多的特性,比如空指针安全、方法扩展、支持函数式编程、丰富的语法糖等。这些特性使得Kotlin的代码比Java简洁优雅许多,提高了代码的可读性和可维护性,节省了开发时间,提高了开发效率。这也是我们团队转向Kotlin的原因,但是在实际的使用过程中,我们发现看似写法简单的Kotlin代码,可能隐藏着不容忽视的额外开销。本文剖析了K
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
小万哥 小万哥
1年前
Kotlin 循环与函数详解:高效编程指南
Kotlin中的循环结构让你能轻松遍历数组或范围内的元素。使用for循环结合in操作符,可以简洁地访问数组中的每个项,如字符串数组或整数数组。对于范围,可以用..来定义一系列连续的值并进行迭代。此外,Kotlin支持通过break和continue控制循环流程。函数则允许封装可复用的代码块,你可以定义接受参数并返回值的函数,利用简写语法使代码更加紧凑。例如,myFunction(x:Int,y:Int)xy简洁地定义了一个计算两数之和的函数。