【原創】Kotlin Coroutine協程——1.協程是什麼

Kotlin Coroutine協程

  • Kotlin Coroutine 協程是什麼? Coroutine協程這個概念在20世紀60年代就有了,可謂久遠,Wiki 百科上也有解釋。有人說它是控制流的讓出和恢復,也有說能像線程併發處理但不會阻塞,官方說它比線程更輕量化。咱們並不須要把Kotlin Coroutine神化,它到底是什麼?

經過最近一年多閱讀文檔、使用及閱讀源碼的感覺:java

  • 運行在線程裏,實際是運行在線程池裏。
  • 由於運行在線程裏,只要使用不當,依然會存在阻塞的狀況,例如使用sleep(),或者死鎖。
  • 讓協程與new Thread()新建線程的方式來比較性能消耗,來顯得更輕量化是不厚道的。我以爲應該與Executors.newCachedThreadPool()來比才合適。
  • 讓編寫異步代碼變得容易,特別是在一個多個異步同時處理的時候。
  • 異步代碼簡單了以後,咱們能夠把UI線程解脫出來,使用更多異步風格,優化了UI性能。
  • 在UI線程和IO線程切換十分的方便。
  • 少了回調及廣播的方式來處理異步,代碼更容易閱讀。

第一個Kotlin Coroutine

實現一個從網絡請求用戶信息,而且把用戶暱稱顯示在UI上的功能: 實現第一個android

GlobalScope.launch(Dispatchers.Main) {
    val userInfo = getUserFromNetwork(userId)//網絡請求,運行在後臺
    textView.text = userInfo.name//更新UI,運行在主線程
}

suspend fun getUserFromNetwork(userId: String): String {
    ...  //具體這裏的實現忽略,下文會分析到
}
複製代碼

從上面的協程示例中看到:api

  • 網絡請求與UI寫在一個方法序列裏,沒有回調
  • 2行邏輯是運行在不一樣的線程裏的
  • getUserFromNetwork()suspend這個修飾符
  • GlobalScope.launch使用了這個方法來啓動一個協程

回調的困境

例如須要在一個列表中顯示,用戶的在線狀態及等級。bash

//須要經過網絡請求後臺api
api.getUserInfo(userId)//1.查詢用戶信息
api.getOnlineStatus(userId)//2.查詢在線狀態
api.getLevelInfo(userId)//3.查詢等級
複製代碼

3個api之間沒有依賴關係,最合理的方式應該是3個一塊兒併發請求再組裝數據。網絡

但在用回調時,要實現這樣的邏輯會變得很困難,權衡之下會把它寫成了在回調中串行執行,如此整個網絡的延時就會是原來的3倍多線程

回調的串行實現

