什么是Coroutine
回答这个问题我们首先需要知道关于函数和线程
函数是什么:
带有输入的一连串的指令执行后返回一个值
线程是什么:
函数执行的上下文(这里只是针对函数角度去定义线程,当然线程也有别的定义.)
如下我们有一个线程1
Thread 1
println("Hello World")
var x = 3
x *= x
println("The result is $x")
上面一段指令我们从上到下依次执行在同一个线程
现在有一个Thread 2
println("Hello World from the 2. Thread!")
var x = 3
x *= x
println("The result from 2. Thread is $x")
这个时候我们Thread 1和Thread 2执行顺序是独立的, 互相不影响,Thread 1执行到第二行,Thread 2可能还在第一行.
我们在Android中所有的UI相关绘制都是执行在Main Thread中的,如果出现耗时操作就会阻塞UI线程的绘制
instantiateView()
updateUI()
doNetworkCall() //耗时操作
updateUI()
println("Hello from the main thread")
updateUI()
对于这种情况我们另外启动一个新线程
instantiateViews()
updateUI()
Thread {
doNetworkCall()
}.start()
updateUI()
println("Hello from the main thread")
updateUI()
我们网络请求结束回来又需要更新UI, 这个时候我们通常会使用Handler.
那么现在来回答什么是Coroutine?
它其实主要还是启动后台线程,能够解决卡UI的问题. 但是它相对于线程来说有一些区别
- 它执行在一个线程里面
- Coroutine是可以挂起的
- Coroutine支持Context切换(解决网络请求结束回来又需要更新UI的场景)
创建你的第一个Coroutine
GlobalScope.launch {
Log.d("tag", "Hello from thread ${Thread.currentThread().name}")
}
Log.d("tag", "Coroutine says hello from thread ${Thread.currentThread().name}")
运行后输出:
D Coroutine says hello from thread main
D Hello from thread DefaultDispatcher-worker-2
suspend函数
kotlin协程库中提供了一个挂起函数delay
GlobalScope.launch {
delay(1000) //挂起函数
Log.d("tag", "Hello from thread ${Thread.currentThread().name}")
}
Log.d("tag", "Coroutine says hello from thread ${Thread.currentThread().name}")
我们看到前面有suspend关键字代表了它是挂起函数
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
挂起函数特点是它只能在挂起函数或者协程中调用,下面我们自定义一个挂起函数
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch {
val answer = doNetworkCall()
Log.d("tag", answer)
}
Log.d("tag", "Coroutine says hello from thread ${Thread.currentThread().name}")
}
suspend fun doNetworkCall() : String {
delay(3000L)
return "This is the answer"
}
}
Coroutine Context
通常Coroutine是执行在一个上下文当中的,我们可以通过设置上下文中的Dispacher去指定Coroutine执行的线程.
Dispatchers是一种CoroutineContext用来指定Coroutine在哪里运行,目前分别有四种
Dispatchers.Default
它使用共享后台线程的公共池。对于消耗 CPU 资源的计算密集型协程来说,这是一个合适的选择。
Dispatchers.IO
使用按需创建线程的共享池,专为密集型阻塞操作(如文件 I/O 和阻塞套接字 I/O)而设计。
Dispatchers.Unconfined
不限定线程池,Coroutine直接执行在当前调用帧,当出现suspend的时候,执行完suspend后又会以suspend的线程继续执行后续的逻辑。(通常不使用)
另外还可以创建自己的线程池执行
通过newSingleThreadContext 和 newFixedThreadPoolContext.
使用withContext可以切换上下文,用来在后台执行网络请求后刷新UI
GlobalScope.launch(Dispatchers.IO) {
val answer = doNetworkCall()
withContext(Dispatchers.Main) {
Log.d("tag", "update ui $answer")
}
}
runBlocking
runBlocking会运行一个阻塞当前线程运行的协程.
我们同样在主线程中调用GlobalScope.launch(Dispatchers.Main)和runBlocking有一些区别,如下
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch(Dispatchers.Main) {
}
runBlocking {
}
}
GlobalScope.launch虽然也是启动一个协程在UI线程中运行,但是它不会阻塞后续命令的执行, runBlocking就会直接阻塞.
我们直接看下面一段程序
Log.d("tag", "Before runBlocking")
runBlocking {
Log.d("tag", "Start of runBlocking")
delay(5000)
Log.d("tag", "End of runBlocking")
}
Log.d("tag", "after runBlocking")
最终输出是
D Before runBlocking
D Start of runBlocking
D End of runBlocking
D after runBlocking
从上面输出我们可以看出来runBlocking阻塞住了.
我们还可以在runBlocking中启动协程
Log.d("tag", "Before runBlocking")
runBlocking {
launch(Dispatchers.IO) {
delay(3000L)
Log.d("tag","Finished IO Coroutine 1")
}
launch(Dispatchers.IO) {
delay(3000L)
Log.d("tag","Finished IO Coroutine 2")
}
Log.d("tag", "Start of runBlocking")
delay(5000)
Log.d("tag", "End of runBlocking")
}
Log.d("tag", "after runBlocking")
输出:
D Before runBlocking
D Start of runBlocking
D Finished IO Coroutine 1
D Finished IO Coroutine 2
D End of runBlocking
D after runBlocking
关于Job
每当我们启动一个协程后会返回一个Job对象
val job = GlobalScope.launch(Dispatchers.IO) { }
job提供一些常用方法例如join.
val job = GlobalScope.launch(Dispatchers.IO) { }
runBlocking {
job.join() //等待协程执行完成
}
对上面程序做以下修改
val job = GlobalScope.launch(Dispatchers.IO) {
repeat(5) {
Log.d("tag", "Coroutine is still working")
delay(1000L)
}
}
runBlocking {
delay(2000L)
job.join()
Log.d("tag", "Main Thread is continuing...")
}
输出
D Coroutine is still working
D Coroutine is still working
D Coroutine is still working
D Coroutine is still working
D Coroutine is still working
D Main Thread is continuing...
job的cancel
val job = GlobalScope.launch(Dispatchers.IO) {
repeat(5) {
Log.d("tag", "Coroutine is still working")
delay(1000L)
}
}
runBlocking {
delay(2000L)
job.cancel()
Log.d("tag", "Main Thread is continuing...")
}
输出
D Coroutine is still working
D Coroutine is still working
D Coroutine is still working
D Main Thread is continuing...
这样我们成功了cancel了一个Coroutine. 但是cancel一个Coroutine有时候并不是像上面这么容易,因为我们上一个示例中Coroutine大多数时间是在delay,所以调用cancel()的Coroutine很快能够接收到通知。但是如果在Coroutine中正在进行一些复杂的运算。例如改成下面这样
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val job = GlobalScope.launch(Dispatchers.IO) {
Log.d("tag", "Starting long running calculation...")
repeat(5) {
for (i in 30..40) {
Log.d("tag", "Result for i = $i : ${fib(i)}")
}
}
Log.d("tag", "Ending long running calculation...")
}
runBlocking {
delay(2000L)
job.cancel()
Log.d("tag", "Canceled job!")
}
}
fun fib(n : Int) : Long {
return if (n == 0) 0
else if (n == 1) 1
else fib(n -1) + fib(n - 2)
}
}
输出
D Result for i = 33 : 3524578
D Result for i = 34 : 5702887
D Result for i = 35 : 9227465
D Result for i = 36 : 14930352
D Result for i = 37 : 24157817
D Canceled job!
D Result for i = 38 : 39088169
D Result for i = 39 : 63245986
D Result for i = 40 : 102334155
我们看到实际上调用了cancel并没有实际取消掉Coroutine的执行.为什么会这样?
实际上在Coroutine它在不停的进行计算,根本没有时间去执行cancel的判断,这个时候我们需要手动加上
for (i in 30..40) {
if(isActive) {
Log.d("tag", "Result for i = $i : ${fib(i)}")
}
}
withTimeout方法
我们通常取消一个Coroutine都是因为超时,所以这里有一个简单的withTimeout方法用于取消协程
val job = GlobalScope.launch(Dispatchers.IO) {
Log.d("tag", "Starting long running calculation...")
withTimeout(3000L) {
for (i in 30..40) {
if (isActive) {
Log.d("tag", "Result for i = $i : ${fib(i)}")
}
}
}
Log.d("tag", "Ending long running calculation...")
}
2023-01-07 16:36:56.257 25494-25531 tag D Starting long running calculation...
2023-01-07 16:36:56.265 25494-25531 tag D Result for i = 30 : 832040
2023-01-07 16:36:56.272 25494-25531 tag D Result for i = 31 : 1346269
2023-01-07 16:36:56.286 25494-25531 tag D Result for i = 32 : 2178309
2023-01-07 16:36:56.312 25494-25531 tag D Result for i = 33 : 3524578
2023-01-07 16:36:56.362 25494-25531 tag D Result for i = 34 : 5702887
2023-01-07 16:36:56.414 25494-25531 tag D Result for i = 35 : 9227465
2023-01-07 16:36:56.502 25494-25531 tag D Result for i = 36 : 14930352
2023-01-07 16:36:56.647 25494-25531 tag D Result for i = 37 : 24157817
2023-01-07 16:36:56.884 25494-25531 tag D Result for i = 38 : 39088169
2023-01-07 16:36:57.254 25494-25531 tag D Result for i = 39 : 63245986
2023-01-07 16:36:57.819 25494-25531 tag D Result for i = 40 : 102334155
2023-01-07 16:36:57.819 25494-25531 tag D Ending long running calculation...
Async和Await
通常我们在一个CoroutineScope中执行代码都是顺序执行的,但是有的场景我们希望它是并行执行的,例如现在有两个网络请求调用
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch(Dispatchers.IO) {
val time = measureTimeMillis {
val answer1 = networkCall1()
val answer2 = networkCall2()
Log.d("tag", "Answer1 is $answer1")
Log.d("tag", "Answer1 is $answer2")
}
Log.d("tag","Request took $time ms.")
}
}
suspend fun networkCall1() : String {
delay(3000L)
return "Answer 1"
}
suspend fun networkCall2() : String {
delay(3000L)
return "Answer 2"
}
}
输出
D Answer1 is Answer 1
D Answer1 is Answer 2
D Request took 6004 ms.
实际上它是串行执行的
我们现在希望把它改成并行执行.现在把代码执行方式换一下,我们在GlobalScope再去创建Scope执行协程.
GlobalScope.launch(Dispatchers.IO) {
val time = measureTimeMillis {
var answer1: String? = null
var answer2: String? = null
val job1 = launch {
answer1 = networkCall1()
}
val job2 = launch {
answer2 = networkCall2()
}
job1.join()
job2.join()
Log.d("tag", "Answer1 is $answer1")
Log.d("tag", "Answer1 is $answer2")
}
Log.d("tag","Request took $time ms.")
}
最后运行
2023-01-07 17:05:22.400 25998-26035 tag D Answer1 is Answer 1
2023-01-07 17:05:22.400 25998-26035 tag D Answer1 is Answer 2
2023-01-07 17:05:22.400 25998-26035 tag D Request took 3011 ms.
这样它们就是并行执行.
这里还有一种更为科学的方式,使用async和await.
GlobalScope.launch(Dispatchers.IO) {
val time = measureTimeMillis {
var answer1 = async {
networkCall1()
}
var answer2= async {
networkCall2()
}
Log.d("tag", "Answer1 is ${answer1.await()}")
Log.d("tag", "Answer1 is ${answer2.await()}")
}
Log.d("tag","Request took $time ms.")
}
输出的结果和上面一致. 这样代码实现起来更加简洁,通常我们有返回值的异步操作都可以使用async.
lifecycleScope 和 viewModelScope
使用普通的CoroutineScope会存在一个问题,例如
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.btn_skip).setOnClickListener {
//协程A
GlobalScope.launch {
while (true) {
delay(1000L)
Log.d("tag", "Still running...")
}
}
//协程B
GlobalScope.launch {
delay(5000L)
Intent(this@MainActivity, SecondActivity::class.java).also {
startActivity(it)
finish()
}
}
}
}
}
当我们点击按钮,启动一个新的一个Activity后,实际上协程A还是会继续运行, 这会造成内存泄漏. 如果我们换成lifecycleScope后,就避免了上面的现象.
lifecycleScope.launch {
while (true) {
delay(1000L)
Log.d("tag", "Still running...")
}
}
lifecycleScope能够感知生命周期,在Activity Destory后会取消Coroutine. 但是需要注意的是之前在Job章节中取消Coroutine提到,Coroutine在不停的进行密集型计算操作中,根本没有时间去执行cancel的判断,这个时候我们需要手动加上,isActive的判断.
viewModelScope实际上和lifecycleScope一样,只是它是绑定了viewmodel的生命周期.
深入理解Coroutine的Cancel和异常处理
lifecycleScope.launch {
try {
launch {
throw Exception()
}
} catch (e : Exception) {
println("Caught exception : $e")
}
}
如果运行上面代码,你会发现try catch根本没有用,还是会抛出异常.
如果我们要try catch它,必须写成这样
lifecycleScope.launch {
launch {
try {
throw Exception()
} catch (e: Exception) {
}
}
}
实际上Coroutine对于异常是向上抛出的,例如
lifecycleScope.launch { //抛出 3
try {
launch { //抛出 2
launch { //抛出 1
throw Exception()
}
}
} catch (e : Exception) {
println("Caught exception : $e")
}
}
如上,我们实际上并没有捕获异常,异常按照 1 -> 2 -> 3的顺序抛出.
如果我们使用async创建Coroutine会是怎么样的呢?
lifecycleScope.launch { //抛出 2
val string = async { //抛出 1
delay(500L)
throw Exception("error")
"Result"
}
}
以上情况还是会抛出异常,如果改成下面这样,是不会出现异常的
lifecycleScope.async { // 等待用户消费(调用await)
val string = async { //抛出 1
delay(500L)
throw Exception("error")
"Result"
}
}
实际上async只有在调用await才会抛出异常, 所以下面的代码依然会抛出异常.
val deferred = lifecycleScope.async {
val string = async {
delay(500L)
throw Exception("error")
"Result"
}
}
lifecycleScope.launch { //抛出 2
deferred.await() //抛出 1
}
只能使用如下方式捕获异常
val deferred = lifecycleScope.async {
val string = async {
delay(500L)
throw Exception("error")
"Result"
}
}
lifecycleScope.launch {
try {
deferred.await()
} catch (e : Exception) {
e.printStackTrace()
}
}
Coroutine标准的异常捕获方式
使用CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, throwable ->
println("Caught exception : $throwable")
}
lifecycleScope.launch(handler) {
launch {
throw Exception("Error")
}
}
CoroutineScope和SupervisorScope
val handler = CoroutineExceptionHandler { _, throwable ->
println("Caught exception : $throwable")
}
CoroutineScope(Dispatchers.Main + handler).launch {
launch {
delay(300L)
throw Exception("Coroutine 1 failed")
}
launch {
delay(400L)
println("Coroutine 2 finished")
}
}
输出
Caught exception : java.lang.Exception: Coroutine 1 failed
可以看到Coroutine 1失败了,也导致了Coroutine 2不会执行. CoroutineScope的机制就是,其中一个Coroutine失败会导致其中所有的Coroutine都会取消执行.
val handler = CoroutineExceptionHandler { _, throwable ->
println("Caught exception : $throwable")
}
CoroutineScope(Dispatchers.Main + handler).launch {
SupervisorScope {
launch {
delay(300L)
throw Exception("Coroutine 1 failed")
}
launch {
delay(400L)
println("Coroutine 2 finished")
}
}
}
输出
I Caught exception : java.lang.Exception: Coroutine 1 failed
I Coroutine 2 finished
可以看到使用SupervisorScope并没有出现一个Coroutine失败会导致其中所有的Coroutine都会取消执行.
关于viewModelScope
viewModelScope实际上也是一个SupervisorScope,所以它里面的子协程是独立失败的,互相不影响
public val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
//使用了SupervisorJob
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}