「译」Kotlin 的性能优化那些事
- 如果评论区没有及时回复,欢迎来公众号:ByteCode 咨询
- 公众号:ByteCode。致力于分享最新技术原创文章,涉及 Kotlin、Jetpack、算法、译文、系统源码相关的文章
前言
- 原标题: Item: Consider aggregating elements to a map
- 原地址: https://blog.kotlin-academy.com/item……
- 原作者:Marcin Moskala
- 译者:hi-dhl
- 本文已收录于仓库 Technical-Article-Translation
这篇文章应该可以说是 [译][2.4K Start] 放弃 Dagger 拥抱 Koin 文章的续集,在 “放弃 Dagger 拥抱 Koin” 文章中介绍了过渡使用 Inline 修饰符所带来的后果,以及 Koin 团队在为修复 1x 版本所做的性能优化,这边文章将继续学习如何提升 Kotlin 的查询速度。
通过这篇文章你将学习到以下内容,将在译者思考部分会给出相应的答案
- 如何提升 Kotlin 的查询速度?
- 性能和代码可读性该做如何选择?
- Kotlin 内存泄露那些事, 消除过期的对象引用?
- 如何提高 Kotlin 代码的可读性?
- Kotlin 算法:一行代码实现杨辉三角?
这篇文章涉及很多重要的知识点,带着自己理解,请耐心读下去,应该可以从中学到很多技巧
译文
我们需要多次访问大量的数据情况,这其实并不少见,例如:
- cache:从服务上下载的数据,然后保存在本地内存中以更快地访问它们
- repository:从一些文件中加载数据
- in-memory repository:用于不同类型的内存测试
这些数据可能表示一些用户、id、配置等等,它们通常以 list 形式返给我们,它们可能以相同的方式存储在内存中:
class NetworkUserRepo(val userService: UserService): UserRepo { |
这可能是存储这些元素的最好方式,注意我们是如何加载数据如何使用的,我们通过某个标识符或者名字访问这些元素(它们与我们设计数据库时唯一值有关),当 n 等于 list 的大小时,在 list 中查找元素的复杂度为 O(n),更准确的说,平均需要 n / 2 次比较才能找到一个元素,如果是一个比较的大的 list,查找效率极其低效,解决这个问题的一个好办法是使用 Map 代替 list, Kotlin 默认使用的是 hash map, 更具体的说是 LinkedHashMap,当我们使用 hash map 查找元素的性能要好得多, 实际上 JVM 使用的 hash map 的大小根据映射本身的大小进行了调整, 如果实现 hashCode 方式正确,查找一个元素只需要进行一次比较。
这是 InMemoryRepo 中使用 map 代替 list
class InMemoryUserRepo: UserRepo { |
大多是其他操作,比如修改或者迭代这些数据(可能使用集合方法 filter, map, flatMap, sorted, sum 等等)对于 list 和 map 性能差不多的。
那么我们如何从 list 转换到 map,或者从 map 转换到 list,使用 associate 方法来完成 list 转换到 map,最常见的方法是 associateBy,它构建一个映射,其中的值是列表中的元素,键是通过一个 lambda 表达式提供。
data class User(val id: Int, val name: String) |
注意,映射中的键必须是唯一的,否则相同键值的元素会被删掉,这就是为什么我们应该根据唯一标识符进行关联(对于键值不是唯一的,应该使用 groupBy 方法)
val users = listOf(User(1, "Michal"), User(2, "Michal")) |
从 map 转换到 list 使用 values 方法
val users = listOf(User(1, "Michal"), User(2, "Michal")) |
如何在 repositories 中用 Map 提高元素访问的性能
class NetworkUserRepo(val userService: UserService): UserRepo { |
这个技巧是非常重要的,但是并不适合所有的 cases,当我们需要访问比较大的 list 的时候是非常有用的,这在后台访问是非常重要的,这些 list 可能在后台每秒被访问很多次,但是在前台并不重要(这里说的是 Android 或者 iOS)用户最多只会访问几次 repository,需要注意的是从 list 转换到 map 是需要时间的,如果过渡使用,可能会对性能有不好的影响。
译者思考
作者总共从三个方面 Network、Configurations、InMemory 告诉我们应该如何从 list 转 map, 或者从 map 转 list, 以及应该在后台需要多次访问很大的数据集合中使用 map,过渡的使用只会对性能产生负面的影响。
- list 转 map 调用用 associateBy 方法,接受一个 lambda 表达式
val users = listOf(User(1, "Michal"), User(2, "Michal")) |
- 从 map 转 list 调用 values 方法
val users = listOf(User(1, "Michal"), User(2, "Michal")) |
这是一个非常重要的优化的手段(使用空间换取时间),在 [译][2.4K Start] 放弃 Dagger 拥抱 Koin 文章中介绍了当我们引入 Koin 1x 的时候冷启动时间变长了,而且在有大量依赖的时候,查找的时间会有点长,用过这个版本的朋友,应该都会有这个感觉,Koin 团队的解决方案中用到了 HashMap,使用空间换取时间,查找一个 Definition 时间复杂度变成了 O(1),从提高的访问速度。
其实我们应该在头脑中,保持内存管理的意识,在每次优化、修改代码之前,不要急于写代码,先整理一下思路,在头脑中过一遍自己的方案,我们应该为项目找到一个折衷方案,不仅要考虑内存和性能,还要考虑代码的可读性。当我们做一个应用程序,在大多数情况下可读性更重要。当我们开发一个库时,通常性能和内存更重要。
性能和代码可读性该做如何选择
如果用 Java 和 Kotlin 语言刷过 LeetCode,使用相同的思路实现同一个算法,在正常的 Case 中,Kotlin 和 Java 执行时间差值很小,数据量越大的情况下 Kotlin 和 Java 差距会越来越大,Kotlin 执行时间会越来越慢,但是为什么 Kotlin 语言还会成为 Android 开发的首选语言呢?来看一下作者 Marcin Moskala 另外一篇文章 My favorite examples of functional programming in Kotlin 展示的快排算法。
在之前的文章中分享了过这个算法,现在我们来分析一下这个算法。
fun <T : Comparable<T>> List<T>.quickSort(): List<T> = |
这是一个非常酷的函数式编程的例子,当看到这个算法的第一感觉,它非常的简洁,可读性很强,其次我们来看一下这个算法执行时间,其实它根本没有针对性能进行优化。
如果你需要使用高性能的算法,你可以使用 Java 标准库当中的函数,Kotlin 扩展函数 sorted() 就是用 Java 标准库中的函数,Java 标准库中的函数效率会更高的,但是实际执行时间怎么样呢?生成一个随机数数组,使用使用 quickSort() 和 sorted() 方法进行排序,比较它们的执行时间,代码如下所示:
val r = Random() |
执行结果如下所示:
Java stdlib sorting of 100000 elements took 83 |
正如你所见,quickSort() 比 sorted() 排序算法要慢两倍,在正常情况下,差值通常在 0.1ms 和 0.2ms 之间,基本上可以忽略不计,但是它更简洁,可读性更强。这解释了在某些情况下,我们可以考虑使用一个优化程度稍低,但可读性强且简洁的函数,你同意作者这种观点吗?
Kotlin 内存泄露那些事, 消除过期的对象引用
我看过很多文章都说 Kotlin 简洁和高效,Kotlin 确实很简洁,在 “如何提高 Kotlin 代码的可读性” 部分我会列举一些例子,但是高效的背后是有代价的,这块往往很容易被我们忽略,这就需要我们去研究 kotlin 语法糖背后的魔法,当我们在开发的时候,选择合适的语法糖,尽量避免这些错误,例如带有 lnmba 表达式高阶函数,不使用 Inline 修饰符,会被编译成匿名内部类等等,更详细的内容参考 [译][2.4K Start] 放弃 Dagger 拥抱 Koin Inline 修饰符带来的性能损失部分。
内存管理最重要的一条规则是,不使用的对象应该被释放
这篇文章 Effective Java in Kotlin, item 7: Eliminate obsolete object references 作者也列举了 Kotlin 的一些例子,例如我们需要使用 mutableLazy 属性委托,像 lazy 一样工作,我们来看一下实现代码:
fun <T> mutableLazy(initializer: () -> T): ReadWriteProperty<Any?, T> = MutableLazy(initializer) |
如何使用:
var game: Game? by mutableLazy { readGameFromSave() } |
思考一下 mutableLazy 实现正确吗? 它有一个地方不对,lnmba 表达式 initializer 在使用后没有被删除。这意味着只要对 MutableLazy 实例的引用存在,它就会被保持,即使它不再有用,如何改进 MutableLazy 实现的方法,优化代码如下所示:
fun <T> mutableLazy(initializer: () -> T): ReadWriteProperty<Any?, T> = MutableLazy(initializer) |
在使用完之后将 initializer 设置为 null,它将会被 GC 回收。特别要注意当一个高阶函数会被编译成匿名类时或者它是一个未知类(任何或泛型类型)时,这个优化显得非常重要,我们来看一下 Kotlin stdlib 库中的类 SynchronizedLazyImpl 代码如下所示:
kotlin-stdlib……/kotlin/util/LazyJVM.kt
private class SynchronizedLazyImpl<out T>( |
请注意,在使用完之后 initializers 设置为 null,将会被 GC 回收
如何提高 Kotlin 代码的可读性
上文提到了 Kotlin 简洁可读性很强,但是呢通过 AndroidStudio 提供了 convert our Java code to Kotlin 插件,将 Java 代码转换为 Kotlin 代码,Java-Style Kotlin 的代码明显很难看,那么如何提升 Kotlin 代码的可读性,我想分享几个很酷的例子 Improve Java to Kotlin code review,用到了 Elvis 表达式、run, with 等等函数
消除!!
myList!!.length |
change to
myList?.length |
空检查
if (callback != null) { |
change to
callback?.response() |
使用 Elvis 表达式
if (toolbar != null) { |
change to
toolbar?.title = arguments?.getString(TITLE) ?: “” |
使用 scope 函数
val intent = intentUtil.createIntent(activity!!.applicationContext) |
change to
activity?.run { |
ps: scope 函数还有 run, with, let, also and apply,它们的区别是什么,如何正确使用它们,后面的文章会详细的介绍。
使用 takeIf if 函数
if (something != null && something == preference) { |
change to
something?.takeIf { it == preference }?.let { something.doThing() } |
Android TextUtil
if (TextUtils.isEmpty(someString)) {...} |
change to
if (someString.isEmpty()) {...} |
Java Util
val strList = Arrays.asList("someString") |
change to
val strList = listOf("someString") |
Empty and null
if (myList == null || myList.isEmpty()) {...} |
change to
if (myList.isNullOrEmpty() {...} |
避免对对象进行重复操作
recyclerView.setLayoutManager(layoutManager) |
change to
with(recyclerView) { |
避免列表循环
for (str in stringList) { |
change to
stringList.forEach { println(it) } |
避免使用 mutable 集合
val stringList: List<String> = mutableListOf() |
change to
val stringList = otherList.map { dosSomething(it) } |
使用 when 代替 if
if (requestCode == REQUEST_1) { |
change to
when (requestCode) { |
使用 const
companion object { |
change to
companion object { |
如果有更好的例子,欢迎留言
Kotlin 算法:一行代码实现杨辉三角
我想分享一个很酷的算法,用一行代码实现杨辉三角,代码来自 Marcin Moskala 大神的 Twitter
fun pascal() = generateSequence(listOf(1)) { prev -> |
安利一个译者自己撸的导航网站
基于 Python + Material Design 开发的 “为互联网人而设计 国内国外名站导航“ ,收集了国内外热门网址,涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android开发等等导航网站 地址
参考文献
- Item: Consider aggregating elements to a map
- Effective Java in Kotlin, item 7: Eliminate obsolete object references
- Improve Java to Kotlin code review
推荐文章
- 本文作者:hi-dhl
- 本文标题:「译」Kotlin 的性能优化那些事
- 本文链接:https://hi-dhl.com/2020/08/05/translate/04-kotlin-performance/
- 版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 hi-dhl