Kotlin中一些知識點學習

1. 協程

github地址:kotlinx.coroutines(https://github.com/kotlin/kotlinx.coroutines

fun main(args: Array<String>) {
    launch(CommonPool) {
        delay(1000L) 
        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 = 1
        while(true) {
            println("$i little sheep")
            ++i
            delay(500L)  // 每半秒數一隻, 一秒可以輸兩隻
        }
    }

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

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

1 little sheep
2 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(10000L)  // 這個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(10000L) // 請不要關注這個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(1000L)  // 過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 <T> suspendCoroutine(block: (Continuation<T>) -> Unit): T

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

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(10000L) // 繼續無視這個sleep
}

suspend fun requestDataAsync1(): String {
    delay(1000L)
    return "data1"    
}
suspend fun requestDataAsync2(): String {
    delay(1000L)
    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 = 0, len: Int = b.size) { …… }

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

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

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

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

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

foo(baz = 1) // 使用默認值 bar = 0

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

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

foo(1) { 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)

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

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(), 27.5f, "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 1080,
  "height" to 720,
  "dp" to 240,
   "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 this@MyActivity
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()) 
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章