Kotlin(android)協程中文翻譯

1004145468發表於2018-09-01

1.官方文件地址

github.com/Kotlin/kotl…

2. 協程的配置

compile "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.18"

3.開啟協程

Coroutines are experimental feature in Kotlin. You need to enable coroutines in Kotlin compiler by adding the following line to gradle.properties file。
協程是Kotlin一項實驗性的功能,你需要開啟在專案工程 gradle.properties中宣告開啟。

新增程式碼: kotin.coroutines = enable

4. 在UIContext啟動協程,可以更新UI

import kotlinx.coroutines.experimental.CommonPool  // 執行線上程池中一個協程
import kotlinx.coroutines.experimental.Unconfined // 在當前預設的協程中執行
import kotlinx.coroutines.experimental.android.UI //執行在可控制UI的協程中

 ...
 
 launch(UI) { // launch coroutine in UI context
        for (i in 10 downTo 1) { // countdown from 10 to 1 
            hello.text = "Countdown $i ..." // update text
            delay(500) // wait half a second
        }
        hello.text = "Done!"
    }
複製程式碼

5. 取消一個協程任務

We can keep a reference to the Job object that launch function returns and use it to cancel coroutine when we want to stop it.

我們能持有協程的引用,通過它去獲取協程處理後的結果或取消協程

    val job = launch(UI) { // launch coroutine in UI context
        for (i in 10 downTo 1) { // countdown from 10 to 1 
            hello.text = "Countdown $i ..." // update text
            delay(500) // wait half a second
        }
        hello.text = "Done!"
    }
    // cancel coroutine on click
   job.cancel() 
}
複製程式碼

6. 在UI執行緒中使用actor

1. 模式一: At most one concurrent job(最多執行一次)

使用情況:View防止被多次點選,造成沒必要的浪費。

fun View.onClick(action: suspend () -> Unit) {
    // launch one actor
    val eventActor = actor<Unit>(UI) {
        for (event in channel) action()
    }
    // install a listener to activate this actor
    setOnClickListener { 
        eventActor.offer(Unit)
    }
}
複製程式碼

官方解釋:

Try clicking repeatedly on a circle in this version of the code. The clicks are just ignored while the countdown animation is running. This happens because the actor is busy with an animation and does not receive from its channel. By default, an actor's mailbox is backed by RendezvousChannel, whose offer operation succeeds only when the receive is active.

在上面的這份程式碼中,進行迴圈的點選,當一次點選的倒數計時動畫進行時,後續頻繁點選事件將會被忽略。原因是因為此actor正在忙於處理該動畫,並且將receive狀態設定為unactive,當有actor.offer()時,由於此actor的receive狀態為unactive,所以事件就會被拋棄!

模式二: Event conflation(事件地合併)

Sometimes it is more appropriate to process the most recent event, instead of just ignoring events while we were busy processing the previous one. The actor coroutine builder accepts an optional capacity parameter that controls the implementation of the channel that this actor is using for its mailbox. The description of all the available choices is given in documentation of the Channel() factory function.use ConflatedChannel by passing Channel.CONFLATED capacity value.

當我們正在執行一次點選的動畫時,後續頻繁點選事件更合理的處理方式是接受在動畫結束期間的最後一次點選事件,而不是像上面那樣直接被忽視。actor協程在建立時,可接受一個引數來控制它內部的channel(也稱為信箱)的實現方式。通過傳遞引數Channel.CONFLATED來建立一個ConflatedChannel(channel的一種實現型別)

fun Node.onClick(action: suspend (MouseEvent) -> Unit) {
    // launch one actor to handle all events on this node
    val eventActor = actor<MouseEvent>(UI, capacity = Channel.CONFLATED) { // <--- Changed here
        for (event in channel) action(event) // pass event to action
    }
    // install a listener to offer events to this actor
    onMouseClicked = EventHandler { event ->
        eventActor.offer(event)
    }
}
複製程式碼

Now, if a circle is clicked while the animation is running, it restarts animation after the end of it. Just once. Repeated clicks while the animation is running are conflated and only the most recent event gets to be processed.

