[译][5k+] Kotlin 的性能优化那些事

前言

  • 原标题: Item: Consider aggregating elements to a map
  • 原文地址: blog.kotlin-academy.com/item......
  • 原文做者:Marcin Moskala
  • 介绍:做者 Marcin Moskala 是大神级别的人物,在 Medium 上至少有 5K+ 的关注者,在 Twitter 上至少有 4K+ 的关注者,是 「Effective Kotlin」一书的做者之一。「Effective Kotlin」总结了 Kotlin 社区的最佳实践和经验,不少来自 Google 工程师的案例,揭露了不少鲜为人知的 Kotlin 背后的魔法。

这篇文章应该能够说是 [译][2.4K Start] 放弃 Dagger 拥抱 Koin 文章的续集,在 “放弃 Dagger 拥抱 Koin” 文章中介绍了过渡使用 Inline 修饰符所带来的后果,以及 Koin 团队在为修复 1x 版本所作的性能优化,这边文章将继续学习如何提高 Kotlin 的查询速度。html

经过这篇文章你将学习到如下内容,将在译者思考部分会给出相应的答案前端

  • 如何提高 Kotlin 的查询速度?
  • 性能和代码可读性该作如何选择?
  • Kotlin 内存泄露那些事, 消除过时的对象引用?
  • 如何提升 Kotlin 代码的可读性?
  • Kotlin 算法:一行代码实现杨辉三角?

这篇文章涉及不少重要的知识点,带着本身理解,请耐心读下去,应该能够从中学到不少技巧java

译文

咱们须要屡次访问大量的数据状况,这其实并很多见,例如:git

  • cache:从服务上下载的数据,而后保存在本地内存中以更快地访问它们
  • repository:从一些文件中加载数据
  • in-memory repository:用于不一样类型的内存测试

这些数据可能表示一些用户、id、配置等等,它们一般以 list 形式返给咱们,它们可能以相同的方式存储在内存中:算法

class NetworkUserRepo(val userService: UserService): UserRepo {
    private var users: List<User>? = null
    override fun getUser(id: UserId): User? {
        if(users == null) {
            users = userService.getUsers()
        }
        return users?.firstOrNull { it.id == id }
    }
}

class ConfigurationsRepository(
    val configurations: List<Configuration>
) {
    fun getByName(name: String) = configurations
        .firstOrNull { it.name == name }
}
class InMemoryUserRepo: UserRepo {
   private val users: MutableList<User> = mutableListOf()
   override fun getUser(id: UserId): User?
      = users.firstOrNull { it.id == id }
   
   fun addUser(user: User) {
      user.add(user)
   }
}
复制代码

这多是存储这些元素的最好方式,注意咱们是如何加载数据如何使用的,咱们经过某个标识符或者名字访问这些元素(它们与咱们设计数据库时惟一值有关),当 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 {
   private val users: MutableMap<UserId, User> = mutableMapOf()
   override fun getUser(id: UserId): User? = users[id]
   
   fun addUser(user: User) {
      user.put(user.id, user)
   }
}
复制代码

大可能是其余操做,好比修改或者迭代这些数据(可能使用集合方法 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)
val users = listOf(User(1, "Michal"), User(2, "Marek"))
val byId = users.associateBy { it.id }
byId == mapOf(1 to User(1, "Michal"), 2 to User(2, "Marek")) 
val byName = users.associateBy { it.name }
byName == mapOf("Michal" to User(1, "Michal"), 
              "Marek" to User(2, "Marek"))
复制代码

注意,映射中的键必须是惟一的,不然相同键值的元素会被删掉,这就是为何咱们应该根据惟一标识符进行关联(对于键值不是惟一的,应该使用 groupBy 方法)bash

val users = listOf(User(1, "Michal"), User(2, "Michal"))
val byName = users.associateBy { it.name }
byName == mapOf("Michal" to User(2, "Michal"))
复制代码

从 map 转换到 list 使用 values 方法

val users = listOf(User(1, "Michal"), User(2, "Michal"))
val byId = users.associateBy { it.id }
users == byId.values
复制代码

如何在 repositories 中用 Map 提升元素访问的性能

class NetworkUserRepo(val userService: UserService): UserRepo {
    private var users: Map<UserId, User>? = null
    override fun getUser(id: UserId): User? {
        if(users == null) {
            users = userService.getUsers().associateBy { it.id }
        }
        return users?.get(id)
    }
}

class ConfigurationsRepository(
    configurations: List<Configuration>
) {
    val configurations: Map<String, Configuration> = 
        configurations.associateBy { it.name }
    
    fun getByName(name: String) = configurations[name]
}
复制代码

