影响性能的 Kotlin 代码(一)

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

image.png


公众号:ByteCode,致力于分享最新技术原创文章,涉及 Kotlin、Jetpack、译文、系统源码、 LeetCode / 剑指 Offer / 多线程 / 国内外大厂算法题 等等一系列文章。


要开始写新的 Kotlin 系列了 「影响性能的 Kotlin 代码」, 同时我也在写另一个系列 「为数不多的人知道的 Kotlin 技巧及解析」,没有看过的小伙伴,可以点击下方链接前去查看。



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


Lambda 表达式



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


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


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


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


image.png


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


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


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


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


image.png


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


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


Inline 修饰符


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


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


image.png


编译器建议我们在含有 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 代码


image.png


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


toUpperCase() 编译之后的 Java 代码


image.png


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


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


equals 编译之后的 Java 代码


image.png


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


by lazy



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


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


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


image.png


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


仓库 KtKit 是用 Kotlin 语言编写的小巧而实用的工具库,包含了项目中常用的一系列工具, 正在逐渐完善中,如果你有兴趣,想邀请你和我一起来完善这个库。



如果这个仓库对你有帮助,请在仓库右上角帮我 star 一下,非常感谢你的支持,同时也欢迎你提交 PR


如果有帮助 点个赞 就是对我最大的鼓励


代码不止,文章不停


欢迎关注公众号:ByteCode,持续分享最新的技术


最后推荐我一直在更新维护的项目和网站:


  • 个人博客,将所有文章进行分类,欢迎前去查看 hi-dhl.com
  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看:AndroidX-Jetpack-Practice
  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析

image.png


  • 最新 Android 10 源码分析系列文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,仓库持续更新,欢迎前去查看 Android10-Source-Analysis
  • 整理和翻译一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的解读,仓库持续更新,欢迎前去查看 Technical-Article-Translation
  • 「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址,欢迎前去查看 为互联网人而设计导航网站


近期必读热门文章




目录
相关文章
|
XML 编译器 Android开发
Kotlin DSL 实战:像 Compose 一样写代码
Kotlin DSL 实战:像 Compose 一样写代码
658 0
|
JSON 监控 数据挖掘
使用Kotlin代码简化局域网监控软件开发流程
使用Kotlin简化局域网监控软件开发,通过获取网络设备的IP和MAC地址,实现实时监控网络流量。示例代码展示了如何创建Kotlin项目,获取网络设备信息,监控网络流量以及进行数据分析和处理。此外,还演示了如何使用HTTP库将数据提交到网站,为网络管理提供高效支持。
435 0
|
自然语言处理 Java 网络架构
解锁跨平台微服务新纪元:Micronaut与Kotlin联袂打造的多语言兼容服务——代码、教程、实战一次打包奉送!
【9月更文挑战第6天】Micronaut是一款轻量级、高性能的Java框架,适用于微服务开发。它支持Java、Groovy和Kotlin等多种语言,提供灵活的多语言开发环境。本文通过创建一个简单的多语言兼容服务,展示如何使用Micronaut及其注解驱动特性实现REST接口,并引入国际化支持。无论是个人项目还是企业应用,Micronaut都能提供高效、一致的开发体验,成为跨平台开发的利器。通过简单的配置和代码编写,即可实现多语言支持,展现其强大的跨平台优势。
422 3
|
安全 Java Android开发
Kotlin字符串秘籍:解锁高效处理与创意应用,让你的代码闪耀不凡!
【8月更文挑战第2天】Kotlin是一门现代化的静态类型语言,以简洁、安全及强互操作性著称,在Android及服务器端开发中广受好评。本文通过与其他语言对比,深入解析Kotlin中字符串的基础和高级用法。Kotlin简化了字符串拼接,支持直接使用`+`操作符,并引入了直观的字符串模板。它提供了丰富的字符串操作函数,如使用索引范围进行子字符串提取,增强了代码的可读性。Kotlin字符串的不可变性提升了程序稳定性。利用扩展函数特性,可以轻松定制字符串行为,提高代码的模块化和重用性。掌握这些技巧能显著提升开发效率和代码质量。
287 1
|
API 数据处理 数据库
掌握 Kotlin Flow 的艺术:让无限数据流处理变得优雅且高效 —— 实战教程揭秘如何在数据洪流中保持代码的健壮与灵活
Kotlin Flow 是一个强大的协程 API,专为处理异步数据流设计。它适合处理网络请求数据、监听数据库变化等场景。本文通过示例代码展示如何使用 Kotlin Flow 管理无限流,如实时数据流。首先定义了一个生成无限整数的流 `infiniteNumbers()`,然后结合多种操作符(如 `buffer`、`onEach`、`scan`、`filter`、`takeWhile` 和 `collectLatest`),实现对无限流的优雅处理,例如计算随机数的平均值并在超过阈值时停止接收新数据。这展示了 Flow 在资源管理和逻辑清晰性方面的优势。
396 0
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
Java Kotlin
java调用kotlin代码编译报错“找不到符号”的问题
java调用kotlin代码编译报错“找不到符号”的问题
782 10
|
缓存 API Android开发
Kotlin 学习笔记(七)—— Flow 数据流学习实践指北(三)冷流转热流以及代码实例(下)
Kotlin 学习笔记(七)—— Flow 数据流学习实践指北(三)冷流转热流以及代码实例(下)
523 0
|
缓存 Java Kotlin
Kotlin 学习笔记(七)—— Flow 数据流学习实践指北(三)冷流转热流以及代码实例(上)
Kotlin 学习笔记(七)—— Flow 数据流学习实践指北(三)冷流转热流以及代码实例(上)
434 0
|
Java Android开发 Kotlin
kotlin查看编译后的Java代码
kotlin查看编译后的Java代码