一篇文章帶你瞭解——Kotlin協程

南方吳彥祖_藍斯發表於2021-10-30

前言

本文的定位是協程的稍微深入的全面知識,也會示例一些簡單的使用,這裡不對 suspend講解,,也不對協程的高階用法做闡述( 熱資料通道Channel冷資料流Flow.…),本文主要講協程稍微深入的全面知識。

Kotlin Coroutine 簡介

Kotlin 中的協程提供了一種全新處理併發的方式,您可以在  Android 平臺上使用它來簡化非同步執行的程式碼。協程是從  Kotlin 1.3 版本開始引入,但這一概念在程式設計世界誕生的黎明之際就有了,最早使用協程的程式語言可以追溯到  1967 年的  Simula 語言。

在過去幾年間,協程這個概念發展勢頭迅猛,現已經被諸多主流程式語言採用,比如  JavascriptC#Python、Ruby 以及  Go 等。 Kotlin 的協程是基於來自其他語言的既定概念。

在  Android 平臺上,協程主要用來解決兩個問題:

  • 處理耗時任務 (Long running tasks),這種任務常常會阻塞住主執行緒;
  • 保證主執行緒安全 (Main-safety) ,即確保安全地從主執行緒呼叫任何  suspend 函式。

Kotlin Coroutine Version

Kotlin Version: 1.4.32

Coroutine Version: 1.4.3

Kotlin Coroutine 生態

kotlin的協程實現分為了兩個層次:

  • 基礎設施層:

    標準庫的協程API,主要對協程提供了概念和語義上最基本的支援

  • 業務框架層 kotlin.coroutines:

    協程的上層框架支援,也是我們日常開發使用的庫

接入Coroutine

dependencies {
    // Kotlin
    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
    // 協程核心庫
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
    // 協程Android支援庫
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
    // 協程Java8支援庫
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.3"
    // lifecycle對於協程的擴充套件封裝
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
}

Coroutine 基本使用

suspend

Suspend function 掛起函式,的分析,基於 Coroutine 1.3 版本的原始碼。

簡單用法

一個簡單的例子如下:

fun test1() {
    GlobalScope.launch {
        val arg1 = sunpendF1()
        var arg2 = sunpendF2()
        doLog("suspend finish arg1:$arg1  arg2:$arg2  result:${arg1 + arg2}")
    }
}
private suspend fun sunpendF1(): Int {
    delay(1000)
    doLog("suspend fun 1")
    return 2
}
private suspend fun sunpendF2(): Int {
    delay(1000)
    doLog("suspend fun 2")
    return 4
}

最終控制檯的輸入結果如下:

01-07 19:21:49.626 9616-10074/com.yy.yylite.kotlinshare I/suspend: suspend fun 1
01-07 19:21:50.633 9616-10074/com.yy.yylite.kotlinshare I/suspend: suspend fun 2
    suspend finish arg1:2  arg2:4  result:6

也就是說,最後的輸出依賴,sunpendF1() sunpendF2,只有等兩個函式都做完了,才會輸出,但是掛起並不是阻塞。

Suspend 掛起函式原理

掛起函式,並不會阻塞執行緒,這裡的實現原理背後是 kotlin 進行了巧妙的編譯層次的設計,通常來說一個掛起函式如下:

suspend fun test1() {
    KLog.i("test1") { "test1" }
    val homeItemInfo = HomeItemInfo()
    homeItemInfo.adId = "89"
    delay(100)
    KLog.i("test1") { "test1-end" }
}

我們使用 tools->show kotlin bytecode->decompile 檢視 轉化為 java 實現程式碼如下:

public final Object test1(@NotNull Continuation var1) {
   Object $continuation;
   label28: {
      if (var1 instanceof <undefinedtype>) {
         $continuation = (<undefinedtype>)var1;
         if ((((<undefinedtype>)$continuation).getLabel() & Integer.MIN_VALUE) != 0) {
            ((<undefinedtype>)$continuation).setLabel(((<undefinedtype>)$continuation).getLabel() - Integer.MIN_VALUE);
            break label28;
         }
      }
      $continuation = new CoroutineImpl(var1) {
         // $FF: synthetic field
         Object data;
         // $FF: synthetic field
         Throwable exception;
         Object L$0;
         Object L$1;
         @Nullable
         public final Object doResume(@Nullable Object data, @Nullable Throwable throwable) {
            this.data = data;
            this.exception = throwable;
            super.label |= Integer.MIN_VALUE;
            return SuspendTest.this.test1(this);
         }
         // $FF: synthetic method
         final int getLabel() {
            return super.label;
         }
         // $FF: synthetic method
         final void setLabel(int var1) {
            super.label = var1;
         }
      };
   }
   Object var3 = ((<undefinedtype>)$continuation).data;
   Throwable var4 = ((<undefinedtype>)$continuation).exception;
   Object var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
   HomeItemInfo homeItemInfo;
   switch(((<undefinedtype>)$continuation).getLabel()) {
   case 0:
      if (var4 != null) {
         throw var4;
      }
      KLog.INSTANCE.i("test1", (Function0)null.INSTANCE);
      homeItemInfo = new HomeItemInfo();
      homeItemInfo.adId = "89";
      ((<undefinedtype>)$continuation).L$0 = this;
      ((<undefinedtype>)$continuation).L$1 = homeItemInfo;
      ((<undefinedtype>)$continuation).setLabel(1);
      if (DelayKt.delay(100, (Continuation)$continuation) == var6) {
         return var6;
      }
      break;
   case 1:
      homeItemInfo = (HomeItemInfo)((<undefinedtype>)$continuation).L$1;
      SuspendTest var7 = (SuspendTest)((<undefinedtype>)$continuation).L$0;
      if (var4 != null) {
         throw var4;
      }
      break;
   default:
      throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
   }
   KLog.INSTANCE.i("test1", (Function0)null.INSTANCE);
   return Unit.INSTANCE;
}

