天天看點

Kotlin中一些知識點學習1. 協程2.函數3.其他知識點4.使用協程别導錯包

1. 協程

github位址:kotlinx.coroutines(https://github.com/kotlin/kotlinx.coroutines)

fun main(args: Array<String>) {
    launch(CommonPool) {
        delay(L) 
        println("World!") 
    }
    println("Hello,")
    Thread.sleep(2000L)
}

/* 
運作結果: ("Hello,"會立即被列印, 1000毫秒之後, "World!"會被列印)
Hello, 
World!
*/
           

解釋一下delay方法:

在協程裡delay方法作用等同于線程裡的sleep, 都是休息一段時間, 但不同的是delay不會阻塞目前線程, 而像是設定了一個鬧鐘, 在鬧鐘未響之前, 運作該協程的線程可以被安排做了别的事情, 當鬧鐘響起時, 協程就會恢複運作.

協程啟動後還可以取消:

launch方法有一個傳回值, 類型是Job, Job有一個cancel方法, 調用cancel方法可以取消協程, 看一個數羊的例子:

fun main(args: Array<String>) {
    val job = launch(CommonPool) {
        var i = 
        while(true) {
            println("$i little sheep")
            ++i
            delay(L)  // 每半秒數一隻, 一秒可以輸兩隻
        }
    }

    Thread.sleep(L)  // 在主線程睡眠期間, 協程裡已經數了兩隻羊
    job.cancel()  // 協程才數了兩隻羊, 就被取消了
    Thread.sleep(L)
    println("main process finished.")
}
           

運作結果是,如果不調用cancel, 可以數到4隻羊:

little sheep
 little sheep
main process finished.
           

注意還有一個方法:job.join() // 持續等待,直到子協程執行完成

1.1 了解suspend方法

suspend方法的文法很簡單, 隻是比普通方法隻是多了個suspend關鍵字:

suspend fun foo(): ReturnType {
    // ...
}
           

suspend方法隻能在協程裡面調用, 不能在協程外面調用.

suspend方法本質上, 與普通方法有較大的差別, suspend方法的本質是異步傳回(注意: 不是異步回調).

現在, 我們先來看一個異步回調的例子:

fun main(...) {
  requestDataAsync {
    println("data is $it")
  }
  Thead.sleep()  // 這個sleep隻是為了保活程序
}

fun requestDataAsync(callback: (String)->Unit) {
    Thread() {
        // do something need lots of times.
        callback(data)
    }.start()
}
           

邏輯很簡單, 就是通過異步的方法拉一個資料, 然後使用這個資料, 按照以往的程式設計方式, 若要接受異步回來的資料, 唯有使用callback.

但是假如使用協程, 可以不使用callback, 而是直接把這個資料”return”回來, 調用者不使用callback接受資料, 而是像調用同步方法一樣接受傳回值. 如果上述功能改用協程, 将會是:

fun main(...) {
    launch(Unconfined) {  // 請重點關注協程裡是如何擷取異步資料的
        val data = requestDataAsync()  // 異步回來的資料, 像同步一樣return了
        println("data is $it")
    }

    Thead.sleep() // 請不要關注這個sleep
}

suspend fun requestDataAsync() { // 請注意方法前多了一個suspend關鍵字
    return async(CommonPool) { // 先不要管這個async方法, 後面解釋
        // do something need lots of times.
        // ...
        data  // return data, lambda裡的return要省略
    }.await()
}
           

這裡, 我們首先将requestDataAsync轉成了一個suspend方法, 其原型的變化是:

1.在前加了個suspend關鍵字.

2.去除了原來的callback參數.

這是怎麼做到的呢?

當程式執行到requestDataAsync内部時, 通過async啟動了另外一個新的子協程去拉取資料, 啟動這個新的子協程後, 目前的父協程就挂起了, 此時requestDataAsync還沒有傳回.子協程一直在背景跑, 過了一段時間, 子協程把資料拉回來之後, 會恢複它的父協程, 父協程繼續執行, requestDataAsync就把資料傳回了.

為了加深了解, 我們來對比一下另一個例子: 不使用協程, 将異步方法也可以轉成同步的方法(在單元測試裡, 我們經常這麼做):

fun main(...) {
    val data = async2Sync()  // 資料是同步傳回了, 但是線程也阻塞了
    println("data is $it")
    // Thead.sleep(10000L)  // 這一句在這裡毫無意義了, 注釋掉
}

private var data = ""
private fun async2Sync(): String {
    val obj = Object() // 随便建立一個對象當成鎖使用
    requestDataAsync { data ->
        this.data = data  // 暫存data
        synchronized(locker) {
            obj.notifyAll() // 通知所有的等待者
        }
    }
    obj.wait() // 阻塞等待
    return this.data
}

fun requestDataAsync(callback: (String)->Unit) {
    // ...普通的異步方法
}
           

注意對比上一個協程的例子, 這樣做表面上跟它是一樣的, 但是這裡main方法會阻塞的等待async2Sync()方法完成. 同樣是等待, 協程就不會阻塞目前線程, 而是自己主動放棄執行權, 相當于遣散目前線程, 讓它去幹别的事情去.

為了更好的了解這個”遣散”的含義, 我們再來看一個例子:

fun main(args: Array<String>) {
    // 1. 程式開始
    println("${Thread.currentThread().name}: 1");  

    // 2. 啟動一個協程, 并立即啟動
    launch(Unconfined) { // Unconfined意思是在目前線程(主線程)運作協程
        // 3. 本協程在主線程上直接開始執行了第一步
        println("${Thread.currentThread().name}: 2");  

        /* 4. 本協程的第二步調用了一個suspend方法, 調用之後, 
         * 本協程就放棄執行權, 遣散運作我的線程(主線程)請幹别的去.
         * 
         * delay被調用的時候, 在内部建立了一個計時器, 并設了個callback.
         * 1秒後計時器到期, 就會調用剛設定的callback.
         * 在callback裡面, 會調用系統的接口來恢複協程. 
         * 協程在計時器線程上恢複執行了. (不是主線程, 跟Unconfined有關)
         */
        delay(L)  // 過1秒後, 計時器線程會resume協程

        // 7. 計時器線程恢複了協程, 
        println("${Thread.currentThread().name}: 4")
    }

    // 5. 剛那個的協程不要我(主線程)幹活了, 是以我繼續之前的執行
    println("${Thread.currentThread().name}: 3");

    // 6. 我(主線程)睡2秒鐘
    Thread.sleep(2000L)

    // 8. 我(主線程)睡完後繼續執行
    println("${Thread.currentThread().name}: 5");
}
           

運作結果:

main: 1
main: 2
main: 3
kotlinx.coroutines.ScheduledExecutor: 4
main: 5
           

上述代碼的注釋詳細的列出了程式運作流程, 看完之後, 應該就能明白 “遣散” 和 “放棄執行權” 的含義了.

Unconfined的含義是不給協程指定運作的線程, 逮到誰就使用誰, 啟動它的線程直接執行它, 但被挂起後, 會由恢複它的線程繼續執行, 如果一個協程會被挂起多次, 那麼每次被恢複後, 都可能被不同線程繼續執行.

現在再來回顧剛剛那句: suspend方法的本質就是異步傳回.含義就是将其拆成 “異步” + “傳回”:

首先, 資料不是同步回來的(同步指的是立即傳回), 而是異步回來的.

其次, 接受資料不需要通過callback, 而是直接接收傳回值.

調用suspend方法的詳細流程是:

在協程裡, 如果調用了一個suspend方法, 協程就會挂起, 釋放自己的執行權, 但在協程挂起之前, suspend方法内部一般會啟動了另一個線程或協程, 我們暫且稱之為”分支執行流”吧, 它的目的是運算得到一個資料.當suspend方法裡的*分支執行流”完成後, 就會調用系統API重新恢複協程的執行, 同時會資料傳回給協程(如果有的話).

為什麼不能再協程外面調用suspend方法?

suspend方法隻能在協程裡面調用, 原因是隻有在協程裡, 才能遣散目前線程, 在協程外面, 不允許遣散, 反過來思考, 假如在協程外面也能遣散線程, 會怎麼樣, 寫一個反例:

fun main(args: Array<String>) {
    requestDataSuspend(); 
    doSomethingNormal();
}
suspend fun requestDataSuspend() { 
    // ... 
}
fun doSomethingNormal() {
    // ...
}
           

requestDataSuspend是suspend方法, doSomethingNormal是正常方法, doSomethingNormal必須等到requestDataSuspend執行完才會開始, 如果main方法失去了并行的能力, 所有地方都失去了并行的能力, 這肯定不是我們要的, 是以需要約定隻能在協程裡才可以遣散線程, 放棄執行權, 于是suspend方法隻能在協程裡面調用.

協程建立後, 并不總是立即執行, 要分是怎麼建立的協程, 通過launch方法的第二個參數是一個枚舉類型CoroutineStart, 如果不填, 預設值是DEFAULT, 那麼協程建立後立即啟動, 如果傳入LAZY, 建立後就不會立即啟動, 直到調用Job的start方法才會啟動.

在協程裡, 所有接受callback的方法, 都可以轉成不需要callback的suspend方法,上面的requestDataSuspend方法就是一個這樣的例子, 我們回過頭來再看一眼:

suspend fun requestDataSuspend() {
    return async(CommonPool) {
        // do something need lots of times.
        // ...
        data  // return data
    }.await()
}
           

其内部通過調用了async和await方法來實作(關于async和await我們後面再介紹), 這樣雖然實作功能沒問題, 但并不最合适的方式, 上面那樣做隻是為了追求最簡短的實作, 合理的實作應該是調用suspendCoroutine方法, 大概是這樣:

suspend fun requestDataSuspend() {
    suspendCoroutine { cont ->
        // ... 細節暫時省略
    }
}
// 可簡寫成:
suspend fun requestDataSuspend() = suspendCoroutine { cont ->
    // ...
}
           

在完整實作之前, 需要先了解suspendCoroutine方法, 它是Kotlin标準庫裡的一個方法, 原型如下:

現在來完善一下剛剛的例子:

suspend fun requestDataSuspend() = suspendCoroutine { cont ->
    requestDataFromServer { data -> // 普通方法還是通過callback接受資料
        if (data != null) {
            cont.resume(data)
        } else {
            cont.resumeWithException(MyException())
        }
    }
}

/** 普通的異步回調方法 */
fun requestDataFromServer(callback: (String)->Unit) {
    // ... get data from server, it will call back when finished.
}
           

suspendCoroutine有個特點:

suspendCoroutine { cont ->
    // 如果本lambda裡傳回前, cont的resume和resumeWithException都沒有調用
    // 那麼目前執行流就會挂起, 并且挂起的時機是在suspendCoroutine之前
    // 就是在suspendCoroutine内部return之前就挂起了

    // 如果本lambda裡傳回前, 調用了cont的resume或resumeWithException
    // 那麼目前執行流不會挂起, suspendCoroutine直接傳回了, 
    // 若調用的是resume, suspendCoroutine就會像普通方法一樣傳回一個值
    // 若調用的是resumeWithException, suspendCoroutine會抛出一個異常
    // 外面可以通過try-catch來捕獲這個異常
}
           

回過頭來看一下, 剛剛的實作有調用resume方法嗎, 我們把它折疊一下:

suspend fun requestDataSuspend() = suspendCoroutine { cont ->
    requestDataFromServer { ... }
}
           

清晰了吧, 沒有調用, 是以suspendCoroutine還沒有傳回之前就挂起了, 但是挂起之前lambda執行完了, lambda裡調用了requestDataFromServer, requestDataFromServer裡啟動了真正做事的流程(異步執行的), 而suspendCoroutine則在挂起等待.

等到requestDataFromServer完成工作, 就會調用傳入的callback, 而這個callback裡調用了cont.resume(data), 外層的協程就恢複了, 随後suspendCoroutine就會傳回, 傳回值就是data.

1.2 async/await模式:

我們前面多次使用了launch方法, 它的作用是建立協程并立即啟動, 但是有一個問題, 就是通過launch方法建立的協程都沒辦法攜帶傳回值. async之前也出現過, 但一直沒有詳細介紹.

async方法作用和launch方法基本一樣, 建立一個協程并立即啟動, 但是async建立的協程可以攜帶傳回值.

launch方法的傳回值類型是Job, async方法的傳回值類型是Deferred, 是Job的子類, Deferred裡有個await方法, 調用它可得到協程的傳回值.

async/await是一種常用的模式, async的含義是啟動一個異步操作, await的含義是等待這個異步操作結果.

是誰要等它啊, 在傳統的不使用協程的代碼裡, 是線程在等(線程不幹别的事, 就在那裡傻等). 在協程裡不是線程在等, 而且是執行流在等, 目前的流程挂起(底下的線程會被遣散去幹别的事), 等到有了運算結果, 流程才繼續運作.

是以我們又可以順便得出一個結論: 在協程裡執行流是線性的, 其中的步驟無論是同步的還是異步的, 後面的步驟都會等前面的步驟完成.

我們可以通過async起多個任務, 他們會同時運作, 我們之前使用的async姿勢不是很正常, 下面看一下使用async正常的姿勢:

fun main(...) {
    launch(Unconfined) {
        // 任務1會立即啟動, 并且會在别的線程上并行執行
        val deferred1 = async { requestDataAsync1() }

        // 上一個步驟隻是啟動了任務1, 并不會挂起目前協程
        // 是以任務2也會立即啟動, 也會在别的線程上并行執行
        val deferred2 = async { requestDataAsync2() }

        // 先等待任務1結束(等了約1000ms), 
        // 然後等待任務2, 由于它和任務1幾乎同時啟動的, 是以也很快完成了
        println("data1=$deferred2.await(), data2=$deferred2.await()")
    }

    Thead.sleep(L) // 繼續無視這個sleep
}

suspend fun requestDataAsync1(): String {
    delay(L)
    return "data1"    
}
suspend fun requestDataAsync2(): String {
    delay(L)
    return "data2"    
}
           

運作結果很簡單, 不用說了, 但是協程總耗時是多少呢, 約1000ms, 不是2000ms, 因為兩個任務是并行運作的.

有一個問題: 假如任務2先于任務1完成, 結果是怎樣的呢?

答案是: 任務2的結果會先儲存在deferred2裡, 當調用deferred2.await()時, 會立即傳回, 不會引起協程挂起, 因為deferred2已經準備好了.

是以, suspend方法并不總是引起協程挂起, 隻有其内部的資料未準備好時才會.

需要注意的是: await是suspend方法, 但async不是, 是以它才可以在協程外面調用, async隻是啟動了協程, async本身不會引起協程挂起, 傳給async的lambda(也就是協程體)才可能引起協程挂起.

2.函數

2.1預設參數

函數參數可以有預設值,當省略相應的參數時使用預設值。與其他語言相比,這可以減少重載數量。

fun read(b: Array<Byte>, off: Int = , len: Int = b.size) { …… }
           

覆寫方法總是使用與基類型方法相同的預設參數值。 當覆寫一個帶有預設參數值的方法時,必須從簽名中省略預設參數值:

open class A {
    open fun foo(i: Int = ) { …… }
}

class B : A() {
    override fun foo(i: Int) { …… }  // 不能有預設值
}
           

如果一個預設參數在一個無預設值的參數之前,那麼該預設值隻能通過使用命名參數調用該函數來使用:

fun foo(bar: Int = , baz: Int) { /* …… */ }

foo(baz = ) // 使用預設值 bar = 0
           

不過如果最後一個 lambda 表達式參數從括号外傳給函數函數調用,那麼允許預設參數不傳值:

fun foo(bar: Int = , baz: Int = , qux: () -> Unit) { /* …… */ }

foo() { println("hello") } // 使用預設值 baz = 1 
foo { println("hello") }    // 使用兩個預設值 bar = 0 與 baz = 1
           

2.2 命名參數

可以在調用函數時使用命名的函數參數。當一個函數有大量的參數或預設參數時這會非常友善。

給定以下函數

fun reformat(str: String,
             normalizeCase: Boolean = true,
             upperCaseFirstLetter: Boolean = true,
             divideByCamelHumps: Boolean = false,
             wordSeparator: Char = ' ') {
……
}
           

我們可以使用預設參數來調用它:

然而,當使用非預設參數調用它時,該調用看起來就像:

reformat(str, true, true, false, '_')
           

使用命名參數我們可以使代碼更具有可讀性:

reformat(str,
    normalizeCase = true,
    upperCaseFirstLetter = true,
    divideByCamelHumps = false,
    wordSeparator = '_'
)
           

并且如果我們不需要所有的參數:

reformat(str, wordSeparator = '_')
           

當一個函數調用混用位置參數與命名參數時,所有位置參數都要放在第一個命名參數之前。例如,允許調用 f(1, y = 2) 但不允許 f(x = 1, 2)。

可以通過使用星号操作符将可變數量參數(vararg) 以命名形式傳入:

fun foo(vararg strings: String) { /* …… */ }

foo(strings = *arrayOf("a", "b", "c"))
foo(strings = "a") // 對于單個值不需要星号
           

3.其他知識點

3.1 componentX (多聲明)

val f1 = Forecast(Date(), f, "Shinny")
val (date, temperature, details) = f1
//=======================
// 上面的多聲明會被編譯成下面的代碼
val date = f1.component1()
val temperature = f1.component2()
val details = f1.copmponent3()
// 映射對象的每一個屬性到一個變量中,這就是 多聲明。
// object class 預設具有該屬性。但普通 class 想要具有這種屬性,需要這樣做:
class person(val name: String, val age: Int) {
    operator fun component1(): String {
        return name
    }
    operator fun component2(): Int {
        return age
    }
}
           

val 必須有: 用來儲存在 component1 和 component2 中傳回構造函數傳進來的參數的。

operator 暫時還不明真相,IDE 提示的。 操作符重載,函數名為操作符名(即系統預設的關鍵詞,此處為 component1,component2).當使用該操作時,自己重寫的操作會覆寫系統預設的操作。

// 常見用法:該特性功能強大,可以極大的簡化代碼量。 如 map 中的擴充函數實作,允許在疊代時使用 key value
for ((key, value) in map) {
    Log.d("map","key:$key, value:$value")
}
           

3.2 inline (内聯函數)

内聯函數與普通的函數有點不同。一個内聯函數會在編譯的時候被替換掉,而不是真正的方法調用。這在譯寫情況下可以減少記憶體配置設定和運作時開銷。例如,有一函數隻接收一個函數作為它的參數。如果是普通函數,内部會建立一個含有那個函數的對象。而内聯函數會把我們調用這個函數的地方替換掉,是以它不需要為此生成一個内部的對象。

// 例一、建立代碼塊隻提供 Lollipop 或更高版本來執行
inline fun supportsLollipop(code: () -> Unit) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        code()
    }
}
// usage
supportsLollipop {
    window.setStatusBarColor(Color.BLACK)
}
           

