0.背景
干啥事总要有一个背景和目的,就像我写方案一样,一定有一个目标才能驱使前行。
因为换了新公司,新公司卷的一笔,平时很少有时间总结。这不快三个月了吗,抽点时间总结一下。
写这个文章的目的也是因为公司项目中有一个模块大量使用到了 Kotlin 协程。但是我对协程的理解和实际应用又比较少。整好前段时间看到扔物线推出了协程课程,也想着花一些钱去买个课。但后来想想也要不少钱,就花了十几块钱买了另外一个平台"朱涛"老师的 Kotlin 课。
认真看下来,着实让我收获到很多。特别是其中提到的各种思维模型的建立,表达式思维、空安全思维等,让我对技术的理解有了更高的维度。
本文章就将其中学习的协程部分总结出来,目的是方便学习协程的人可以快速的当做笔记来查看。课程中包括的内容很多,我这里只是将其中部分记录下来,方便回忆。
1. Android 中调试协程
arduino
System.setProperty("kotlinx.coroutines.debug", "on" )
2. launch 方法解释
kotlin
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job { ... }
- CoroutingContext 是协程的上下文,默认是 EmptyCoroutineContext,一般使用
Dispatchers
- CoroutineStart 协程的启动方式,有 DEFAULT, LAZY, ATOMIC, UNDISPATCHED 几种
kotlin
block: suspend CoroutineScope.() -> Unit
表示 block 是接收者是 CoroutineScope 的挂起函数,简单理解就是 CoroutineScope 的扩展函数。
使用 launch 需要协程的作用域,例如如下:
markdown
GlobalScope.launch{
}
GlobalScope 就是协程的作用域
3. runBlocking
runBlocking 启动的协程会阻塞当前线程的执行,例如:
scss
binding.mainBtn.setOnClickListener {
println(11111)
runBlocking {
launch {
println(1)
println(Thread.currentThread().name)
delay(1000)
println(2)
}
launch {
println(3)
println(Thread.currentThread().name)
delay(500)
println(4)
}
}
println(222222)
GlobalScope.launch {
println(33333)
}
}
上述会等待 runBlocking 中的代码执行完毕以后才会执行后面的代码。
对于这一点,Kotlin 官方也强调了:runBlocking 只推荐用于连接线程与协程,并且,大部分情况下,都只应该用于编写 Demo 或是测试代码。所以,请不要在生产环境当中使用 runBlocking。
runBlocking 方法签名:
kotlin
public actual fun <T> runBlocking(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T): T {
...
}
可以看到,runBlocking 就是一个普通的顶层函数,它并不是 CoroutineScope 的扩展函数,因此,我们调用它的时候,不需要 CoroutineScope 的对象。前面我们提到过,GlobalScope 是不建议使用的,因此,后面的案例我们将不再使用 GlobalScope。
另外注意 runBlocking 是有返回值的。
4. async 启动协程
kotlin
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit // 不同点1
): Job {} // 不同点2
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T // 不同点1
): Deferred<T> {} // 不同点2
观察上述不同点,返回值不同,async 可以获取值。例子如下:
kotlin
fun main() = runBlocking {
println("In runBlocking:${Thread.currentThread().name}")
val deferred: Deferred<String> = async {
println("In async:${Thread.currentThread().name}")
delay(1000L) // 模拟耗时操作
return@async "Task completed!"
}
println("After async:${Thread.currentThread().name}")
val result = deferred.await()
println("Result is: $result")
}
/*
输出结果:
In runBlocking:main @coroutine#1
After async:main @coroutine#1 // 注意,它比"In async"先输出
In async:main @coroutine#2
Result is: Task completed!
*/
5. 三种启动协程方式的总结
另外,我们还学到了三种启动协程的方式,分别是 launch、runBlocking、async。
-
launch,是典型的"Fire-and-forget"场景,它不会阻塞当前程序的执行流程,使用这种方式的时候,我们无法直接获取协程的执行结果。它有点像是生活中的射箭。
-
runBlocking,我们可以获取协程的执行结果,但这种方式会阻塞代码的执行流程,因为它一般用于测试用途,生产环境当中是不推荐使用的。
-
async,则是很多编程语言当中普遍存在的协程模式。它像是结合了 launch 和 runBlocking 两者的优点。它既不会阻塞当前的执行流程,还可以直接获取协程的执行结果。它有点像是生活中的钓鱼。
6. 协程生命周期
协程是有生命周期的,可以取消,例如:
kotlin
runBlocking {
val job = launch {
logX("start")
delay(1000)
logX("end")
}
job.log()
job.cancel()
job.log()
delay(1500)
}
fun Job.log(){
logX("isActive = $isActive isCancelled = $isCancelled isCompleted = $isCompleted ".trimIndent())
}
fun logX(any:Any?){
println("""
================================
$any
Thread:${Thread.currentThread().name}
================================""".trimIndent())
}
上述代码可以通过 Job.cancel() 来取消掉。
再看下面一个例子:
scss
runBlocking {
val job = launch(start = CoroutineStart.LAZY) {
logX("start")
delay(1000)
logX("end")
}
delay(500)
job.log()
job.start()
job.log()
delay(500)
job.cancel()
delay(500)
job.log()
delay(2999)
logX("end===")
}
这个例子中 start 设置为 LAZY,lanunch 以后并不会立刻执行,而是在调用 job.start() 之后才会执行。
6.1. 生命周期图
协程的生命周期大概如下:
LAZY 的初始状态是 NEW,非 LAZY 的初始状态就直接是 Active。
6.2. 监听协程完成
协程结束以后会调用这个方法:
go
job.invokeOnCompletion {
println("结束了")
}
6.3. 结构化并发
简单来说,"结构化并发"就是:带有结构和层级的并发。
协程之间是有父子关系的.
7. 协程上下文 Context
CoroutineContext 很容易理解,它只是个上下文而已,实际开发中它最常见的用处就是切换线程池。
我们在调用 launch 的时候,都没有传 context 这个参数,因此它会使用默认值 EmptyCoroutineContext,顾名思义,这就是一个空的上下文对象。而如果我们想要指定 launch 工作的线程池的话,就需要自己传 context 这个参数了。
7.1. 内置 Dispatchers
- Dispatchers.Main,它只在 UI 编程平台才有意义,在 Android、Swing 之类的平台上,一般只有 Main 线程才能用于 UI 绘制。这个 Dispatcher 在普通的 JVM 工程当中,是无法直接使用的
- Dispatchers.Unconfined,代表无所谓,当前协程可能运行在任意线程之上
- Dispatchers.Default,它是用于 CPU 密集型任务的线程池。一般来说,它内部的线程个数是与机器 CPU 核心数量保持一致的,不过它有一个最小限制 2
- Dispatchers.IO,它是用于 IO 密集型任务的线程池。它内部的线程数量一般会更多一些(比如 64 个),具体线程的数量我们可以通过参数来配置:kotlinx.coroutines.io.parallelism。
7.2. 自定义 Dispatchers
scss
val mySingleDispatcher = Executors.newSingleThreadExecutor {
Thread(it).also { it.isDaemon = true }
}.asCoroutineDispatcher()
suspend fun getUserInfo(): String {
logX("Before IO Context.")
withContext(mySingleDispatcher) {
logX("In IO Context.")
delay(1000L)
logX("In IO Context22222.")
}
logX("After IO Context.")
return "BoyCoder"
}
这里的 asCoroutineDispatcher 实际上是实现 ExecutorCoroutineDispatcherImpl 的实现。
kotlin
public fun ExecutorService.asCoroutineDispatcher(): ExecutorCoroutineDispatcher =
ExecutorCoroutineDispatcherImpl(this)
7.3. withContext
使用 withContext 来切换上下文:
scss
binding.mainBtn.setOnClickListener {
runBlocking {
val user = getUserInfo()
logX(user)
}
}
suspend fun getUserInfo(): String {
logX("Before IO Context.")
withContext(Dispatchers.IO) {
logX("In IO Context.")
delay(1000L)
logX("In IO Context22222.")
}
logX("After IO Context.")
return "BoyCoder"
}
withContext 会在其他线程中执行任务,并且挂起知道任务执行完成。
7.4. CoroutineContext
在 Kotlin 协程当中,但凡是重要的概念,都或多或少跟 CoroutineContext 有关系:Job、Dispatcher、CoroutineExceptionHandler、CoroutineScope,甚至挂起函数,它们都跟 CoroutineContext 有着密切的联系。甚至,它们之中的 Job、Dispatcher、CoroutineExceptionHandler 本身,就是 Context。
CoroutineScope 也遵从结构化并发:
跟下面这个例子类似:
CoroutineContext 接口设计:
kotlin
// 代码段13
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public operator fun plus(context: CoroutineContext): CoroutineContext {}
public fun minusKey(key: Key<*>): CoroutineContext
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public interface Key<E : Element>
}
可以将其当做 Map 接口来理解。
8. Java 线程转 Kotlin 协程
让 Java 函数支持 Kotlin 挂起函数:
比如说,项目中用到的 SDK 是开源的,或者 SDK 是公司其他部门开发的,我们无法改动 SDK。
在这里,我们需要用到 Kotlin 官方提供的一个顶层函数:suspendCoroutine{},它的函数签名是这样的:
kotlin
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T {
// 省略细节
}
从它的函数签名,我们可以发现,它是一个挂起函数,也是一个高阶函数,参数类型是"(Continuation) -> Unit",,它其实就等价于挂起函数类型!所以,我们可以使用 suspendCoroutine{} 来实现 await() 方法:
kotlin
/*
注意这里
↓ */
suspend fun <T: Any> KtCall<T>.await(): T = suspendCoroutine{
continuation ->
// ↑
// 注意这里
}
public interface Continuation<in T> {
public val context: CoroutineContext
// 关键在于这个方法
public fun resumeWith(result: Result<T>)
}
完整例子
suspend fun <T: Any> KtCall<T>.await(): T =
suspendCoroutine { continuation ->
call(object : Callback<T> {
override fun onSuccess(data: T) {
//注意这里
continuation.resumeWith(Result.success(data))
}
override fun onFail(throwable: Throwable) {
//注意这里
continuation.resumeWith(Result.failure(throwable))
}
})
}
接下来可以调用 continuation.resumeWith() 方法进行后续的操作,这样就可以让扩展方法 await 支持挂起了。
不过这里要注意 suspendCoroutine 是不支持取消的,必须使用 suspendCancellableCoroutine 才可以。
示例如下:
kotlin
suspend fun <T : Any> KtCall<T>.await(): T =
// 变化1
// ↓
suspendCancellableCoroutine { continuation ->
val call = call(object : Callback<T> {
override fun onSuccess(data: T) {
println("Request success!")
continuation.resume(data)
}
override fun onFail(throwable: Throwable) {
println("Request fail!:$throwable")
continuation.resumeWithException(throwable)
}
})
// 变化2
// ↓
continuation.invokeOnCancellation {
println("Call cancelled!") //在这里进行取消
call.cancel()
}
}
当我们使用 suspendCancellableCoroutine{} 的时候,可以往 continuation 对象上面设置一个监听:invokeOnCancellation{},它代表当前的协程被取消了,这时候,我们只需要将 OkHttp 的 call 取消即可
9. Channel
Channel 可以跨越不同的协程进行通信
ruby
// 代码段1
fun main() = runBlocking {
// 1,创建管道
val channel = Channel<Int>()
launch {
// 2,在一个单独的协程当中发送管道消息
(1..3).forEach {
channel.send(it) // 挂起函数
logX("Send: $it")
}
}
launch {
// 3,在一个单独的协程当中接收管道消息
for (i in channel) { // 挂起函数
logX("Receive: $i")
}
}
logX("end")
}
/*
================================
end
Thread:main @coroutine#1
================================
================================
Receive: 1
Thread:main @coroutine#3
================================
================================
Send: 1
Thread:main @coroutine#2
================================
================================
Send: 2
Thread:main @coroutine#2
================================
================================
Receive: 2
Thread:main @coroutine#3
================================
================================
Receive: 3
Thread:main @coroutine#3
================================
================================
Send: 3
Thread:main @coroutine#2
================================
// 4,程序不会退出
*/
通过运行结果,我们还可以发现一个细节,那就是程序在输出完所有的结果以后,并不会退出。主线程不会结束,整个程序还会处于运行状态。
kotlin
// 代码段2
fun main() = runBlocking {
val channel = Channel<Int>()
launch {
(1..3).forEach {
channel.send(it)
logX("Send: $it")
}
channel.close() // 变化在这里
}
launch {
for (i in channel) {
logX("Receive: $i")
}
}
logX("end")
}
对于 Receive 还有一些其他的方法:
scss
lifecycleScope.launch {
while(!channel.isClosedForReceive){
logX("1---> Receive ${channel.receive()}") //channel.receiveCatching()
}
println("=========")
}
lifecycleScope.launch {
for (y in channel) {
logX("2---> Receive $y")
}
}
lifecycleScope.launch {
channel.consumeEach {
logX("3---> Receive ${channel.receive()}")
}
}
10. Flow
10.1. catch 异常
前面我已经介绍过,Flow 主要有三个部分:上游、中间操作、下游。那么,Flow 当中的异常,也可以根据这个标准来进行分类,也就是异常发生的位置。对于发生在上游、中间操作这两个阶段的异常,我们可以直接使用 catch 这个操作符来进行捕获和进一步处理。如下所示:
scss
// 代码段8
fun main() = runBlocking {
val flow = flow {
emit(1)
emit(2)
throw IllegalStateException()
emit(3)
}
flow.map { it * 2 }
.catch { println("catch: $it") } // 注意这里
.collect {
println(it)
}
}
/*
输出结果:
2
4
catch: java.lang.IllegalStateException
*/
catch 的作用域,仅限于 catch 的上游。换句话说,发生在 catch 上游的异常,才会被捕获,发生在 catch 下游的异常,则不会被捕获.
例如:
scss
// 代码段9
fun main() = runBlocking {
val flow = flow {
emit(1)
emit(2)
emit(3)
}
flow.map { it * 2 }
.catch { println("catch: $it") }
.filter { it / 0 > 1} // 故意制造异常
.collect {
println(it)
}
}
/*
输出结果
Exception in thread "main" ArithmeticException: / by zero
*/
这种只能在 collect 方法中对其捕获了。
kotlin
// 代码段10
fun main() = runBlocking {
flowOf(4, 5, 6)
.onCompletion { println("onCompletion second: $it") }
.collect {
try {
println("collect: $it")
throw IllegalStateException()
} catch (e: Exception) {
println("Catch $e")
}
}
}
10.2. 切换 Context:flowOn、launchIn
ruby
// 代码段11
fun main() = runBlocking {
val flow = flow {
logX("Start")
emit(1)
logX("Emit: 1")
emit(2)
logX("Emit: 2")
emit(3)
logX("Emit: 3")
}
flow.filter {
logX("Filter: $it")
it > 2
}
.flowOn(Dispatchers.IO) // 注意这里
.collect {
logX("Collect $it")
}
}
/*
输出结果
================================
Start
Thread:DefaultDispatcher-worker-1 @coroutine#2
================================
================================
Filter: 1
Thread:DefaultDispatcher-worker-1 @coroutine#2
================================
================================
Emit: 1
Thread:DefaultDispatcher-worker-1 @coroutine#2
================================
================================
Filter: 2
Thread:DefaultDispatcher-worker-1 @coroutine#2
================================
================================
Emit: 2
Thread:DefaultDispatcher-worker-1 @coroutine#2
================================
================================
Filter: 3
Thread:DefaultDispatcher-worker-1 @coroutine#2
================================
================================
Emit: 3
Thread:DefaultDispatcher-worker-1 @coroutine#2
================================
================================
Collect 3
Thread:main @coroutine#1
================================
flowOn 操作符也是和它的位置强相关的。它的作用域跟前面的 catch 类似:flowOn 仅限于它的上游。
在上面的代码中,flowOn 的上游,就是 flow{}、filter{} 当中的代码,所以,它们的代码全都运行在 DefaultDispatcher 这个线程池当中。只有 collect{} 当中的代码是运行在 main 线程当中的。
上述代码使用 flowOn 用来控制上有的线程,那么如何控制 collect 中的线程呢?这里有两种方式:
- 使用 withContext
- 使用 luanchIn
示例如下:
kotlin
// 代码段15
val scope = CoroutineScope(mySingleDispatcher)
flow.flowOn(Dispatchers.IO)
.filter {
logX("Filter: $it")
it > 2
}
.onEach {
logX("onEach $it")
}
.launchIn(scope)
/*
输出结果:
onEach{}将运行在MySingleThread
filter{}运行在MySingleThread
flow{}运行在DefaultDispatcher
*/
// 看一下 launchIn() 方法
public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
collect() // tail-call
}
所以上述代码就等于:
scss
// 代码段17
fun main() = runBlocking {
val scope = CoroutineScope(mySingleDispatcher)
val flow = flow {
logX("Start")
emit(1)
logX("Emit: 1")
emit(2)
logX("Emit: 2")
emit(3)
logX("Emit: 3")
}
.flowOn(Dispatchers.IO)
.filter {
logX("Filter: $it")
it > 2
}
.onEach {
logX("onEach $it")
}
scope.launch { // 注意这里
flow.collect()
}
delay(100L)
}
10.3. 综合例子
通过 Flow 的这些 API 可以构建非常灵活的使用,例如:
scss
// 代码段22
fun main() = runBlocking {
fun loadData() = flow {
repeat(3) {
delay(100L)
emit(it)
logX("emit $it")
}
}
fun updateUI(it: Int) {}
fun showLoading() { println("Show loading") }
fun hideLoading() { println("Hide loading") }
val uiScope = CoroutineScope(mySingleDispatcher)
loadData()
.onStart { showLoading() } // 显示加载弹窗
.map { it * 2 }
.flowOn(Dispatchers.IO)
.catch { throwable ->
println(throwable)
hideLoading() // 隐藏加载弹窗
emit(-1) // 发生异常以后,指定默认值
}
.onEach { updateUI(it) } // 更新UI界面
.onCompletion { hideLoading() } // 隐藏加载弹窗
.launchIn(uiScope)
delay(10000L)
}
上述的 onStart, flowOn, catch, onCompletion, launchIn 等方法的使用。
11. select
通常用于在选择几个并发任务中最先执行的那一个:select,就是选择"更快的结果"。
例子如下:
kotlin
// 代码段5
fun main() = runBlocking {
suspend fun getCacheInfo(productId: String): Product? {
delay(100L)
return Product(productId, 9.9)
}
suspend fun getNetworkInfo(productId: String): Product? {
delay(200L)
return Product(productId, 9.8)
}
fun updateUI(product: Product) {
println("${product.productId}==${product.price}")
}
val startTime = System.currentTimeMillis()
val productId = "xxxId"
// 1,缓存和网络,并发执行
val cacheDeferred = async { getCacheInfo(productId) }
val latestDeferred = async { getNetworkInfo(productId) }
// 2,在缓存和网络中间,选择最快的结果
val product = select<Product?> {
cacheDeferred.onAwait {
it?.copy(isCache = true)
}
latestDeferred.onAwait {
it?.copy(isCache = false)
}
}
// 3,更新UI
if (product != null) {
updateUI(product)
println("Time cost: ${System.currentTimeMillis() - startTime}")
}
// 4,如果当前结果是缓存,那么再取最新的网络服务结果
if (product != null && product.isCache) {
val latest = latestDeferred.await()?: return@runBlocking
updateUI(latest)
println("Time cost: ${System.currentTimeMillis() - startTime}")
}
}
/*
输出结果:
xxxId==9.9
Time cost: 120
xxxId==9.8
Time cost: 220
*/
12. 协程中的并发处理
与 Java 中类似,在协程中也面临中并发的场景。那么在协程中都有那些方式来处理并发呢?
首先可以使用 Java 中的方式 synchronized{},例如:
scss
// 代码段3
fun main() = runBlocking {
var i = 0
val lock = Any() // 变化在这里
val jobs = mutableListOf<Job>()
repeat(10){
val job = launch(Dispatchers.Default) {
repeat(1000) {
// 变化在这里
synchronized(lock) { //不支持挂起函数
i++
}
}
}
jobs.add(job)
}
jobs.joinAll()
println("i = $i")
}
/*
输出结果
i = 10000
*/
不过 synchronized 中并不支持挂起函数。
12.1. 通过 Mutex 来添加并发代码块
scss
// 代码段7
fun main() = runBlocking {
val mutex = Mutex()
var i = 0
val jobs = mutableListOf<Job>()
repeat(10) {
val job = launch(Dispatchers.Default) {
repeat(1000) {
// 变化在这里
mutex.lock()
i++
mutex.unlock()
}
}
jobs.add(job)
}
jobs.joinAll()
println("i = $i")
}
不过,对于 mutex.lock() 和 unlock() 之间的代码要处理异常情况,通常使用:mutex.withLock 方法。
kotlin
// 代码段10
fun main() = runBlocking {
val mutex = Mutex()
var i = 0
val jobs = mutableListOf<Job>()
repeat(10) {
val job = launch(Dispatchers.Default) {
repeat(1000) {
// 变化在这里
mutex.withLock {
i++
}
}
}
jobs.add(job)
}
jobs.joinAll()
println("i = $i")
}
// withLock的定义
public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T {
lock(owner)
try {
return action()
} finally {
unlock(owner)
}
}
12.2. Actor 管道来处理并发
Actor 实际上就是一个 Channel,可以在多个线程中往同一个管道中添加数据,在管道接收的地方做计算,这样也能实现同步的效果。
kotlin
// 代码段11
sealed class Msg
object AddMsg : Msg()
class ResultMsg(
val result: CompletableDeferred<Int>
) : Msg()
fun main() = runBlocking {
suspend fun addActor() = actor<Msg> {
var counter = 0
for (msg in channel) {
when (msg) {
is AddMsg -> counter++
is ResultMsg -> msg.result.complete(counter)
}
}
}
val actor = addActor()
val jobs = mutableListOf<Job>()
repeat(10) {
val job = launch(Dispatchers.Default) {
repeat(1000) {
actor.send(AddMsg)
}
}
jobs.add(job)
}
jobs.joinAll()
val deferred = CompletableDeferred<Int>()
actor.send(ResultMsg(deferred))
val result = deferred.await()
actor.close()
println("i = ${result}")
}
13. try-catch
在协程中对于异常的处理要特别注意,例如 cancel() 不成功、捕获异常位置不对等。
13.1. cancel 不起作用
案例一:cancel 不被响应
scss
val singleThreadScope = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
val scope: CoroutineScope = CoroutineScope(singleThreadScope)
binding.flowTestBtn.setOnClickListener {
scope.launch {
val job = launch(Dispatchers.Default) {
var i = 0
while (true) {
Thread.sleep(500)
i++
println("current: $i")
}
}
delay(2000)
job.cancel()
job.join()
println("===end===")
}
}
上述方式其启动了一个协程,然后 delay 2000 后 cancel() 协程,可以发现并不能取消,还是会一直 print。原因在于:
协程是互相协作的程序。因此,对于协程任务的取消,也是需要互相协作的。协程外部取消,协程内部需要做出响应才行。
正确用法如下:
scss
binding.flowTestBtn.setOnClickListener {
scope.launch {
val job = launch(Dispatchers.Default) {
var i = 0
while (isActive) { //内部添加了一个判断条件就可以cancel成功了
Thread.sleep(500)
i++
println("current: $i")
}
}
delay(2000)
job.cancel()
job.join()
println("===end===")
}
}
案例二:结构被破坏
scss
// 代码段3
val fixedDispatcher = Executors.newFixedThreadPool(2) {
Thread(it, "MyFixedThread").apply { isDaemon = false }
}.asCoroutineDispatcher()
fun main() = runBlocking {
// 父协程
val parentJob = launch(fixedDispatcher) {
// 1,注意这里
launch(Job()) { // 子协程1 这里的 Job()
var i = 0
while (isActive) {
Thread.sleep(500L)
i ++
println("First i = $i")
}
}
launch { // 子协程2
var i = 0
while (isActive) {
Thread.sleep(500L)
i ++
println("Second i = $i")
}
}
}
delay(2000L)
parentJob.cancel()
parentJob.join()
println("End")
}
/*
输出结果
First i = 1
Second i = 1
First i = 2
Second i = 2
Second i = 3
First i = 3
First i = 4
Second i = 4
End
First i = 5
First i = 6
// 子协程1永远不会停下来
*/
注意上述子协程 1 使用了 Job() 作为其上下文,实际上这破坏了:协程的父子关系
案例三:未正确处理 CancellationExeception
其实,对于 Kotlin 提供的挂起函数,它们是可以自动响应协程的取消的,比如说,当我们把 Thread.sleep(500) 改为 delay(500) 以后,我们就不需要在 while 循环当中判断 isActive 了。
实际上,对于 delay() 函数来说,它可以自动检测当前的协程是否已经被取消,如果已经被取消的话,它会抛出一个 CancellationException,从而终止当前的协程。
kotlin
// 代码段6
fun main() = runBlocking {
val parentJob = launch(Dispatchers.Default) {
launch {
var i = 0
while (true) {
// 1
try {
delay(500L)
} catch (e: CancellationException) {
println("Catch CancellationException")
// 2
throw e
}
i ++
println("First i = $i")
}
}
launch {
var i = 0
while (true) {
delay(500L)
i ++
println("Second i = $i")
}
}
}
delay(2000L)
parentJob.cancel()
parentJob.join()
println("End")
}
/*
输出结果
First i = 1
Second i = 1
First i = 2
Second i = 2
First i = 3
Second i = 3
Second i = 4
Catch CancellationException
End
*/
请看注释 1,在用 try-catch 包裹了 delay() 以后,我们就可以在输出结果中,看到"Catch CancellationException",这就说明 delay() 确实可以自动响应协程的取消,并且产生 CancellationException 异常。不过,以上代码中,最重要的其实是注释 2:"throw e"。当我们捕获到 CancellationException 以后,还要把它重新抛出去。而如果我们删去这行代码的话,子协程将同样无法被取消。
所以:捕获了 CancellationException 以后,要考虑是否应该重新抛出来。
13.2. try-catch 不起作用
注意下面这段代码中的 try-catch 不起作用:
kotlin
// 代码段8
fun main() = runBlocking {
try {
launch {
delay(100L)
1 / 0 // 故意制造异常
}
} catch (e: ArithmeticException) {
println("Catch: $e")
}
delay(500L)
println("End")
}
/*
输出结果:
崩溃
Exception in thread "main" ArithmeticException: / by zero
*/
不起作用的原因是 try-catch 与其中的 launch 部分代码并不是同步的, try-catch 先运行完毕,launch 后运行,相当于失去了作用范围。
所以:不用用 try-catch 直接包裹 launch 和 async 代码块。
13.3. SupervisorJob
尝试下面的代码会发现还是会崩掉的:
scss
binding.mainBtn.setOnClickListener {
lifecycleScope.launch {
val deferred = async {
delay(100)
1/0
}
delay(500)
println("end")
}
}
不管是调不调用 deferred.await() 方法,又或者是否给 await() 方法是否添加 try-catch 都会崩,但是如果使用 SupervisorJob:
scss
binding.mainBtn.setOnClickListener {
lifecycleScope.launch {
val deferred = async (context = SupervisorJob()){ //注意这里
delay(100)
1/0
}
delay(500)
//deferred.await()
println("end")
}
}
只要不调用 deferred.await() 就不会崩。所以就可以下面的方式来捕获 async 异常:
kotlin
// 代码段15
fun main() = runBlocking {
val scope = CoroutineScope(SupervisorJob())
// 变化在这里
val deferred = scope.async {
delay(100L)
1 / 0
}
try {
deferred.await()
} catch (e: ArithmeticException) {
println("Catch: $e")
}
delay(500L)
println("End")
}
/*
输出结果
Catch: java.lang.ArithmeticException: / by zero
End
*/
SupervisorJob 与 Job 最大的区别就在于,当它的子 Job 发生异常的时候,其他的子 Job 不会受到牵连。我这么说你可能会有点懵,下面我做了一个动图,来演示普通 Job 与 SupervisorJob 之间的差异。
所以:要使用 SupervisorJob 控制异常传播的范围。
13.4. CoroutineExceptionHandler
对于 CoroutineExceptionHandler,我们其实在第 17 讲里也简单地提到过。它是 CoroutineContext 的元素之一,我们在创建协程的时候,可以指定对应的 CoroutineExceptionHandler。
看下面这个例子:
scss
// 代码段18
fun main() = runBlocking {
val myExceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("Catch exception: $throwable")
}
// 注意这里
val scope = CoroutineScope(coroutineContext + Job() + myExceptionHandler)
scope.launch {
async {
delay(100L)
}
launch {
delay(100L)
launch {
delay(100L)
1 / 0 // 故意制造异常
}
}
delay(100L)
}
delay(1000L)
println("End")
}
/*
Catch exception: ArithmeticException: / by zero
End
*/
只要出异常都会在 CoroutineExceptionHandler 这里进行捕获,相当于 Android 中的 ExceptionHandle 全局捕获。
CoroutineExceptionHandler 只在顶层的协程当中才会起作用。也就是说,当子协程当中出现异常以后,它们都会统一上报给顶层的父协程,然后顶层的父协程才会去调用 CoroutineExceptionHandler,来处理对应的异常。
14. callback 转 Flow
Callback 转 Flow,用法跟 Callback 转挂起函数是差不多的。如果你去分析代码段 1 当中的代码模式,会发现 Callback 转挂起函数,主要有三个步骤。第一步:使用 suspendCancellableCoroutine 执行 Callback 代码,等待 Callback 回调;第二步:将 Callback 回调结果传出去,onSuccess 的情况就传结果,onFail 的情况就传异常;第三步:响应协程取消事件 invokeOnCancellation{}。所以使用 callbackFlow,也是这样三个步骤。如果你看过 Google 官方写的文档,你可能会写出这样的代码:
kotlin
// 代码段7
fun <T : Any> KtCall<T>.asFlow(): Flow<T> = callbackFlow {
val call = call(object : Callback<T> {
override fun onSuccess(data: T) {
// 1,变化在这里
trySendBlocking(data)
.onSuccess { close() }
.onFailure { close(it) }
}
override fun onFail(throwable: Throwable) {
close(throwable)
}
})
awaitClose {
call.cancel()
}
}
/*
输出结果
输出正常
程序等待一会后自动终止
*/
上述这个是标准做法:
- 使用 trySendBlocking() 而不是 offer() 和 trySend() ,这样即便 Channel 中满了,也可以等待 Channel 有空余的空间
- 在 onSuccess 和 onFailure 中要 close 或者 cancel() 掉,不然 Channel 一直开着会占用内存。