Kotlin異步流

一、表示多個值

Kotlin 中可以使用集合來表示多個值

1.序列

fun  simple(): Sequence<Int> = sequence {//序列構建器
    for (i in 1..3) {
        Thread.sleep(100)
        yield(i)
    }
}

fun main() {
    simple().forEach { value -> println(value) }
}

2.掛起函數

計算過程阻塞運行該代碼的主線程。當這些值由異步代碼計算時,我們可以使用 suspend 修飾符標記函數 simple ,這樣它就可以在不阻塞的情況下執行其工作并將結果作為列表返回:

suspend fun simple(): List<Int> {
    delay(1000)
    return listOf(1, 2, 3)
}

fun main() = runBlocking {
    simple().forEach { value -> println(value) }
}

3.流

為了表式異步計算的值流(stream),我們可以使用 Flow 類型(正如同步計算值會使用 Sequence 類型):

fun simple(): Flow<Int> = flow { // 流構建器
    for (i in 1..3) {
        delay(100)
        emit(i) // 發送下一個值
    }
}

fun main() = runBlocking {
    // 啟動并發的協程以驗證主線程并未阻塞
    launch {
        for (k in 1..3) {
            println("I'm not blocked $k")
            delay(100)
        }
    }
    simple().collect { value -> println(value) }
}

輸出:
I'm not blocked 1
1
I'm not blocked 2
2
I'm not blocked 3
3

二、流是冷的

Flow 是?種類似于序列的冷流 。 flow 構建器中的代碼直到流被收集的時候才運行。

三、流取消基礎

流采用與協程同樣的協作取消。

四、流構建器

flow { ... } 構建器是最基礎的?個。還有構建器使流的聲明更簡單:

  • flowOf 構建器定義了?個發射固定值集的流。
  • 使用 .asFlow() 擴展函數,可以將各種集合與序列轉換為流。

五、過度流操作符

過渡操作符應用于上游流,并返回下游流。這些操作符也是冷操作符,就像流?樣。這類操作符本身不是掛起函數。它運行的速度很快,返回新的轉換流的定義。
流與序列的主要區別在于這些操作符中的代碼可以調用掛起函數。

suspend fun performRequest(request: Int): String {
    delay(1000)
    return "response $request"
}

fun main() = runBlocking {
    (1..3).asFlow()
        .map { request -> performRequest(request) }
        .collect { response -> println(response) }
}

輸出:

response 1
response 2
response 3

1.轉換操作符

在流轉換操作符中,最通用的?種稱為 transform。它可以用來模仿簡單的轉換,例如 map 與 filter,以及實施更復雜的轉換。使用 transform 操作符,我們可以發射任意值任意次。
使用 transform 我們可以在執行長時間運行的異步請求之前發射?個字符串并跟蹤這個響應。

suspend fun performRequest(request: Int): String {
    delay(1000)
    return "response $request"
}

fun main() = runBlocking {
    (1..3).asFlow()
        .transform { request ->
            emit("Making request $request")
            emit(performRequest(request))
        }
        .collect { response -> println(response) }
}

輸出:

Making request 1
response 1
Making request 2
response 2
Making request 3
response 3

2.限長操作符

限長過渡操作符(例如 take)在流觸及相應限制的時候會將它的執行取消。協程中的取消操作總是通過拋出異常來執行(不會在控制臺輸出對賬信息,被取消的協程中 CancellationException 被認為是協程執行結束的正常原因),不會奔潰這樣所有的資源管理函數(如 try {...} finally {...} 塊)會在取消的情況下正常運行:

fun numbers(): Flow<Int> = flow {
    try {
        emit(1)
        emit(2)
        println("This line will not execute")
        emit(3)
    } finally {
        println("Finally in numbers")
    }
}

fun main() = runBlocking {
    numbers()
        .take(2)
        .collect { value -> println(value) }
}

輸出:

1
2
Finally in numbers

六、末端流操作符

末端操作符是在流上用于啟動流收集的掛起函數。collect 是最基礎的末端操作符,還有?些其它末端操作符:

  • 轉化為各種集合,例如 toList 與 toSet
  • 獲取第?個(first)值與確保流發射單個(single)值的操作符。single空流拋出NoSuchElementException,對于包含多個元素的流拋出IllegalStateException。
  • 使用 reduce 與 fold 將流規約到單個值。
fun main() = runBlocking {
    val sum = (1..5).asFlow()
        .map { it * it }
//        .first()//輸出1
//        .reduce { a, b -> a + b }  //輸出55
          .fold(1) { a, b -> a + b } //帶初始值的累加輸出56
    println(sum)
}

七、流是連續的

流的每次單獨收集都是按順序執行的,除非進行特殊操作的操作符使用多個流。默認情況下不啟動新協程。從上游到下游每個過渡操作符都會處理每個發射出的值,然后再交給末端操作符。

八、流上下文