3.3 Application 單例化和屬性的 Delegated (by)

class App : Application() {
      companion object {
          private var instance: Application? = null
          fun  instance() = instance!!
      }
      override fun onCreate() {
          super.onCreate()
          instance = this
      }
  }
           

我們可能需要一個屬性具有一些相同的行為,使用 lazy 或 observable 可以被很有趣的實作重用,而不是一次又一次的去聲明那些相同的代碼。kotlin 提供了一個委托屬性到一個類的方法。這就是委托屬性。

class Delegate<T> : ReadWriteProperty<Any?, T> {
      fun getValue(thisRef: Any?, property: KProperty<*>): T {
            return ...
      }
      fun setValue(thisRef: Any?,property: KProperty<*>, value: T) {...}  
      // 如果該屬性是不可修改(val), 就會隻有一個 getValue 函數
  }
           

3.4 Not Null

場景1:需要在某些地方初始化該屬性,但不能在構造函數中确定,或不能在構造函數中做任何事。

場景2:在 Activity fragment service receivers…中,一個非抽象的屬性在構造函數執行之前需要被指派。

解決方案1:使用可 null 類型并且指派為 null,直到真正去指派。但是,在使用時就需要不停的進行 not null 判斷。

解決方案2:使用 notnull 委托。含有一個可 null 的變量并會在設定該屬性時配置設定一個真實的值。如果該值在被擷取之前沒有被配置設定,它就會抛出一個異常。