當一次動畫沒有結束,在這期間的點選事件都將被合併成一次,當動畫結束後,又會僅此一次的啟動該動畫!

模式三:Sequence Event(序列事件)

You can experiment with capacity parameter in the above line to see how it affects the behaviour of the code. Setting capacity = Channel.UNLIMITED creates a coroutine with LinkedListChannel mailbox that buffers all events. In this case, the animation runs as many times as the circle is clicked.

上面我們看到了通過actor來建立協程,引數capacity來控制channel(信箱)的型別。你能夠嘗試capacity的不同值來體驗它帶來的效果。比如,我們設定capacity=Channel.UNLIMITED 來建立一個內部的channel(信箱)型別為LinkedListChannel,顧名思義它能接受我們的每一次事件。在這樣的情景下,我們點選按鈕多少次,動畫將會執行多少次!

模式四: more type(通過修改capacity的型別,建立不同的channel)

7. Blocking operation(塊級操作)

官方給出的事例:

fun fib(x: Int): Int =
    if (x <= 1) 1 else fib(x - 1) + fib(x - 2)
    
fun setup(hello: Text, fab: Circle) {
    var result = "none" // the last result
    // counting animation 
    launch(UI) {
        var counter = 0
        while (true) {
            hello.text = "${++counter}: $result"
            delay(100) // update the text every 100ms
        }
    }
    // compute the next fibonacci number of each click
    var x = 1
    fab.onClick {
        result = "fib($x) = ${fib(x)}"
        x++
    }
}

複製程式碼

Issume(產生問題): Try clicking on the circle in this example. After around 30-40th click our naive computation is going to become quite slow and you would immediately see how the UI thread freezes, because the animation stops running during UI freeze.

根據上面的程式碼,如果我們快速點選30-40次,計算就會變得越來越慢,UI執行緒慢慢被凍結,動畫也就會停止執行。

Solusation(解決方案): Blocking Operation(塊級操作) The fix for the blocking operations on the UI thread is quite straightforward with coroutines. We'll convert our "blocking" fib function to a non-blocking suspending function that runs the computation in the background thread by using run function to change its execution context to CommonPool of background threads. Notice, that fib function is now marked with suspend modifier. It does not block the coroutine that it is invoked from anymore, but suspends its execution when the computation in the background thread is working

在協程中通過塊級操作直接來處理UI,我們可以使用“run”函式來改變它的執行上下文到一個後臺執行緒(CommonPool),從而將一個阻塞的“fib”函式轉化為一個不被阻塞的掛起函式。注意,此時的“fib”函式被suspend修飾,這個函式使用時不會阻塞協程,當後臺執行緒計算時,它會掛起此函式的執行,如下:

suspend fun fib(x: Int): Int = run(CommonPool) {
    if (x <= 1) 1 else fib(x - 1) + fib(x - 2)
}
複製程式碼

Note(說明):You can run this code and verify that UI is not frozen while large Fibonacci numbers are being computed. However, this code computes fib somewhat slower, because every recursive call to fib goes via run. This is not a big problem in practice, because run is smart enough to check that the coroutine is already running in the required context and avoids overhead of dispatching coroutine to a different thread again. It is an overhead nonetheless, which is visible on this primitive code that does nothing else, but only adds integers in between invocations to run. For some more substantial code, the overhead of an extra run invocation is not going to be significant.

你能執行這份程式碼,驗證在計算大數字時,Ui執行緒會不會阻塞。然而,它計算fib時會變得更慢,因為每一次回撥fib函式時都會執行run方法,但是它並不是一個大問題,因為run函式是非常智慧的,它會檢查當前管理並正在執行的協程,避免此協程在不同的執行緒中重複的執行。儘管原始程式碼中在協程中反覆的使用,但是不造成什麼影響,因為每一個協程持有的是這份程式碼的Int值,對於一些更實質性的程式碼,額外執行呼叫的開銷並不顯著。

為了不反覆的呼叫run函式,當然你也可以這樣:

suspend fun fib(x: Int): Int = run(CommonPool) {
    fibBlocking(x)
}

