Android 开发进阶:Kotlin Coroutines 使
还记得第一次听到 Coroutines
的时候,纳闷了一下,口瑞停,这是什么新的番号招式(误),之后其实也没有多在意了,好一段时间,因为一个档案的 I/O 会把 UI Thread 卡住,必须要用异步程序去处理,写 Handler Thread 可以避免,这也是最基础的方式,缺点也很明显某些时候还是避不掉,写 RX 又总觉得微妙感觉有点杀鸡用牛刀的感觉,后来看了一下决定用 Coroutines
,于是有了本篇文章。
是什么问题要解决?
functionA()
functionB()
functionC()
普通情况下,执行的顺序会是很直白的 functionA()
-> functionB()
-> functionC()
。
如果只有一个 thread ,这样很顺畅没问题。
但假如这是一个跑在 main thread 上,而 ·function A· 是需要另一个 thread 的处理结果,而该结果是需要该 thread 耗费长时间作业才可以获得的。这边姑且称为 IO thread 好了。那不就意味着 function A
得等到 IO thread 处理结束并告知结果才能继续执行 function A
乃至 function B
之后才是 function C
呢?
那在等待 function A
的时候 main thread 啥事都不能做,只能 idle 在那边动也不动。
这如果是在 Android main thread 上,这样的行为会让画面 freeze ,时间稍微长一点就会 ANR
被 OS 当作坏掉进行异常排除了。
其实这个异步问题解决方案很多,诸如自己写一个 callback ,或者自干 Handler thread 去控管或者是用 RX ,去订阅之类。某些时候显得不直观,或者使用起来麻烦,总有种杀鸡何需使用牛刀的感觉。
那有没有可能?我就写成上面那样,但是当 function A
在等待 IO thread 时,让 main thread 去做其他的事情,等到 IO thread 结束耗时处理后,再回来继续执行 function A
,function B
、 function C
呢?
是的,可以,这个解决方案便是 Coroutine
。
Coroutines
到底是什么?
Coroutines
,这个单字会被标成错字,理由是他其实是两个单字合并而成的,分别是 cooperation + routine, cooperation 意指合作,routine 意指例行作业、惯例,照这里直接翻译就会是合作式例行作业。
想到辉夜姬让人想告白提到了惯例行为,也是念作 routine
那我们看到的翻译多半会是协程、协作程序…这样讲没啥前后感,谁协助程序?协助啥程序? 总之就是满头的问号。
这里 routine 指得是程序中被呼叫的 function、method ,也就是说,我们将 function 、method 协同其他更多的 function、method 共同作业这件事情称为 Coroutines
。
协同作业听起来还是很抽象,具体协同什么呢?
这便是 Coroutines
最典型的特色,允许 method 被暂停( suspended)执行之后再回复(resumed)执行,而暂停执行的 method 状态允许被保留,复原后再以暂停时的状态继续执行。
换句话说,就是我在 main thread 执行到 function A
需要等 IO thread 耗时处理的结果,那我先暂停 function A
, 协调让出 main thread 让 main thread 去执行其他的事情,等到 IO thread 的耗时处理结束后得到结果后再回复 function A
继续执行,已得到我要的结果,这便是 Coroutines
的概念,这听起来不是很简单了呢?
事实上这个概念早在 1964 年就已经被提出了,而很多语言也都有这样的概念,只是 Android 上头类似的东西一直没有被积极推广,直到 Kotlin 成为官方语言后,Coroutines
以 Support Library 的形式被推广才又在 Android 业界流行起来。
Kotlin Coroutines
首先,因为 Kotlin 的 Coroutine
并没有包含在原有包装中,而是以 Support Library 的形式提供开发者使用,所以需要另外导入该 Library。
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
这里选用这个版本进行演示,实际中可以根据自己的需要修改版本。
那因为是在 Android 上使用的, Android 上头的 main thread 跟普通 java 有点不一样,所以还需要另一个 implementation,不然会报错。
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
导入之后就可以开始使用了。
一个简单的开始
这边我想做的是画面上有一个会倒数的 Text ,用 Coroutines
可以简单地做到
class CoroutineActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutine)
GlobalScope.launch(Dispatchers.Main) {
for (i in 10 downTo 1) {
textView.text = "count down $i ..." // update text
delay(1000)
}
textView.text = "Done!"
}
}
}
那跑起来结果就像这样:
image.png
这样如果要 Thread 有相同的结果可以写成这样:
Thread {
for (i in 10 downTo 1) {
Thread.sleep(1000)
runOnUiThread {
textView.text = "count down $i ..."
}
}
runOnUiThread {
textView.text = "Done!"
}
}.start()
这样会有什么问题就是另一个故事了,至少现在这样不会马上出现 Exception (最常见的就是使用者离开画面没多久就出现一个 Exception),不过也并不是说用 Coroutines
就不会发生这些问题,记得这些做法没有什么优劣,差别在都选择就是了。
说回 Coroutines
,那跟 Thread 一样,某些时候我们会想要临时把它停住,那 GlobalScope.launch
会回传一个 Job class 的玩意
val job: Job = GlobalScope.launch(Dispatchers.Main) {
// launch coroutine in the main thread
for (i in 10 downTo 1) { // countdown from 10 to 1
textView.text = "count down $i ..." // update text
delay(1000) // wait half a second
}
textView.text = "Done!"
}
想要把它停住的话就用 cancel
即可
job.cancel()
Scope
GlobalScope 是什么玩意?
Scope
指得是范围,Coroutines
可以作用的范围。在 Main thread 上或是 IO thread 上,又或者希望开更多的 Worker thread,然后是可以在某个控制流(e.g Activity 的生命周期)中可被控制的。
原则上,在 Kotlin 里头使用任何标记 suspend
的 method(后面会提)都会在 Scope
里面,这样才可以控制 Coroutines
的行进与存活与否。
那这边举的例子, GlobalScope
继承自 CoroutineScope
。它是 CoroutineScope
的一个实作,它的概念就是最高层级的 Coroutines
,它的作用的范围伴随着 Application 的生命周期,那其实他的概念与 Dispatch.Unconfined
相同(待会会提到),用他其实可以避免 Coroutines
被过早结束,但也要记得是,这个用法类似直接呼叫静态函数,需要注意。
那如果直接实作 CoroutineScope
呢?
class CoroutineActivity : AppCompatActivity(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = TODO("not implemented")
//...ignore
}
那会要求实作一个 CoroutineContext
,这是什么玩意?指的就是 Coroutines
作用的情景,这边可以指定他是在 Main thread 上或者就直接弄一个 Job 给他:
class CoroutineActivity : AppCompatActivity(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = job
private val job = Job()
//...ignore
}
这样 launch 的时候就会使用这个 Job 来操作了,如果没有特别定义,那这个 Job 就是跑在 Worker thread 上了,用它更新 UI 会出现 Exception
,这方面可以依据自己的需求去做调整。
不过更多时候我会希望他能够跑在 Main Thread 上,Koltinx Coroutine
有提供 CoroutineScope
的实作 - MainScrope