首先來看函式簽名,這裡會變成傳入一個引數 Continuation,這個是 kotlin 中實現掛起的介面,其原始碼定義如下,也就是最終掛起函式的邏輯,是透過這個 resumeWith() 分發的。

public interface Continuation<in T> {
    /**
     * Context of the coroutine that corresponds to this continuation.
     */
    // todo: shall we provide default impl with EmptyCoroutineContext?
    public val context: CoroutineContext
    /**
     * Resumes the execution of the corresponding coroutine passing successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

所以實際上 掛起函式在編譯器的封裝下,變成了一個 Continuation。透過位元組碼工具,我們可以看到:

kotlinx/coroutines/Deferred.delay (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

實際上這是一個 Continuation 物件。

所以實際上其流程等於:

建立協程

建立協程的方式有很多種,這裡不延伸協程的高階用法( 熱資料通道Channel冷資料流Flow.…),也許以後會在文章裡補充或者新寫文章來專門講解,建立協程這裡介紹常用的兩種方式:

  • CoroutineScope.launch()
  • CoroutineScope.async()
      這是常用的協程建立方式, launch 構建器適合執行 “一勞永逸” 的工作,意思就是說它可以啟動新協程而不將結果返回給呼叫方; async 構建器可啟動新協程並允許您使用一個名為  await 的掛起函式返回  result。  launch 和  async 之間的很大差異是它們對異常的處理方式不同。如果使用  async 作為最外層協程的開啟方式,它期望最終是透過呼叫  await 來獲取結果 (或者異常),所以預設情況下它不會丟擲異常。這意味著如果使用  async 啟動新的最外層協程,而不使用 await,它會靜默地將異常丟棄。

CoroutineScope.launch()

直接上程式碼:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
    /**
     * 使用官方庫的 MainScope()獲取一個協程作用域用於建立協程
     */
    private val mScope = MainScope()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 建立一個預設引數的協程,其預設的排程模式為Main 也就是說該協程的執行緒環境是Main執行緒
        val job1 = mScope.launch {
            // 這裡就是協程體
            // 延遲1000毫秒  delay是一個掛起函式
            // 在這1000毫秒內該協程所處的執行緒不會阻塞
            // 協程將執行緒的執行權交出去,該執行緒該幹嘛幹嘛,到時間後會恢復至此繼續向下執行
            delay(1000)
        }
        // 建立一個指定了排程模式的協程,該協程的執行執行緒為IO執行緒
        val job2 = mScope.launch(Dispatchers.IO) {
            // 此處是IO執行緒模式
            // 切執行緒 將協程所處的執行緒環境切至指定的排程模式Main
            withContext(Dispatchers.Main) {
                // 現在這裡就是Main執行緒了  可以在此進行UI操作了
            }
        }
        // 下面直接看一個例子: 從網路中獲取資料  並更新UI
        // 該例子不會阻塞主執行緒
        mScope.launch(Dispatchers.IO) {
            // 執行getUserInfo方法時會將執行緒切至IO去執行
            val userInfo = getUserInfo()
            // 獲取完資料後 切至Main執行緒進行更新UI
            withContext(Dispatchers.Main) {
                // 更新UI
            }
        }
    }
    /**
     * 獲取使用者資訊 該函式模擬IO獲取資料
     * @return String
     */
    private suspend fun getUserInfo(): String {
        return withContext(Dispatchers.IO) {
            delay(2000)
            "Kotlin"
        }
    }
    override fun onDestroy() {
        super.onDestroy()
        // 取消協程 防止協程洩漏  如果使用lifecycleScope則不需要手動取消
        mScope.cancel()
    }
}

上面的程式碼中,給出了一些程式碼示例,其實協程的簡單使用非常簡單,你甚至完全不需要擔心其他的東西,你只需要記得及時取消協程就ok,如果你使用 lifecycleScope或者 viewModelScope你連取消都不用自己管,介面或 ViewModel被銷燬時,會自動幫你把協程取消掉。使用協程只需要會建立、會切執行緒、懂四種排程模式,基本就ok了,基本開發已滿足。

CoroutineScope.async()

async主要用於獲取返回值和併發,直接上程式碼:

fun asyncTest() {
    mScope.launch {
        // 開啟一個IO模式的執行緒 並返回一個Deferred,Deferred可以用來獲取返回值
        // 程式碼執行到此處時會新開一個協程 然後去執行協程體  父協程的程式碼會接著往下走
        val deferred = async(Dispatchers.IO) {
            // 模擬耗時
            delay(2000)
            // 返回一個值
            "Quyunshuo"
        }
        // 等待async執行完成獲取返回值 此處並不會阻塞執行緒  而是掛起 將執行緒的執行權交出去
        // 等到async的協程體執行完畢後  會恢復協程繼續往下執行
        val date = deferred.await()
    }
}

上面的程式碼主要展示 async的返回值功能,需要與 await()掛起函式結合使用

下面展示 async的併發能力:

fun asyncTest2() {
    mScope.launch {
        // 此處有一個需求  同時請求5個介面  並且將返回值拼接起來
        val job1 = async {
            // 請求1 
            delay(5000)
            "1"
        }
        val job2 = async {
            // 請求2 
            delay(5000)
            "2"
        }
        val job3 = async {
            // 請求3 
            delay(5000)
            "3"
        }
        val job4 = async {
            // 請求4 
            delay(5000)
            "4"
        }
        val job5 = async {
            // 請求5
            delay(5000)
            "5"
        }
        // 程式碼執行到此處時  5個請求已經同時在執行了
        // 等待各job執行完 將結果合併
        Log.d(
            "TAG",
            "asyncTest2: ${job1.await()} ${job2.await()} ${job3.await()} ${job4.await()} ${job5.await()}"
        )
        // 因為我們設定的模擬時間都是5000毫秒  所以當job1執行完時  其他job也均執行完成
    }
}

上面的程式碼就是一個簡單的併發示例,是不是感覺十分的簡單,協程的優勢立馬凸顯出來了。

這就是最基本的協程使用,關於作用域,更推薦的是在UI元件中使用 LifecycleOwner.lifecycleScope,在 ViewModel中使用 ViewModel.viewModelScope

Coroutine的深入

其實簡單的使用,就已經滿足大部分日常開發需求,但是我們有必要全面瞭解一下 Coroutine,以便能夠排查問題及自定義場景,下面我們從一個最基本的函式來切入,這個函式就是 launch{}

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,	// 協程上下文
    start: CoroutineStart = CoroutineStart.DEFAULT,			// 協程啟動模式
    block: suspend CoroutineScope.() -> Unit						// 執行在協程的邏輯
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

上面是 launch函式的定義,它以 CoroutineScope的擴充套件函式的形成出現,函式引數分別是: 協程上下文CoroutineContext協程啟動模式CoroutineStart協程體,返回值是 協程例項Job,其中 CoroutineContext又包括了 JobCoroutineDispatcherCoroutineName。下面我們就一一介紹這些內容: CoroutineContextJobCoroutineDispatcherCoroutineStartCoroutineScope

CoroutineContext - 協程上下文

CoroutineContext即協程的上下文,是 Kotlin 協程的一個基本結構單元。巧妙的運用協程上下文是至關重要的,以此來實現正確的執行緒行為、生命週期、異常以及除錯。它包含使用者定義的一些資料集合,這些資料與協程密切相關。它是一個有索引的  Element 例項集合。這個有索引的集合類似於一個介於  set 和 map之間的資料結構。每個  element 在這個集合有一個唯一的 Key 。當多個  element 的 key 的引用相同,則代表屬於集合裡同一個  element。它由如下幾項構成:

  • Job: 控制協程的生命週期;
  • CoroutineDispatcher: 向合適的執行緒分發任務;
  • CoroutineName: 協程的名稱,除錯的時候很有用;
  • CoroutineExceptionHandler: 處理未被捕捉的異常。

CoroutineContext 有兩個非常重要的元素 —  Job 和  DispatcherJob 是當前的  Coroutine 例項而  Dispatcher 決定了當前  Coroutine 執行的執行緒,還可以新增 CoroutineName,用於除錯,新增  CoroutineExceptionHandler 用於捕獲異常,它們都實現了 Element介面。看一個例子:

fun main() {
    val coroutineContext = Job() + Dispatchers.Default + CoroutineName("myContext")
    println("$coroutineContext,${coroutineContext[CoroutineName]}")
    val newCoroutineContext = coroutineContext.minusKey(CoroutineName)
    println("$newCoroutineContext")
}

輸出結果如下:

[JobImpl{Active}@7eda2dbb, CoroutineName(myContext), Dispatchers.Default],CoroutineName(myContext)
[JobImpl{Active}@7eda2dbb, Dispatchers.Default]

CoroutineContext介面的定義如下:

public interface CoroutineContext {
    public operator fun <E : Element> get(key: Key<E>): E?
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R
    public operator fun plus(context: CoroutineContext): CoroutineContext{...}
    public fun minusKey(key: Key<*>): CoroutineContext
    public interface Key<E : Element>
    public interface Element : CoroutineContext {...}
}

CoroutineContext 定義了四個核心的操作:

  • 運算子get

    可以透過  key 來獲取這個  Element。由於這是一個  get 運算子,所以可以像訪問 map 中的元素一樣使用  context[key] 這種中括號的形式來訪問。

  • 運算子 plus

    和  Set.plus 擴充套件函式類似,返回一個新的  context 物件,新的物件裡面包含了兩個裡面的所有  Element,如果遇到重複的(Key 一樣的),那麼用 +號右邊的  Element 替代左邊的。 + 運算子可以很容易的用於結合上下文,但是有一個很重要的事情需要小心 —— 要注意它們結合的次序,因為這個  + 運算子是不對稱的。

  • fun fold(initial: R, operation: (R, Element) -> R): R

    和  Collection.fold 擴充套件函式類似,提供遍歷當前  context 中所有  Element 的能力。

  • fun minusKey(key: Key<*>): CoroutineContext

    返回一個上下文,其中包含該上下文中的元素,但不包含具有指定 key的元素。

某些情況需要一個上下文不持有任何元素,此時就可以使用  EmptyCoroutineContext 物件。可以預見,新增這個物件到另一個上下文不會對其有任何影響。

在任務層級中,每個協程都會有一個父級物件,要麼是  CoroutineScope 或者另外一個  coroutine。然而,實際上協程的父級  CoroutineContext 和父級協程的  CoroutineContext 是不一樣的,因為有如下的公式:

父級上下文 = 預設值 + 繼承的 CoroutineContext + 引數