这个技巧是很是重要的,可是并不适合全部的 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"))
val byName = users.associateBy { it.name }
byName == mapOf("Michal" to User(2, "Michal"))
复制代码
  • 从 map 转 list 调用 values 方法
val users = listOf(User(1, "Michal"), User(2, "Michal"))
val byId = users.associateBy { it.id }
users == byId.values
复制代码

这是一个很是重要的优化的手段(使用空间换取时间),在 [译][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> = 
    if(size < 2) this
    else {
        val pivot = first()
        val (smaller, greater) = drop(1).partition { it <= pivot}
        smaller.quickSort() + pivot + greater.quickSort()
    }
    
// 使用 [2,5,1] -> [1,2,5]
listOf(2,5,1).quickSort() // [1,2,5]
复制代码

这是一个很是酷的函数式编程的例子,当看到这个算法的第一感受,它很是的简洁,可读性很强,其次咱们来看一下这个算法执行时间,其实它根本没有针对性能进行优化。

若是你须要使用高性能的算法,你可使用 Java 标准库当中的函数,Kotlin 扩展函数 sorted() 就是用 Java 标准库中的函数,Java 标准库中的函数效率会更高的,可是实际执行时间怎么样呢?生成一个随机数数组,使用使用 quickSort() 和 sorted() 方法进行排序,比较它们的执行时间,代码以下所示:

val r = Random()
listOf(100_000, 1_000_000, 10_000_000)
    .asSequence()
    .map { (1..it).map { r.nextInt(1000000000) } }
    .forEach { list: List<Int> ->
        println("Java stdlib sorting of ${list.size} elements took ${measureTimeMillis { list.sorted() }}")
        println("quickSort sorting of ${list.size} elements took ${measureTimeMillis { list.quickSort() }}")
    }
复制代码

执行结果以下所示:

Java stdlib sorting of 100000 elements took 83
quickSort sorting of 100000 elements took 163
Java stdlib sorting of 1000000 elements took 558
quickSort sorting of 1000000 elements took 859
Java stdlib sorting of 10000000 elements took 6182
quickSort sorting of 10000000 elements took 12133`
复制代码

正如你所见,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)

private class MutableLazy<T>(
    val initializer: () -> T
) : ReadWriteProperty<Any?, T> {

    private var value: T? = null
    private var initialized = false

    override fun getValue(
        thisRef: Any?, 
        property: KProperty<*>
    ): T {
        synchronized(this) {
            if (!initialized) {
                value = initializer()
                initialized = true
            }
            return value as T
        }
    }

    override fun setValue(
        thisRef: Any?, 
        property: KProperty<*>, 
        value: T
    ) {
        synchronized(this) {
            this.value = value
            initialized = true
        }
    }
}
复制代码

如何使用:

var game: Game? by mutableLazy { readGameFromSave() }

fun setUpActions() {
    startNewGameButton.setOnClickListener {
        game = makeNewGame()
        startGame()
    }
    resumeGameButton.setOnClickListener {
        startGame()
    }
}
复制代码

思考一下 mutableLazy 实现正确吗? 它有一个地方不对,lnmba 表达式 initializer 在使用后没有被删除。这意味着只要对 MutableLazy 实例的引用存在,它就会被保持,即便它再也不有用,如何改进 MutableLazy 实现的方法,优化代码以下所示:

fun <T> mutableLazy(initializer: () -> T): ReadWriteProperty<Any?, T> = MutableLazy(initializer)

private class MutableLazy<T>(
    var initializer: (() -> T)?
) : ReadWriteProperty<Any?, T> {

    private var value: T? = null

    override fun getValue(
        thisRef: Any?, 
        property: KProperty<*>
    ): T {
        synchronized(this) {
            val initializer = initializer
            if (initializer != null) {
                value = initializer()
                this.initializer = null
            }
            return value as T
        }
    }

    override fun setValue(
        thisRef: Any?, 
        property: KProperty<*>, 
        value: T
    ) {
        synchronized(this) {
            this.value = value
            this.initializer = null
        }
    }
}
复制代码

在使用完以后将 initializer 设置为 null,它将会被 GC 回收。特别要注意当一个高阶函数会被编译成匿名类时或者它是一个未知类(任何或泛型类型)时,这个优化显得很是重要,咱们来看一下 Kotlin stdlib 库中的类 SynchronizedLazyImpl 代码以下所示:
kotlin-stdlib....../kotlin/util/LazyJVM.kt

private class SynchronizedLazyImpl<out T>(
    initializer: () -> T, lock: Any? = null
) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    private var _value: Any? = UNINITIALIZED_VALUE
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
    ......
}
复制代码

请注意,在使用完以后 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) {              
    callback!!.response()
}
复制代码

change to

callback?.response()
复制代码

使用 Elvis 表达式

if (toolbar != null) {
  if (arguments != null) {                  
    toolbar!!.title = arguments!!.getString(TITLE)              
  } else {                
    toolbar!!.title = ""            
  }
}
复制代码

change to

toolbar?.title = arguments?.getString(TITLE) ?: “”
复制代码

使用 scope 函数

val intent = intentUtil.createIntent(activity!!.applicationContext) 
activity!!.startActivity(intent)
dismiss()
复制代码

change to

activity?.run { 
    val intent = intentUtil.createIntent(this)        
    startActivity(intent) 
    dismiss() 
}
复制代码

ps: scope 函数还有 run, with, let, also and apply,它们的区别是什么,如何正确使用它们,后面的文章会详细的介绍。

使用 takeIf if 函数

if (something != null && something == preference) {   
     something.doThing() 
复制代码

change to

something?.takeIf { it == preference }?.let { something.doThing() }
复制代码

Android TextUtil

if (TextUtils.isEmpty(someString)) {...}
val joinedString = TextUtils.join(COMMA, separateList)
复制代码

change to

if (someString.isEmpty()) {...}
val joinedString = separateList.joinToString(separator = COMMA)
复制代码

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)
recyclerView.setAdapter(adapter) 
recyclerView.setItemAnimator(animator)
复制代码

change to

with(recyclerView) {
    setLayoutManager(layoutManager)         
    setAdapter(adapter)         
    setItemAnimator(animator)
}
复制代码

避免列表循环

for (str in stringList) {
    println(str)
}
复制代码

change to

stringList.forEach { println(it) }
复制代码

避免使用 mutable 集合

val stringList: List<String> = mutableListOf()
for (other in otherList) {
    stringList.add(dosSomething(other))
}
复制代码

change to

val stringList = otherList.map { dosSomething(it) }
复制代码

使用 when 代替 if

if (requestCode == REQUEST_1) {            
    doThis()
} else if (requestCode == REQUEST_2) {
    doThat()
} else {
    doSomething()
}
复制代码

change to

when (requestCode) { 
    REQUEST_1 -> doThis()
    REQUEST_1 -> doThat()
    else -> doSomething()
}
复制代码

使用 const

companion object {        
    val EXTRA_STRING = "EXTRA_EMAIL"
    val EXTRA_NUMBER = 12345
}
复制代码

change to

companion object {        
    const val EXTRA_STRING = "EXTRA_EMAIL"
    const val EXTRA_NUMBER = 12345
}
复制代码

若是有更好的例子,欢迎留言

Kotlin 算法:一行代码实现杨辉三角

我想分享一个很酷的算法,用一行代码实现杨辉三角,代码来自 Marcin Moskala 大神的 Twitter

fun pascal() = generateSequence(listOf(1)) { prev ->
    listOf(1) + (1..prev.lastIndex).map { prev[it - 1] + prev[it] } + listOf(1)
}

fun main() {
    pascal().take(10).forEach(::println)
}
复制代码

20200517-124137

在这里有个小建议,能够关注一些你感兴趣的官方、大牛的 Twitter 帐号,还有,他们不定时就会分享一些新的技术、新的文章等等。

官方、大牛的 Twitter 帐号

  • Jake Wharton @JakeWharton:Android 之神不须要过多介绍。
  • Android @Android: 官网帐号
  • Marcin Moskala @marcinmoskala:是 「Effective Kotlin」一书的做者之一。「Effective Kotlin」总结了 Kotlin 社区的最佳实践和经验
  • Arnaud Giuliani @arnogiu: Koin 软件工程师-演讲者-写技术博客-开源
  • Kotlin @kotlin:官网帐号
  • Kt. Academy @ktdotacademy:Kt学院(原Kotlin学院)的任务是简化Kotlin学习
  • MIT Tech Review @techreview:来自 MIT,质量不错的科技时评,关注最前沿的科技动态
  • Andrew Ng @andrewYNg:Coursera 创始人,AI 大牛吴恩达
  • JetBrains @jetbrains:IntelliJ IDEA、ReSharper、PyCharm、TeamCity、Kotlin等的创造者
  • 还有不少不少 ......

以上大牛的都会在 medium 上学技术文章,有能力的朋友能够多从上面看最新的文章,国内也有不少资源,能够访问译者本身撸的导航网站,"为互联网人而设计 国内国外名站导航" ,收集了国内外热门网址,涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等导航网站

参考文献

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译相关的文章,目前正在翻译一系列欧美精选文章,请持续关注,除了翻译还有对每篇欧美文章思考,若是对你有帮助,请帮我点个赞,感谢!!!期待与你一块儿成长。

文章列表

Android 10 源码系列

Android 应用系列

工具系列

逆向系列

相关文章
相关标签/搜索