fun fibBlocking(x: Int): Int = 
    if (x <= 1) 1 else fibBlocking(x - 1) + fibBlocking(x - 2)
複製程式碼

8. Lifecycle and coroutine parent-child hierarchy(生命週期繫結與父子繼承關係)

(Issume: )A typical UI application has a number of elements with a lifecycle. Windows, UI controls, activities, views, fragments and other visual elements are created and destroyed. A long-running coroutine, performing some IO or a background computation, can retain references to the corresponding UI elements for longer than it is needed, preventing garbage collection of the whole trees of UI objects that were already destroyed and will not be displayed anymore.

(問題)傳統的UI應用都會包含大量擁有生命週期的元素,如視窗,介面,控制元件... 當這個元素不在需要的時候,協程可能存在它的引用,為了解決這個問題。

(solution)The natural solution to this problem is to associate a Job object with each UI object that has a lifecycle and create all the coroutines in the context of this job.

(解決辦法:)將協程與每一個控制元件元素的生命週期繫結。

interface JobHolder {
    val job: Job
}
複製程式碼
class MainActivity : AppCompatActivity(), JobHolder {
    override val job: Job = Job() // the instance of a Job for this activity

    override fun onDestroy() {
        super.onDestroy()
        job.cancel() // cancel the job when activity is destroyed
    }
 
    // the rest of code
}
複製程式碼
val View.contextJob: Job
    get() = (context as? JobHolder)?.job ?: NonCancellable
複製程式碼

這樣的話,每一個控制元件都與該控制元件所在的Activity中的Job關聯起來了,使用如下:

fun View.onClick(action: suspend () -> Unit) {
    // launch one actor as a parent of the context job
    val eventActor = actor<Unit>(contextJob + UI, capacity = Channel.CONFLATED) {
        for (event in channel) action()
    }
    // install a listener to activate this actor
    setOnClickListener {
        eventActor.offer(Unit)
    }
}
複製程式碼
官方解釋

Notice how contextJob + UI expression is used to start an actor in the above code. It defines a coroutine context for our new actor that includes the job and the UI dispatcher. The coroutine that is started by this actor(contextJob + UI) expression is going to become a child of the job of the corresponding context. When the activity is destroyed and its job is cancelled all its children coroutines are cancelled, too.

它包含兩個上下文contextJob + UI來建立一個協程actor物件,該協程將會成為contextJob的子類,當Activity被銷燬,contextJob會被取消,那麼它的子類協程都將會被取消。

9.Starting coroutine in UI event handlers without dispatch(在協程中不使用排程器)

使用排程器的情況:(預設使用)
fun setup(hello: Text, fab: Circle) {
    fab.onMouseClicked = EventHandler {
        println("Before launch")
        launch(UI) { 
            println("Inside coroutine")
            delay(100)
            println("After delay")
        } 
        println("After launch")
    }
}

Before launch
After launch
Inside coroutine
After delay

複製程式碼

However, in this particular case when coroutine is started from an event handler and there is no other code around it, this extra dispatch does indeed add an extra overhead without bringing any additional value. In this case an optional CoroutineStart parameter to launch, async and actor coroutine builders can be used for performance optimization. Setting it to the value of CoroutineStart.UNDISPATCHED has the effect of starting to execute coroutine immediately until its first suspension point as the following example shows:

在上述案例中,只有等EventHandler把事情處理完畢後,才開始協程,這種額外的排程確實增加了額外的開銷而沒有帶來任何附加價值。我們可以通過引數CoroutineStart來控制,此引數受用與launch、async、actor。設定 CoroutineStart.UNDISPATCHED 將馬上開始協程,知道遇到掛載點為止。程式碼如下:

fun setup(hello: Text, fab: Circle) {
    fab.onMouseClicked = EventHandler {
        println("Before launch")
        launch(UI, CoroutineStart.UNDISPATCHED) { // <--- Notice this change
            println("Inside coroutine")
            delay(100)                            // <--- And this is where coroutine suspends      
            println("After delay")
        }
        println("After launch")
    }
}

Before launch
Inside coroutine
After launch
After delay

複製程式碼

相關文章