其中:

  • 一些元素包含預設值: Dispatchers.Default 是預設的 CoroutineDispatcher,以及 “coroutine” 作為預設的 CoroutineName;
  • 繼承的 CoroutineContext 是 CoroutineScope 或者其父協程的 CoroutineContext;
  • 傳入協程 builder 的引數的優先順序高於繼承的上下文引數,因此會覆蓋對應的引數值。

請注意:  CoroutineContext 可以使用 " + " 運算子進行合併。由於  CoroutineContext 是由一組元素組成的,所以加號右側的元素會覆蓋加號左側的元素,進而組成新建立的  CoroutineContext。比如, (Dispatchers.Main, "name") + (Dispatchers.IO) = (Dispatchers.IO, "name")。

Job & Deferred - 任務

Job 用於處理協程。對於每一個所建立的協程 (透過  launch 或者 async),它會返回一個  Job例項,該例項是協程的唯一標識,並且負責管理協程的生命週期

CoroutineScope.launch 函式返回的是一個  Job 物件,代表一個非同步的任務。 Job 具有生命週期並且可以取消。  Job 還可以有層級關係,一個 Job可以包含多個子 Job,當父 Job被取消後,所有的子 Job也會被自動取消;當子 Job被取消或者出現異常後父 Job也會被取消。

除了透過  CoroutineScope.launch 來建立 Job物件之外,還可以透過  Job() 工廠方法來建立該物件。預設情況下,子 Job的失敗將會導致父 Job被取消,這種預設的行為可以透過  SupervisorJob 來修改。

具有多個子  Job 的父 Job 會等待所有子 Job完成(或者取消)後,自己才會執行完成

Job 的狀態

一個任務可以包含一系列狀態: 新建立 ( New)、活躍 ( Active)、完成中 ( Completing)、已完成 (Completed)、取消中 ( Cancelling) 和已取消 ( Cancelled)。雖然我們無法直接訪問這些狀態,但是我們可以訪問  Job 的屬性:  isActiveisCancelled 和  isCompleted

如果協程處於活躍狀態,協程執行出錯或者呼叫  job.cancel() 都會將當前任務置為取消中 ( Cancelling) 狀態 ( isActive = false, isCancelled = true)。當所有的子協程都完成後,協程會進入已取消 ( Cancelled) 狀態,此時  isCompleted = true

State [isActive] [isCompleted] [isCancelled]
New (optional initial state) false false false
Active (default initial state) true false false
Completing (transient state) true false false
Cancelling (transient state) false false true
Cancelled (final state) false true true
Completed (final state) false true false
                                      wait children
+-----+ start  +--------+ complete   +-------------+  finish  +-----------+
| New | -----> | Active | ---------> | Completing  | -------> | Completed |
+-----+        +--------+            +-------------+          +-----------+
                 |  cancel / fail       |
                 |     +----------------+
                 |     |
                 V     V
             +------------+                           finish  +-----------+
             | Cancelling | --------------------------------> | Cancelled |
             +------------+                                   +-----------+

Job 的常用函式

這些函式都是執行緒安全的,所以可以直接在其他  Coroutine 中呼叫。

  • fun start(): Boolean

    呼叫該函式來啟動這個  Coroutine,如果當前  Coroutine 還沒有執行呼叫該函式返回  true,如果當前  Coroutine 已經執行或者已經執行完畢,則呼叫該函式返回  false

  • fun cancel(cause: CancellationException? = null)

    透過可選的取消原因取消此作業。 原因可以用於指定錯誤訊息或提供有關取消原因的其他詳細資訊,以進行除錯。

  • fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle

    透過這個函式可以給  Job 設定一個完成通知,當  Job 執行完成的時候會 同步執行這個通知函式。 回撥的通知物件型別為: typealias CompletionHandler = (cause: Throwable?) -> UnitCompletionHandler 引數代表了  Job 是如何執行完成的。  cause 有下面三種情況:

    • 如果  Job 是正常執行完成的,則  cause 引數為  null
    • 如果  Job 是正常取消的,則  cause 引數為  CancellationException 物件。這種情況不應該當做錯誤處理,這是任務正常取消的情形。所以一般不需要在錯誤日誌中記錄這種情況。
    • 其他情況表示  Job 執行失敗了。

    這個函式的返回值為  DisposableHandle 物件,如果不再需要監控  Job 的完成情況了, 則可以呼叫  DisposableHandle.dispose 函式來取消監聽。如果  Job 已經執行完了, 則無需呼叫  dispose 函式了,會自動取消監聽。

  • suspend fun join()

join 函式和前面三個函式不同,這是一個  suspend 函式。所以只能在 Coroutine 內呼叫。

這個函式會暫停當前所處的  Coroutine直到該 Coroutine執行完成。所以  join 函式一般用來在另外一個  Coroutine 中等待  job 執行完成後繼續執行。當  Job 執行完成後,  job.join 函式恢復,這個時候  job 這個任務已經處於完成狀態了,而呼叫  job.join 的  Coroutine 還繼續處於  activie 狀態。

請注意,只有在其所有子級都完成後,作業才能完成

該函式的掛起是可以被取消的,並且始終檢查呼叫的 CoroutineJob是否取消。如果在呼叫此掛起函式或將其掛起時,呼叫 CoroutineJob被取消或完成,則此函式將引發  CancellationException

Deferred

public interface Deferred<out T> : Job {
    public val onAwait: SelectClause1<T>
    public suspend fun await(): T
    @ExperimentalCoroutinesApi
    public fun getCompleted(): T
    @ExperimentalCoroutinesApi
    public fun getCompletionExceptionOrNull(): Throwable?
}