class App : Application() {
  companion object {
    var instance: App by Delegates.notnull()
  }
   override fun onCreate() {
      super.onCreate()
      instance = this
    }
}
           

3.5 從 Map 中映射值

另一種委托方式,屬性的值會從一個map中擷取 value,屬性的名字對應這個map 中的 key。

import kotlin.properties.getValue
class Configuration(map: Map<String,Any?>) {
  val width: Int by map
  val height: Int by map
  val dp: Int by map
  val deviceName: String by map
}
// usage
conf = Configuration(mapof(
  "width" to ,
  "height" to ,
  "dp" to ,
   "deviceName" to "myDecive"
)) 
           

3.6 custom delegate

自定義委托需要實作 ReadOonlyProperty / ReadWriteProperty 兩個類,具體取決于被委托的對象是 val 還是 var。

// step1
private class NotNullSingleValueVar<T>() : ReadWriteProperty<Any?, T> {
  private var value: T? = null
  override fun getValue(thisRef: Any?, property: KProperty<*>): T {
      return value ?: throw IllegalStateException("${desc.name not initialized}")
  }

  override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
      this.value = if (this.value == null) value else throw IllegalStateException("${desc.name} already initialized")
      }
}
// step2: usage
object DelegatesExt {
  fun notNullSingleValue<T>(): ReadWriteProperty<Any?, T> = NotNullSingleValueVar()
}
           

3.7 重新實作 Application 單例

class App : Application() {
    companion object {
        var instance: App by Delegates.notNull()
    }

    override fun onCreate() {
        super.onCreate()
        instance = this
    }
}  
// 此時可以在 app 的任何地方修改這個值,因為**如果使用 Delegates.notNull(), 
//屬性必須是 var 的。可以使用剛剛建立的委托,隻能修改該值一次
companion object {
    var instance: App by DeleagesExt.notNullSingleValue()
           

4.使用協程别導錯包

import org.jetbrains.anko.coroutines.experimental.Ref
import org.jetbrains.anko.coroutines.experimental.asReference
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
fun loadAndShowData() {
// Ref<T> uses the WeakReference under the hood
val ref: Ref<MainActivity> = this.asReference()

async(CommonPool) {
val data = getData()

// Use ref() instead of [email protected]
launch(UI) {
 ref().asyncOverlay()
 }
}
}


fun getData(): Data { ... } 
fun showData(data: Data) { ... } 
async(UI) {
   val data: Deferred<Data> = bg {
      // Runs in background
      getData() 
}
 // This code is executed on the UI thread 
showData(data.await()) 
}