流的收集總是在調用協程的上下文中發生。該屬性稱為上下文保存 。所以默認的,flow { ... } 構建器中的代碼運行在相應流的收集器提供的上下文中。

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun simple(): Flow<Int> = flow {
    log("Started simple flow")
    for (i in 1..3) {
        emit(i)
    }
}

fun main() = runBlocking {
    simple().collect { value -> log("Collected $value") }
}

輸出:

[main @coroutine#1] Started simple flow
[main @coroutine#1] Collected 1
[main @coroutine#1] Collected 2
[main @coroutine#1] Collected 3

由于 simple().collect 是在主線程調用的,那么 simple 的流主體也是在主線程調用的。這是快速運行或異步代碼的理想默認形式,它不關心執行的上下文并且不會阻塞調用者。

1.withContext發出錯誤

長時間運行的消耗 CPU 的代碼也許需要在 Dispatchers.Default 上下文中執行,并且更新 UI 的代碼也許需要在 Dispatchers.Main 中執行。
通常,withContext 用于在 Kotlin 協程中改變代碼的上下文,但是 flow {...} 構建器中的代碼必須遵循上下文保存屬性,并且不允許從其他上下文中發射(emit)。

2.flowOn操作符

flowOn 函數用于更改流發射的上下文。

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        Thread.sleep(100)
        log("Emitting $i")
        emit(i)
    }
}.flowOn(Dispatchers.Default)

fun main() = runBlocking {
    simple().collect { value ->
        log("Collected $value")
    }
}

輸出:

[DefaultDispatcher-worker-1 @coroutine#2] Emitting 1
[main @coroutine#1] Collected 1
[DefaultDispatcher-worker-1 @coroutine#2] Emitting 2
[main @coroutine#1] Collected 2
[DefaultDispatcher-worker-1 @coroutine#2] Emitting 3
[main @coroutine#1] Collected 3

flowOn 操作符已改變流的默認順序性。現在收集發生在?個協程中 (“coroutine#1”)而發射發生在運行于另?個線程中與收集協程并發運行的另?個協程(“coroutine#2”)中。當上游流必須改變其上下文中的 CoroutineDispatcher 的時候,flowOn 操作符創建了另?個協程。

九、緩沖

buffer 操作符來并發運行流中發射元素的代碼以及收集的代碼,而不是順序運行它們:

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100)
        emit(i)
    }
}

fun main() = runBlocking {
    val time = measureTimeMillis {
        simple()
            .buffer()
            .collect { value ->
                delay(300)
                println(value)
            }
    }
    println("Collected in $time ms")
}

輸出:
1
2
3
Collected in 1098 ms

僅需要等待第?個數字產生的 100 毫秒以及處理每個數字各需花費的 300 毫秒

1.合并

當流代表部分操作結果或操作狀態更新時,可能沒有必要處理每個值,而是只處理最新的那個。
當收集器處理它們太慢的時候,conflate 操作符可以用于跳過中間值。

fun main() = runBlocking {
    val time = measureTimeMillis {
        simple()
            .conflate()
            .collect { value ->
                delay(300)
                println(value)
            }
    }
    println("Collected in $time ms")
}
輸出:
1
3
Collected in 779 ms

2.處理最新值

當發射器和收集器都很慢的時候,合并是加快處理速度的?種方式。它通過刪除發射值來實現。另?種方式是取消緩慢的收集器,并在每次發射新值的時候重新啟動它。有?組與 xxx 操作符執行相同基本邏輯的 xxxLatest 操作符,但是在新值產生的時候取消執行其塊中的代碼。
如collecLatest

fun main() = runBlocking {
    val time = measureTimeMillis {
        simple()
            .collectLatest { value ->
                println("Collecting $value")
                delay(300)
                println("Done $value")
            }
    }
    println("Collected in $time ms")
}
輸出:
Collecting 1
Collecting 2
Collecting 3
Done 3
Collected in 767 ms

十、組合多個流

1.zip

zip 操作符用于組合兩個流中的相關值

fun main() = runBlocking {
    val nums = (1..3).asFlow()
    val strs = flowOf("one", "two", "three")
    nums.zip(strs) { a, b -> "$a -> $b" }
        .collect { println(it) }
}

輸出:
1 -> one
2 -> two
3 -> three

2.combine

當流表示?個變量或操作的最新值時,可能需要執行計算,參考合并操作符conflate。這依賴于相應流的最新值,并且每當上游流產生值的時候都需要重新計算。這種相應的操作符家族稱為combine

fun main() = runBlocking {
    val nums = (1..3).asFlow().onEach { delay(300) }
    val strs = flowOf("one", "two", "three").onEach { delay(400) }
    val startTime = currentTimeMillis()
    nums.combine(strs) { a, b -> "$a -> $b" }
        .collect { value ->
            println("$value at ${currentTimeMillis() - startTime} ms from start")
        }
}

輸出:
1 -> one at 452 ms from start
2 -> one at 656 ms from start
2 -> two at 858 ms from start
3 -> two at 967 ms from start
3 -> three at 1265 ms from start

十一、展平流

1.flatMapConcat

連接模式由 flatMapConcat 與 flattenConcat 操作符實現。它們是相應序列操作符最相近的類似物。它們在等待內部流完成之后開始收集下?個值。

fun requestFlow(i: Int): Flow<String> = flow {
    emit("$i: First")
    delay(500)
    emit("$i: Second")
}

fun main() = runBlocking {
    val startTime = currentTimeMillis()
    (1..3).asFlow().onEach { delay(100) }
        .flatMapConcat { requestFlow(it) }
        .collect { value ->
            println("$value at ${currentTimeMillis() - startTime} ms from start")
        }
}

輸出:
1: First at 134 ms from start
1: Second at 649 ms from start
2: First at 758 ms from start
2: Second at 1270 ms from start
3: First at 1379 ms from start
3: Second at 1889 ms from start

2.flatMapMerge

另?種展平模式是并發收集所有傳入的流,并將它們的值合并到?個單獨的流,以便盡快的發射值。它由 flatMapMerge 與 flattenMerge 操作符實現。他們都接收可選的用于限制并發收集的流的個數的 concurrency 參數。

fun main() = runBlocking {
    val startTime = currentTimeMillis()
    (1..3).asFlow().onEach { delay(100) }
        .flatMapMerge { requestFlow(it) }
        .collect { value ->
            println("$value at ${currentTimeMillis() - startTime} ms from start")
        }
}

輸出:
1: First at 175 ms from start
2: First at 268 ms from start
3: First at 377 ms from start
1: Second at 685 ms from start
2: Second at 777 ms from start
3: Second at 888 ms from start

3.flatMapLatest

flatMapLatest與 collectLatest 操作符類似,在發出新流后立即取消先前流的收集。

fun main() = runBlocking {
    val startTime = currentTimeMillis()
    (1..3).asFlow().onEach { delay(100) }
        .flatMapLatest { requestFlow(it) }
        .collect { value ->
            println("$value at ${currentTimeMillis() - startTime} ms from start")
        }
}

輸出:
1: First at 160 ms from start
2: First at 334 ms from start
3: First at 445 ms from start
3: Second at 955 ms from start

十二、流異常

當運算符中的發射器或代碼拋出異常時,流收集可以帶有異常的完成。

1.收集

try 與 catch

2.透明性

流必須對異常透明,即在 flow { ... } 構建器內部的 try/catch 塊中發射值是違反異常透明性的。

透明捕獲

catch 過渡操作符遵循異常透明性,僅捕獲上游異常( catch 操作符上游的異常,但是它下面的不是)。如果 collect { ... } 塊(位于 catch 之下)拋出?個異常,那么異常會逃逸。

聲明式捕獲

可以將 catch 操作符的聲明性與處理所有異常的期望相結合,將 collect 操作符的代碼塊移動到 onEach 中,并將其放到 catch 操作符之前。收集該流必須調用無參的 collect() 。

十三、流完成

當流收集完成時(普通情況或異常情況),它可能需要執行?個動作。它可以通過兩種方式完成:命令式或聲明式。

  • 命令式 finally 塊
  • 聲明式處理,onCompletion 過渡操作符,它可以在流完全收集時調用

與 catch 操作符的另?個不同點是 onCompletion 能觀察到所有異常并且僅在上游流成功完成(沒有取消或失敗)的情況下接收?個 null 異常。

fun main() = runBlocking {
    simple()
        .onCompletion { cause -> println("Flow completed with $cause") }
        .collect { value ->
            check(value <= 1) { "Collected $value" }
            println(value)
        }
}

輸出:

1
Flow completed with java.lang.IllegalStateException: Collected 2
Exception in thread "main" java.lang.IllegalStateException: Collected 2//程序異常終止

十四、啟動流

launchIn末端操作符, 必要參數是 CoroutineScope ,指定哪個協程來啟動流的收集。

十五、流取消檢測

流構建器對每個發射值執行附加的 ensureActive 檢測以進行取消。這意味著從 flow { ... } 發出的繁忙循環是可以取消的:

fun foo(): Flow<Int> = flow {
    for (i in 1..5) {
        println("Emitting $i")
        emit(i)
    }
}

fun main() = runBlocking {
    foo().collect { value ->
        if (value == 3) cancel()
        println(value)
    }
}

輸出:

Emitting 1
1
Emitting 2
2
Emitting 3
3
Emitting 4
Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@2a742aa2//異常終止

出于性能原因,大多數其他流操作不會自動執行其他取消檢測。例如,如果使? IntRange.asFlow 擴展來編寫相同的繁忙循環。
必須明確檢測是否取消。可以添加 .onEach { currentCoroutineContext().ensureActive() } ,也可使用 cancellable 操作符來執行此操作。

fun main() = runBlocking {
    (1..5).asFlow().cancellable().collect { value ->
        if (value == 3) cancel()
        println(value)
    }
}

十六、流(flow)與響應式流(Reactive Streams)

Flow 依然是響應式流,設計上和RxJava相似。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容