透過使用 async建立協程可以得到一個有返回值 DeferredDeferred 介面繼承自  Job 介面,額外提供了獲取  Coroutine 返回結果的方法。由於  Deferred 繼承自 Job 介面,所以  Job 相關的內容在  Deferred 上也是適用的。 Deferred 提供了額外三個函式來處理和 Coroutine執行結果相關的操作。

  • suspend fun await(): T

    用來等待這個 Coroutine執行完畢並返回結果。

  • fun getCompleted(): T

    用來獲取 Coroutine執行的結果。如果 Coroutine還沒有執行完成則會丟擲 IllegalStateException ,如果任務被取消了也會丟擲對應的異常。所以在執行這個函式之前,可以透過  isCompleted 來判斷一下當前任務是否執行完畢了。

  • fun getCompletionExceptionOrNull(): Throwable?

    獲取已完成狀態的 Coroutine異常資訊,如果任務正常執行完成了,則不存在異常資訊,返回null。如果還沒有處於已完成狀態,則呼叫該函式同樣會丟擲  IllegalStateException,可以透過  isCompleted 來判斷一下當前任務是否執行完畢了。

SupervisorJob

SupervisorJob 是一個頂層函式,定義如下:

/**
 * Creates a _supervisor_ job object in an active state.
 * Children of a supervisor job can fail independently of each other.
 * 
 * A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children,
 * so a supervisor can implement a custom policy for handling failures of its children:
 *
 * * A failure of a child job that was created using [launch][CoroutineScope.launch] can be handled via [CoroutineExceptionHandler] in the context.
 * * A failure of a child job that was created using [async][CoroutineScope.async] can be handled via [Deferred.await] on the resulting deferred value.
 *
 * If [parent] job is specified, then this supervisor job becomes a child job of its parent and is cancelled when its
 * parent fails or is cancelled. All this supervisor's children are cancelled in this case, too. The invocation of
 * [cancel][Job.cancel] with exception (other than [CancellationException]) on this supervisor job also cancels parent.
 *
 * @param parent an optional parent job.
 */
@Suppress("FunctionName")
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)

該函式建立了一個處於  active 狀態的 supervisor job。如前所述,  Job 是有父子關係的,如果子 Job 失敗了父 Job會自動失敗,這種預設的行為可能不是我們期望的。比如在  Activity 中有兩個子 Job分別獲取一篇文章的評論內容和作者資訊。如果其中一個失敗了,我們並不希望父 Job自動取消,這樣會導致另外一個子Job也被取消。而 SupervisorJob就是這麼一個特殊的  Job,裡面的子 Job不相互影響,一個子 Job失敗了,不影響其他子 Job的執行。 SupervisorJob(parent:Job?) 具有一個 parent引數,如果指定了這個引數,則所返回的  Job 就是引數  parent 的子 Job。如果  Parent Job 失敗了或者取消了,則這個  Supervisor Job 也會被取消。當  Supervisor Job 被取消後,所有  Supervisor Job 的子 Job也會被取消。

MainScope() 的實現就使用了  SupervisorJob 和一個  Main Dispatcher

/**
 * Creates the main [CoroutineScope] for UI components.
 *
 * Example of use:
 * ```
 * class MyAndroidActivity {
 *     private val scope = MainScope()
 *
 *     override fun onDestroy() {
 *         super.onDestroy()
 *         scope.cancel()
 *     }
 * }
 * ```
 *
 * The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
 * If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator:
 * `val scope = MainScope() + CoroutineName("MyActivity")`.
 */
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

但是 SupervisorJob是很容易被誤解的,它和協程異常處理、子協程所屬 Job型別還有域有很多讓人混淆的地方,具體異常處理可以看Google的這一篇文章: 協程中的取消和異常 | 異常處理詳解

CoroutineDispatcher - 排程器

CoroutineDispatcher 定義了 Coroutine 執行的執行緒。 CoroutineDispatcher 可以限定  Coroutine 在某一個執行緒執行、也可以分配到一個執行緒池來執行、也可以不限制其執行的執行緒。

CoroutineDispatcher 是一個抽象類,所有  dispatcher 都應該繼承這個類來實現對應的功能。 Dispatchers 是一個標準庫中幫我們封裝了切換執行緒的幫助類,可以簡單理解為一個執行緒池。它的實現如下:

  • Dispatchers.Default

    預設的排程器,適合處理後臺計算,是一個 CPU密集型任務排程器。如果建立  Coroutine 的時候沒有指定  dispatcher,則一般預設使用這個作為預設值。 Default dispatcher 使用一個共享的後臺執行緒池來執行裡面的任務。注意它和 IO共享執行緒池,只不過限制了最大併發數不同。

  • Dispatchers.IO

    顧名思義這是用來執行阻塞  IO 操作的,是和 Default共用一個共享的執行緒池來執行裡面的任務。根據同時執行的任務數量,在需要的時候會建立額外的執行緒,當任務執行完畢後會釋放不需要的執行緒。

  • Dispatchers.Unconfined

    由於 Dispatchers.Unconfined未定義執行緒池,所以執行的時候預設在啟動執行緒。遇到第一個掛起點,之後由呼叫 resume的執行緒決定恢復協程的執行緒。

  • Dispatchers.Main

    指定執行的執行緒是主執行緒,在 Android上就是 UI執行緒·

由於 子Coroutine 會繼承 父Coroutine 的  context,所以為了方便使用,我們一般會在  父Coroutine 上設定一個  Dispatcher,然後所有  子Coroutine 自動使用這個  Dispatcher

