1.官方文档地址
https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/coroutines-guide-ui.md#android
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(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(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(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