api.getUserInfo(userId, object : Callback<UserInfo> {
    override fun onResponse(response: Response<UserInfo>) {
      val userInfo = response.body
      api.getOnlineStatus(userId, object: Callback<OnlineStatus> {
        override fun onResponse(response: Response<OnlineStatus>) {
          val onlineStatus = response.body
          api.getLevelInfo(userId, object: Callback<LevelInfo> {
            override fun onResponse(response: Response<LevelInfo>) {
              val levelInfo = response.body
              val composeInfo = compose(userInfo, onlineStatus, levelInfo)//組裝數據
              getLiveData().postValue(composeInfo)//刷新UI
            }
         }
      }
    }
})
複製代碼

Coroutine的並行實現

實現上述的業務邏輯,協程是怎麼作的?併發

GlobalScope.launch {
  //async裏的3個block是同時請求,無需等待前一個的結果
  val userInfo = async { api.getUserInfo(userId) }
  val onlineStatus = async { api.getOnlineStatus(userId) }
  val levelInfo = async { api.getLevelInfo(userId) }
  //等待3個請求所有返回了,再組裝數據
  val composeInfo = compose(userInfo.await(), onlineStatus.await(), levelInfo.await())
  getLiveData().postValue(composeInfo)//刷新UI
}
複製代碼

咱們能夠用順序的方式來讓多線程執行起來,在同步和異步之間靈活的切換。這樣的特性,可讓咱們寫出以前很難才能作出的邏輯,這是Coroutine的優點。異步

  • 使用async來啓動一個新的協程@userInfo,返回一個Deferred,調用Deferred.await()方法,此時當前協程會掛起,等待@userInfo執行結束。

runBlocking 上面已經講了2種啓動協程的方式,分別是launch,async。然而還有一種叫runBlocking的方式,在官方文檔也有寫到,同時也發現了會有同窗對這個方式存在一些使用上的誤解狀況。 當運行到這個runBlocking()的時候當前線程會被阻塞住。特別注意這個方法不該在Coroutine內部使用。根據官方文檔說明,它是被用在阻塞main線程及測試的時候使用的。不建議你們使用async

runBlocking {
    api.getUserFromNetwork(userId)
    api.getOnlineStatus(userId)
}
...//直到getUserFromNetwork運行完纔會被執行到
複製代碼

在Coroutine裏運行在不一樣的線程上

這是一個第一個示例的全版:ide

GlobalScope.launch(Dispatchers.Main) {
    val userInfo = getUserFromNetwork(userId)//網絡請求,運行在後臺
    textView.text = userInfo.name//更新UI,運行在主線程
}

suspend fun getUserFromNetwork(userId: String) = withContext(Dispatchers.IO) {
    HttpSerivce.getUser(userId)
}
複製代碼

withContext

Coroutine裏有一個withContext()的函數,它能夠指定協程在哪一個線程裏執行,並讓後續代碼等待,按順序去執行。

有了這個函數,能夠消除在切換線程時致使的Callback嵌套。

若是咱們經過啓動不一樣的協程來切換線程,代碼是長這樣的:

GlobalScope.launch(Dispatchers.IO) {
    ...
    launch(Dispatchers.Main) {
    	...
      launch(Dispatchers.IO) {
        ...
      }
    }
}
複製代碼

是否是又有一種回調的感受回來了

而使用withContext()則可讓協程擺脫上面的嵌套寫法。

GlobalScope.launch(Dispatchers.Main) {
    val result0 = withContext(Dispatchers.IO) {...}
    val result1 = withContext(Dispatchers.Main) {...}
    val result2 = withContext(Dispatchers.IO) {...}
}
複製代碼

這裏須要跟前面併發請求的狀況區分開來,使用的場景不一樣進行選擇。

suspend(掛起)

GlobalScope.launch(Dispatchers.Main) {
    val userInfo = getUserFromNetwork(userId)//網絡請求,運行在後臺
    textView.text = userInfo.name//更新UI,運行在主線程
}

suspend fun getUserFromNetwork(userId: String) = withContext(Dispatchers.IO) {
    HttpSerivce.getUser(userId)
}
複製代碼

可見,對於suspendGetUserInfo()內的邏輯運行在什麼線程裏,能夠不禁調用者決定的,能夠由實現者決定的。

咱們去設計本身的掛起函數時,若是須要在特定的線程裏,最好的方式是咱們函數內部去指定。好比操做文件讀寫的邏輯時,定義運行在Dispatchers.IO裏,這樣也不用擔憂外面會使用錯誤。

何爲掛起?!

  • 既不是函數被掛起,也不是線程被掛起,而是當前協程被掛起,正在運行這個協程的線程,從掛起的那時候開始,再也不執行這個協程了。
  • 協程執行到掛起函數的地方時,就會脫離運行它的線程,這條線程並不會阻塞,它會去幹別的事情。
  • 協程脫離後,並非指它中止了,而是等待被系統安排其它的線程在適當的時機來運行它。
launch(Dispatchers.Main) {
    val userInfo = suspendGetUserInfo(userId)
    textView.text = userInfo.name
}
suspend fun suspendGetUserInfo(userId: Long): UserInfo {
  //可切換線程IO線程執行,原來執行的Main線程將空閒執行其它工做
  return withContext(Dispatchers.IO) {
    getUserFromNetwork(userId)
  }
}
複製代碼
//在Main UI線程執行
log.debug("run in click starting")
GlobalScope.launch(Dispatchers.Main) {
    log.debug("run in launch")
    val userInfo = suspendGetUserInfo(userId)
}
log.debug("run in click finishing")
複製代碼

打印的順序是?

運行結果

D: 11:50:30.825 main: run in click starting
D: 11:50:30.869 main: run in click finishing
D: 11:50:30.872 main: run in launch
D: 11:50:30.873 main: run in suspendGetUserInfo starting
複製代碼

協程運行的時候,儘管仍是在Main裏運行,實際上也是在下個一個Main Looper時才運行到。

suspend fun suspendGetUserInfo(userId: Long): UserInfo {
    //可切換線程IO線程執行,原來執行的Main線程將空閒執行其它工做
    log.debug("run in suspendGetUserInfo starting")
    GlobalScope.launch(Dispatchers.Main) {
        log.debug("Main is available")
    }
    val userInfo = withContext(Dispatchers.IO) {
        delay(100)
        log.debug("run in withContext")
        getUserFromNetwork(userId)
    }
    log.debug("run in suspendGetUserInfo finishing")
    return userInfo
}
複製代碼

打印的順序是?

D: 12:03:06.469 main : run in suspendGetUserInfo starting
D: 12:03:06.475 main : Main is available
D: 12:03:06.582 DefaultDispatcher-worker-2 : run in withContext
D: 12:03:06.585 main : run in suspendGetUserInfo finishing
複製代碼

因爲withContext是掛起函數,已經切換到IO上執行,所以Main是空閒的,下一個Looper的時候就能夠執行launch裏的代碼。

suspend的語法規則

  • Kotlin協程規定,一個suspend方法的調用者必須是suspend方法或者是在launch()/async()/runBlocking()啓動的協程調用。
  • 協程的基礎庫有很多帶有suspend的函數,當咱們要去使用時,要留意符合上面的規則。反之,沒有用到suspend函數的地方,並不須要給本身函數加上,Android Studio也會相應的代碼提示。

其它掛起函數

除了withContext(),還有

  • 像上面所用過的await()
  • delay()表示掛起必定時間後再運行,協程裏記得不要用sleep()
  • withTimeout { }表示block執行若是超過必定的時間則會拋出TimeoutCancellationException
public suspend fun <T> withContext( context: CoroutineContext, block: suspend CoroutineScope.() -> T
): T 
複製代碼
public suspend fun delay(timeMillis: Long)
複製代碼

如何將Callback代碼改成Coroutine

以前咱們開發的過程當中若是使用了Callback的形式來處理異步邏輯時,此時咱們想把Callback的API改成Coroutine要怎麼實現呢?

suspendCancellableCoroutine/suspendCoroutine

//原來的Callback形式方法
fun saveToRepositoryCallback(callback: (Int) -> Unit) { ... }

GlobalScope.launch(Dispatchers.IO) {
  val returnValue = saveToRepository()
}

//協程的形式
suspend fun saveToRepository(): Int {
  return suspendCoroutine { continuation ->
    saveToRepositoryCallback {
      continuation.resume(it)
    }
  }
}
複製代碼

經過一個回調的參數continuation來設置返回的數據,此時就至關於把Callback改成了直接返回的形式。

以外若是須要Coroutine能拋出異常時,可使用resumeWithException來把異常拋出來。

//原來的Callback形式方法,第二個參數爲當出錯返回的異常回調
fun saveToRepositoryCallback(success: (Int) -> Unit, error: (Throwable) -> Unit) {...}

GlobalScope.launch(Dispatchers.IO) {
  try {
  	val returnValue = saveToRepository()
  } catch (t: Throwable) {
    // do something on error
  }
}

//調用該方法有可能會throw exception
suspend fun saveToRepository(): Int {
  return suspendCoroutine { continuation ->
    saveToRepositoryCallback({
      continuation.resume(it)
    }, {
      continuation.resumeWithException(it)
    })
  }
}
複製代碼

suspendCancellableCoroutine特性

使用suspendCoroutine啓動的Job,想經過cancel()來中止是不行的,依然會繼續執行

val job = GlobalScope.launch(Dispatchers.IO) {
    val returnValue = saveToRepository()
    LogUtil.debug("returnValue: $returnValue")
}
//協程的形式
suspend fun saveToRepository(): Int {
    return suspendCoroutine { continuation ->
        saveToRepositoryCallback {
            LogUtil.debug("callback: $it")
            continuation.resume(it)
        }
    }
}
//觸發取消
job.cancel()

I: [main] callback: 0
I: [DefaultDispatcher-worker-2] returnValue: 0

複製代碼

若是使用suspendCancellableCoroutine,運行returnValue的代碼則不會被運行到

val job = GlobalScope.launch(Dispatchers.IO) {
    val returnValue = saveToRepository()
    LogUtil.debug("returnValue: $returnValue")
}

suspend fun saveToRepository(): Int {
    return suspendCancellableCoroutine { continuation ->
        saveToRepositoryCallback {
            LogUtil.debug("callback: $it")
            continuation.resume(it)
        }
    }
}

I: [main] callback: 0
複製代碼

總結

對於Kotlin Coroutine來講,更像是一個跨線程工具。能夠把它當作是相似於AsyncTask,Exeutors,Handler,Rxjava之類的工具庫。 碰到下面的狀況時建議使用協程:

  • 有多個併發的任務同時進行,或者想經過併發提升性能的時候
  • 須要在UI線程和工做線程裏作切換的時候

思考

  • 協程能作到徹底避免阻塞問題嗎?
  • 比用線程輕量化嗎?

附錄

如何添加Kotlin Coroutine的依賴:在build.gradle中添加依賴庫。

buildscript {
    ext {
    	kotlin_version = '1.3.50'
    	coroutines_android_version = '1.3.2'
    }
}
dependencies {
    //依賴kotlin
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    //這是協程android的庫,同時也依賴了kotlin coroutine庫
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_android_version"
}
複製代碼

若是你們以爲這篇文章有用的話,歡迎點贊、評論、收藏、分享。

相關文章
相關標籤/搜索