CoroutineStart - 協程啟動模式

  • CoroutineStart.DEFAULT:

    協程建立後立即開始排程,在排程前如果協程被取消,其將直接進入取消響應的狀態

    雖然是立即排程,但也有可能在執行前被取消

  • CoroutineStart.ATOMIC:

    協程建立後立即開始排程,協程執行到第一個掛起點之前不響應取消

    雖然是立即排程,但其將排程和執行兩個步驟合二為一了,就像它的名字一樣,其保證排程和執行是原子操作,因此協程也一定會執行

  • CoroutineStart.LAZY:

    只要協程被需要時,包括主動呼叫該協程的start、join或者await等函式時才會開始排程,如果排程前就被取消,協程將直接進入異常結束狀態

  • CoroutineStart.UNDISPATCHED:

    協程建立後立即在當前函式呼叫棧中執行,直到遇到第一個真正掛起的點

    是立即執行,因此協程一定會執行

這些啟動模式的設計主要是為了應對某些特殊的場景。業務開發實踐中通常使用 DEFAULTLAZY這兩個啟動模式就夠了

CoroutineScope - 協程作用域

定義協程必須指定其  CoroutineScope 。 CoroutineScope 可以對協程進行追蹤,即使協程被掛起也是如此。同排程程式 ( Dispatcher) 不同, CoroutineScope 並不執行協程,它只是確保您不會失去對協程的追蹤。為了確保所有的協程都會被追蹤, Kotlin 不允許在沒有使用  CoroutineScope 的情況下啟動新的協程。 CoroutineScope 可被看作是一個具有超能力的  ExecutorService 的輕量級版本。 CoroutineScope 會跟蹤所有協程,同樣它還可以取消由它所啟動的所有協程。這在  Android 開發中非常有用,比如它能夠在使用者離開介面時停止執行協程。

Coroutine 是輕量級的執行緒,並不意味著就不消耗系統資源。 當非同步操作比較耗時的時候,或者當非同步操作出現錯誤的時候,需要把這個  Coroutine 取消掉來釋放系統資源。在  Android 環境中,通常每個介面( ActivityFragment 等)啟動的  Coroutine 只在該介面有意義,如果使用者在等待  Coroutine 執行的時候退出了這個介面,則再繼續執行這個  Coroutine 可能是沒必要的。另外  Coroutine 也需要在適當的  context 中執行,否則會出現錯誤,比如在非  UI 執行緒去訪問  View。 所以  Coroutine 在設計的時候,要求在一個範圍( Scope)內執行,這樣當這個  Scope 取消的時候,裡面所有的 子 Coroutine 也自動取消。所以要使用  Coroutine 必須要先建立一個對應的  CoroutineScope

CoroutineScope 介面

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

CoroutineScope 只是定義了一個新  Coroutine 的執行  Scope。每個  coroutine builder 都是  CoroutineScope 的擴充套件函式,並且自動的繼承了當前  Scope 的  coroutineContext 。

分類及行為規則

官方框架在實現複合協程的過程中也提供了作用域,主要用以明確寫成之間的父子關係,以及對於取消或者異常處理等方面的傳播行為。該作用域包括以下三種:

  • 頂級作用域

    沒有父協程的協程所在的作用域為頂級作用域。

  • 協同作用域

    協程中啟動新的協程,新協程為所在協程的子協程,這種情況下,子協程所在的作用域預設為協同作用域。此時子協程丟擲的未捕獲異常,都將傳遞給父協程處理,父協程同時也會被取消。

  • 主從作用域

    與協同作用域在協程的父子關係上一致,區別在於,處於該作用域下的協程出現未捕獲的異常時,不會將異常向上傳遞給父協程。

除了三種作用域中提到的行為以外,父子協程之間還存在以下規則:

  • 父協程被取消,則所有子協程均被取消。由於協同作用域和主從作用域中都存在父子協程關係,因此此條規則都適用。
  • 父協程需要等待子協程執行完畢之後才會最終進入完成狀態,不管父協程自身的協程體是否已經執行完。
  • 子協程會繼承父協程的協程上下文中的元素,如果自身有相同 key的成員,則覆蓋對應的 key,覆蓋的效果僅限自身範圍內有效。

常用作用域

官方庫給我們提供了一些作用域可以直接來使用,並且 Android 的Lifecycle Ktx庫也封裝了更好用的作用域,下面看一下各種作用域

GlobalScope - 不推薦使用

public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

GlobalScope是一個單例實現,原始碼十分簡單,上下文是 EmptyCoroutineContext,是一個空的上下文,切不包含任何Job,該作用域常被拿來做示例程式碼,由於 GlobalScope 物件沒有和應用生命週期元件相關聯,需要自己管理 GlobalScope 所建立的 Coroutine,且 GlobalScope的生命週期是 process 級別的,所以一般而言我們不推薦使用 GlobalScope 來建立 Coroutine。

runBlocking{} - 主要用於測試

/**
 * Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion.
 * This function should not be used from a coroutine. It is designed to bridge regular blocking code
 * to libraries that are written in suspending style, to be used in `main` functions and in tests.
 *
 * The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes continuations
 * in this blocked thread until the completion of this coroutine.
 * See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`.
 *
 * When [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of
 * the specified dispatcher while the current thread is blocked. If the specified dispatcher is an event loop of another `runBlocking`,
 * then this invocation uses the outer event loop.
 *
 * If this blocked thread is interrupted (see [Thread.interrupt]), then the coroutine job is cancelled and
 * this `runBlocking` invocation throws [InterruptedException].
 *
 * See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging facilities that are available
 * for a newly created coroutine.
 *
 * @param context the context of the coroutine. The default value is an event loop on the current thread.
 * @param block the coroutine code.
 */
