[譯][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 應用系列

工具系列

逆向系列

相關文章
相關標籤/搜索