@Throws(InterruptedException::class)
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    val currentThread = Thread.currentThread()
    val contextInterceptor = context[ContinuationInterceptor]
    val eventLoop: EventLoop?
    val newContext: CoroutineContext
    if (contextInterceptor == null) {
        // create or use private event loop if no dispatcher is specified
        eventLoop = ThreadLocalEventLoop.eventLoop
        newContext = GlobalScope.newCoroutineContext(context + eventLoop)
    } else {
        // See if context's interceptor is an event loop that we shall use (to support TestContext)
        // or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
        eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
            ?: ThreadLocalEventLoop.currentOrNull()
        newContext = GlobalScope.newCoroutineContext(context)
    }
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}

這是一個頂層函式,從原始碼的註釋中我們可以得到一些資訊,執行一個新的協程並且阻塞當前可中斷的執行緒直至協程執行完成,該函式不應從一個協程中使用,該函式被設計用於橋接普通阻塞程式碼到以掛起風格( suspending style)編寫的庫,以用於主函式與測試。該函式主要用於測試,不適用於日常開發,該協程會阻塞當前執行緒直到協程體執行完成。

MainScope() - 可用於開發

/**
 * Creates the main [CoroutineScope] for UI components.
 *
 * Example of use:
 * ```
 * class MyAndroidActivity {
 *     private val scope = MainScope()
 *
 *     override fun onDestroy() {
 *         super.onDestroy()
 *         scope.cancel()
 *     }
 * }
 * ```
 *
 * The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
 * If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator:
 * `val scope = MainScope() + CoroutineName("MyActivity")`.
 */
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

該函式是一個頂層函式,用於返回一個上下文是 SupervisorJob() + Dispatchers.Main的作用域,該作用域常被使用在Activity/Fragment,並且在介面銷燬時要呼叫 fun CoroutineScope.cancel(cause: CancellationException? = null)對協程進行取消,這是官方庫中可以在開發中使用的一個用於獲取作用域的頂層函式,使用示例在官方庫的程式碼註釋中已經給出,上面的原始碼中也有,使用起來也是十分的方便。

LifecycleOwner.lifecycleScope - 推薦使用

/**
 * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
 *
 * This scope will be cancelled when the [Lifecycle] is destroyed.
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
 */
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

該擴充套件屬性是  Android 的 Lifecycle Ktx庫提供的具有生命週期感知的協程作用域,它與 LifecycleOwnerLifecycle繫結,Lifecycle被銷燬時,此作用域將被取消。這是在 Activity/Fragment中推薦使用的作用域,因為它會與當前的UI元件繫結生命週期,介面銷燬時該協程作用域將被取消,不會造成協程洩漏,相同作用的還有下文提到的 ViewModel.viewModelScope

ViewModel.viewModelScope - 推薦使用

/**
 * [CoroutineScope] tied to this [ViewModel].
 * This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
 */
val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if (scope != null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
        }

該擴充套件屬性和上文中提到的 LifecycleOwner.lifecycleScope基本一致,它是 ViewModel的擴充套件屬性,也是來自 Android 的 Lifecycle Ktx庫,它能夠在此 ViewModel銷燬時自動取消,同樣不會造成協程洩漏。該擴充套件屬性返回的作用域的上下文同樣是 SupervisorJob() + Dispatchers.Main.immediate

coroutineScope & supervisorScope

/**
 * Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend block with this scope.
 * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
 * context's [Job] with [SupervisorJob].
 *
 * A failure of a child does not cause this scope to fail and does not affect its other children,
 * so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for details.
 * A failure of the scope itself (exception thrown in the [block] or cancellation) fails the scope with all its children,
 * but does not cancel parent job.
 */
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = SupervisorCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}
/**
 * Creates a [CoroutineScope] and calls the specified suspend block with this scope.
 * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
 * the context's [Job].
 *
 * This function is designed for _parallel decomposition_ of work. When any child coroutine in this scope fails,
 * this scope fails and all the rest of the children are cancelled (for a different behavior see [supervisorScope]).
 * This function returns as soon as the given block and all its children coroutines are completed.
 * A usage example of a scope looks like this:
 *
 * ```
 * suspend fun showSomeData() = coroutineScope {
 *     val data = async(Dispatchers.IO) { // <- extension on current scope
 *      ... load some UI data for the Main thread ...
 *     }
 *
 *     withContext(Dispatchers.Main) {
 *         doSomeWork()
 *         val result = data.await()
 *         display(result)
 *     }
 * }
 * ```
 *
 * The scope in this example has the following semantics:
 * 1) `showSomeData` returns as soon as the data is loaded and displayed in the UI.
 * 2) If `doSomeWork` throws an exception, then the `async` task is cancelled and `showSomeData` rethrows that exception.
 * 3) If the outer scope of `showSomeData` is cancelled, both started `async` and `withContext` blocks are cancelled.
 * 4) If the `async` block fails, `withContext` will be cancelled.
 *
 * The method may throw a [CancellationException] if the current job was cancelled externally
 * or may throw a corresponding unhandled [Throwable] if there is any unhandled exception in this scope
 * (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope).
 */
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = ScopeCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}

首先這兩個函式都是掛起函式,需要執行在協程內或掛起函式內。 supervisorScope屬於主從作用域,會繼承父協程的上下文,它的特點就是子協程的異常不會影響父協程,它的設計應用場景多用於子協程為獨立對等的任務實體的時候,比如一個下載器,每一個子協程都是一個下載任務,當一個下載任務異常時,它不應該影響其他的下載任務。 coroutineScopesupervisorScope都會返回一個作用域,它倆的差別就是異常傳播: coroutineScope 內部的異常會向上傳播,子協程未捕獲的異常會向上傳遞給父協程,任何一個子協程異常退出,會導致整體的退出; supervisorScope 內部的異常不會向上傳播,一個子協程異常退出,不會影響父協程和兄弟協程的執行。

協程的取消和異常

普通協程如果產生未處理異常會將此異常傳播至它的父協程,然後父協程會取消所有的子協程、取消自己、將異常繼續向上傳遞。下面拿一個官方的圖來示例這個過程:

這種情況有的時候並不是我們想要的,我們更希望一個協程在產生異常時,不影響其他協程的執行,在上文中我們也提到了一些解決方案,下面我們就在實踐一下。

使用SupervisorJob

在上文中我們也對這個頂層函式做了講解,那如何使用呢?直接上程式碼:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
    /**
     * 使用官方庫的 MainScope()獲取一個協程作用域用於建立協程
     */
    private val mScope = MainScope()
    companion object {
        const val TAG = "Kotlin Coroutine"
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mScope.launch(Dispatchers.Default) {
            delay(500)
            Log.e(TAG, "Child 1")
        }
        mScope.launch(Dispatchers.Default) {
            delay(1000)
            Log.e(TAG, "Child 2")
            throw RuntimeException("--> RuntimeException <--")
        }
        mScope.launch(Dispatchers.Default) {
            delay(1500)
            Log.e(TAG, "Child 3")
        }
    }
}
列印結果:
E/Kotlin Coroutine: Child 1
E/Kotlin Coroutine: Child 2
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-3
    Process: com.quyunshuo.kotlincoroutine, PID: 24240
    java.lang.RuntimeException: --> RuntimeException <--
        at com.quyunshuo.kotlincoroutine.MainActivity$onCreate$2.invokeSuspend(MainActivity.kt:31)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
E/Kotlin Coroutine: Child 3

MainScope()我們之前提到過了,它的實現就是用了 SupervisorJob。執行結果就是***Child 2***丟擲異常後,***Child 3***正常執行了,但是程式崩了,因為我們沒有處理這個異常,下面完善一下程式碼

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    mScope.launch(Dispatchers.Default) {
        delay(500)
        Log.e(TAG, "Child 1")
    }
  	// 在Child 2的上下文新增了異常處理
    mScope.launch(Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.e(TAG, "CoroutineExceptionHandler: $throwable")
    }) {
        delay(1000)
        Log.e(TAG, "Child 2")
        throw RuntimeException("--> RuntimeException <--")
    }
    mScope.launch(Dispatchers.Default) {
        delay(1500)
        Log.e(TAG, "Child 3")
    }
}
輸出結果:
E/Kotlin Coroutine: Child 1
E/Kotlin Coroutine: Child 2
E/Kotlin Coroutine: CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--
E/Kotlin Coroutine: Child 3

這一次,程式沒有崩潰,並且異常處理的列印也輸出了,這就達到了我們想要的效果。但是要注意一個事情,這幾個子協程的父級是 SupervisorJob,但是他們再有子協程的話,他們的子協程的父級就不是SupervisorJob了,所以當它們產生異常時,就不是我們演示的效果了。我們使用一個官方的圖來解釋這個關係:

這個圖可以說是非常直觀了,還是官方?。新的協程被建立時,會生成新的  Job 例項替代  SupervisorJob

使用supervisorScope

這個作用域我們上文中也有提到,使用 supervisorScope也可以達到我們想要的效果,上程式碼:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
    companion object {
        const val TAG = "Kotlin Coroutine"
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val scope = CoroutineScope(Job() + Dispatchers.Default)
        scope.launch(CoroutineExceptionHandler { coroutineContext, throwable ->
            Log.e(TAG, "CoroutineExceptionHandler: $throwable")
        }) {
            supervisorScope {
                launch {
                    delay(500)
                    Log.e(TAG, "Child 1 ")
                }
                launch {
                    delay(1000)
                    Log.e(TAG, "Child 2 ")
                    throw  RuntimeException("--> RuntimeException <--")
                }
                launch {
                    delay(1500)
                    Log.e(TAG, "Child 3 ")
                }
            }
        }
    }
}
輸出結果:
E/Kotlin Coroutine: Child 1 
E/Kotlin Coroutine: Child 2 
E/Kotlin Coroutine: CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--
E/Kotlin Coroutine: Child 3

可以看到已經達到了我們想要的效果,但是如果將 supervisorScope換成 coroutineScope,結果就不是這樣了。最終還是拿官方的圖來展示:

結語

至此文章就已經結束,本文主要是我學習協程的一些記錄,分享出來供大家翻閱一下,大家好才是真的好。大家也可以翻閱一下官方的文章進行學習,本文雖然可能描述的不是很詳細,但是該有的細節都提到了。

後續有時間可能會出一些協程的高階用法的文章,比如協程的冷資料流Flow,這個在我們的專案裡也已經用上了,沒錯,是我引入的?。總體來說,協程簡單使用非常簡單,但是想用好,還是需要下一定的功夫去研究的,但是還是逃不過真香定律,大家趕緊學習用起來吧。

本文轉自  ,如有侵權,請聯絡刪除。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2839679/,如需轉載,請註明出處,否則將追究法律責任。

相關文章