为什么要写这篇文章
在学习和应用Kotlin协程过程中后有一些感受:
- 应用型的文章往往只是对照官网案例照猫画虎,几乎所有的文章都是先从
背景
、优势
、demo 基本应用案例
,简单列举一下launch
、async
、Dispatchers
使用方式,而缺少实战案例以及具体应用适用场景的分析,导致看了很多协程文章后,仍然不会在项目中使用和解决实际碰到的问题. - 原理型的文章通常会通过反编译协程源码来讲解
状态机原理
、CPS 转换
、协程与线程性能对比
等协程实现的核心概念和原理,从而深陷于复杂的源码之中。 - 在工作的项目中发现了许多协程
错用
、滥用
地方.
为什么无法掌握协程
大多数文章会从如下几个方面进行讲解 :
- 协程的概念和作用:解释什么是协程以及它解决的问题。
- 状态机原理:阐述协程基于状态机的运行机制。
- 挂起与恢复:详细说明协程如何挂起和恢复执行。
- 调度器:介绍不同类型的调度器及其工作原理和适用场景。
- 协程的构建和启动:包括如何创建和启动协程。
- 上下文:讲解协程上下文的作用和管理。
- 与线程的关系:剖析协程与传统线程的联系和区别。
- 异常处理:说明协程中异常的传播和处理方式。
- 协程的优势和适用场景举例:通过具体案例展示协程的优点和适合应用的情况。
但是通常情况下我们了解到以上知识点以后,仍然无法熟练应用协程解决项目中实际的问题。我仔细思考了一下,许多开发者不能很好的掌握和使用协程,主要原因如下 :
- 不清楚或者记不起来协程能够解决或简化项目中所遭遇的问题
- 缺乏对协程结构化并发设计思想思考
- 缺乏平滑的学习路线,一开始就直接学习
状态机原理
、与线程关系和效率对比
、协程优势
等理论知识,收益很小,容易被劝退 。
如何掌握协程 & 协程应用场景
通常需要重点关注如下知识点,就可以熟练掌握协程
- 作用域(生命周期): 约束作用时间和范围
- 切换与通讯 : 主协程 、子协程 互相切换与通讯
- 异常处理 :良好的异常处理机制
协程的应用场景是“以同步方式编写异步代码,解决回调(Callback)嵌套地狱问题”,然而通常我们难以很好地理解、消化和吸收这句话。这句话可以解释为以下两点:
- 解决多任务(同步/异步)执行时序问题
- 解决 Callback 回调嵌套问题
CoroutineScope 作用域 & Job
CoroutineScope
CoroutineScope
是所有协程开始运行的 "容器"
, 它的主要作用是控制着协程运行的生命周期,包括协程的创建、启动协程、取消、销毁。CoroutineScope
的取消也表示着在此作用域内开启的协程将会被全部取消. CoroutineScope
内还可以创建 子CoroutineScope
, 不同类型的作用域作用域代表着在此作用域内协程最大运行的时间
不同。 例如 GlobalScope
表示协程的最大可运行时间为整个APP的运行生命周期,Activity CoroutineScope(lifecycleScope)
表示协程的最大可运行时间为Activity的生命周期, 协程伴随着 CoroutineScope
销毁而取消停止运行. Android 中常用的 CoroutineScope 类型和作用域 如下 :
Job
Job
表示在一个 CoroutineScope
内开启的一个协程任务, Job
内可以开启多个子Job
, 通常每开启一个协程任务后会返回一个Job
对象,可以通过执行 Job.cancel()
方法取消协程运行
viewLifecycleOwner.lifecycleScope.launch {
val job = launch {
// xxxx
}
val async = async {
// xxxx
}
async.cancel()
}
CoroutineScope & Job
CoroutineScope 可以开启多个 Job , Job内可以存在多个 CoroutineScope ,关系如下图
以下代码仅是为了表达两者之间的关系,不推荐这样使用, 后面我们会讲到 supervisorScope
的使用场景.
viewLifecycleOwner.lifecycleScope.launch {
launch {
supervisorScope {
launch {
}
}
}
supervisorScope {
launch {
}
}
}
coroutineScope vs supervisorScope (推荐使用)
viewLifecycleOwner.lifecycleScope.launch {
try {
coroutineScope {
val job1 = launch {
delay(2000)
// 如果抛出异常 , job1 停止执行 , job2 也会被取消,停止执行
throw NullPointerException()
}
val job2 = launch {
delay(2000)
println("println coroutineScope job 2")
}
// 如果执行 cancel ,job 1 , job 2 均取消
// cancel()
}
} catch (e: Throwable) {
// ignore
}
try {
supervisorScope {
val job1 = launch {
delay(2000)
// 如果抛出异常 , job1 停止执行 , job2 继续执行
throw NullPointerException()
}
val job2 = launch {
delay(2000)
println("println supervisorScope job 2")
}
// 如果执行 cancel ,job 1 , job 2 均取消
// cancel()
}
} catch (e: Throwable) {
// ignore
}
小节
coroutineScope 和 supervisorScope 实际业务开发中使用较少, 通常被使用在一个独立的模块或其他服务请求任务关联后者影响较小时开启一个子作用域, 例如一个独立的服务请求和子系统任务处理 。
supervisorScope
在此作用域内开启的子协程发生异常以后, 则该作用域内的其他兄弟协程不会受到影响,将正常执行.coroutineScope
在此作用域内开启的子协程发生异常以后, 该作用域内的所有协程都将被取消.- 在
coroutineScope
或supervisorScope
内执行cancel()
方法取消协程, 作用域内部的所有Job
均会被取消.
SupervisorJob vs Job
SupervisorJob 、 Job
可以在开启一个协程时设置任务类型,默认开启一个协程方式为 launch(){....}
内部实现为Job(coroutineContext[Job])
,也可以通过 launch(SupervisorJob(coroutineContext[Job])) { } , async(SupervisorJob(coroutineContext[Job])) { }
方式指定Job
类型,它的主要作用异常发生时,对父协程、兄弟协程影响.
SupervisorJob
SupervisorJob
下的supervisorJob1
发生异常后, SupervisorJob
下的 子协程supervisorJob2
也会被取消, SupervisorJob兄弟协程 job1
将正常执行并输出 "println job1"
lifecycleScope.launch(handler) {
val job1 = launch {
delay(1000)
println("println job1")
}
launch(SupervisorJob(coroutineContext[Job])) {
val supervisorJob1 = launch {
delay(1000)
// 如果抛出异常 , job1 停止执行 , job2 继续执行
throw NullPointerException()
}
val supervisorJob2 = launch {
delay(2000)
println("println SupervisorJob job2")
}
}
}
Job
Job
下的supervisorJob1
发生异常后, Job
下的 supervisorJob2
也会被取消, Job的 兄弟协程 job1 也将被取消.
lifecycleScope.launch(handler) {
val job1 = launch {
delay(1000)
println("println job1")
}
launch(Job(coroutineContext[Job])) {
val supervisorJob1 = launch {
delay(1000)
// 如果抛出异常 , job1 停止执行 , job2 继续执行
throw NullPointerException()
}
val supervisorJob2 = launch {
delay(2000)
println("println SupervisorJob job2")
}
}
}
小节
只有在开启的协程任务在发生异常时不希望影响到父协程和兄弟协程
时,可以使用 在 launch() 或者 async()
指定job
类型为 SupervisorJob
, 通常情况下无需单独设置SupervisorJob
。
SupervisorJob
:SupervisorJob
内的子Job发生异常时,不会影响兄弟协程和父协程
执行。Job
:Job
内的子Job
发生异常时,会取消兄弟协程,异常会继续向上传递,直到向上传递的对应层级协程Job类型为null
或SupervisorJob
为止, 并取消对应层级的协程和子协程。
自定义 CoroutineScope
自定义一个 GlobalCoroutineScope
object MyGlobalScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
fun MyGlobalScope() {
MyGlobalScope.launch {
// xxxxxx
}
}
自定义一个 ViewCoroutineScope
仅为示例,不推荐在项目中使用
跟view的移除,取消作用域协程的执行
class ViewCoroutineScope(override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main) : CoroutineScope
class MyView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), CoroutineScope by ViewCoroutineScope() {
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
this.cancel()
}
fun test() {
launch {
// 在自定义作用域内开启协程.
}
}
}
线上错误作用域使用案例
Crash Log
android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@2142206 is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:1724)
...
如以下场景案例均可能会导致Crash和内存无法及时回收 (因为持有Activity或者View的引用
) .
错误案例一 : 谨慎使用 GlobalScope
执行一个点赞操作之后, 关闭Activity页面 , 2s弹出一个点赞成功弹窗
fun testGlobalScopeShowDialog01() {
GlobalScope.launch {
// 模拟一个点赞/收藏网络请求耗时操作
delay(2000)
// 弹出一个弹窗
withContext(Dispatchers.Main) {
AlertDialog.Builder(this@MainActivity2).setTitle("收藏成功").show()
}
}
}
错误风险
: Crash
、Activity/View内存无法及时回收
错误分析:
GlobalScope生命周期大于Activity, Activity关闭后GlobalScope内的协程任务仍继续执行,延时任务结束后在一个已经关闭的Activity弹出Dialog
经验总结:
不要在GlobalScope执行一些Activity/View相关的操作 ,GlobalScope适合执行一些不依赖Activity/View相关的后台任务 ,例如 文件读写、日志上报 等等.
错误案例二 : 谨慎使用 view.postDelayed
在一个页面内,执行某些操作,2s之后,弹出一个弹窗或者对View执行一些操作
fun testGlobalScopeShowDialog02() {
testGlobalScopeShowDialog.postDelayed({
// btnTestGlobalScopeShowDialog.text = "Hello World !"
AlertDialog.Builder(this@MainActivity2).setTitle("收藏成功").show()
}, 2000)
}
`错误风险` : `Crash` 、`Activity/View内存无法及时回收`
错误分析:
view.postDelayed()
方法会将应用主Handler
发送一个延时Message
, 如果Activity
页面关闭未及时移除延时任务,任务仍会被主Handler延时派发执行
, 导致Crash
或者Activity
内存无法及时回收.
经验总结:
页面关闭时及时移除或者取消postDelayed任务
错误案例三: Fragment viewLifecycleOwner.lifecycleScope vs lifecycleScope
// 推荐
viewLifecycleOwner.lifecycleScope.launch {
flow.collect {
view.setBackgroundColor(Color.RED)
}
}
// 不推荐
lifecycleScope.launch {
flow.collect {
view.setBackgroundColor(Color.RED)
}
}
// 推荐
liveData.observe(viewLifecycleOwner) {
// xxx
}
// 不推荐
liveData.observe(this) {
// xxx
}
-
viewLifecycleOwner.lifecycleScope
绑定fragment
的onCreateView()到 onDestroyView()
这个范围的生命周期 -
lifecycleScope
绑定fragment
的整个生命周期onCreate()到onDestroy()
这个范围的生命周期,生命周期范围会更长
错误风险:
Crash
错误分析:
lifecycleScope
,当使用viewModel
获得数据后通过flow
发送数据,此时如果onDestroyView()
被调用已经销毁掉了view
,但 onDestroy()
未被调用,lifecycleScope
内则会将继续观察 flow
,view
为空导致空指针异常 。但是如果改成使用 viewLifecycleOwner.lifecycleScope
,view
销毁生命周期和viewLifecycleOwner.lifecycleScope
协程取消生命周期一致,则可以避免该问题
经验总结:
使用 viewLifecycleOwner 代替 this@Fragment
对LiveData
进行观察 , 使用 viewLifecycleOwner.lifecycleScope 代替 lifecycleScope
进行协程相关操作
错误案例四
Activity lifecycleScope/ Fragment
中使用ViewModel viewModelScope
开启协程
错误案例
class Fragment {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
testViewModelScope()
}
fun testViewModelScope() {
viewModel.viewModelScope.launch {
// 模拟网络请求
delay(2000)
// 更新页面view
view.setBackgroundColor(Color.RED)
}
}
}
错误风险
: Crash
、Activity/View内存无法及时回收
错误分析:
发生屏幕旋转时 Activity
会被销毁, ViewModel viewModelScope
开启的协程内如果持有对Activity
相关的引用,则会导致Activity
无法被回收
经验总结:
Activity/Fragment
内使用自己lifecycleScope
协程作用域, viewModelScope
尽量在 ViewModel
内部使用 或 不要持有Activity/View
相关引用.
实战 - 解决回调嵌套地狱
suspendCoroutine vs suspendCancellableCoroutine(推荐使用)
协程的一个重要的应用场景在于 Callback
嵌套地狱问题. 官方主要提供两个 suspendCoroutine
和 suspendCancellableCoroutine
这两个API函数来实现回调转换为同步调用的挂起函数方式。
suspendCancellableCoroutine
: suspendCancellableCoroutine
的挂起函数内在协程取消后将会抛出一个 CancellationException
, 协程默认不处理这个异常,如果对其进行 try catch
, 将会捕获到 CancellationException
异常 , it.resume(logoUrl)
执行后,将无法唤起恢复挂起函数继续运行 .
suspendCoroutine
: suspendCoroutine
不会检查父协程状态 ,协程取消后, suspendCoroutine
函数内 it.resume(logoUrl)
执行后会唤起恢复挂起函数继续运行.
suspendCancellableCoroutine
比 suspendCoroutine
更加安全
案例一
RxJava Observable
转化为 suspend 函数
suspend fun <T> Observable<T>.toSuspend(): T {
return suspendCancellableCoroutine {
val disposable = subscribe({ value ->
it.resume(value)
}, { throwable ->
it.resumeWithException(throwable)
})
it.invokeOnCancellation {
disposable.dispose()
}
}
}
fun load() {
viewmodelScope.launch {
val observable = ...
// Observable to suspend
val result = observable.toSuspend()
}
}
it.invokeOnCancellation {...}
函数非常关键 ,它的相关特性如下:
- 协程被取消回调 ,这里适合做一些资源回收、任务取消等相关工作. 例如 取消网络 等等
it.resume
或it.resumeWithException
执行完毕后, 即使协程被取消,该回调将不会再执行
案例二
需求如下 :
- 点击滑动
RecyclerView
列表 - 滑动结束后弹出
Toast
提示
常规方式
fun testOnRecyclerViewScroll() {
view.setOnClickListener {
recyclerView.smoothScrollToPosition(10)
recyclerView.addOnScrollListener(object : OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
Toast.makeText(context, "scroll end " , Toast.LENGTH_LONG).show()
}
}
})
}
}
协程方式
// RecyclerView 扩展挂起函数
suspend fun RecyclerView.onScrollIdle() {
suspendCancellableCoroutine<Unit> {
this.addOnScrollListener(object : OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
removeOnScrollListener(this)
it.resume(Unit)
}
}
})
}
}
fun testOnRecyclerViewScroll() {
view.setOnClickListener {
lifecycleScope.launch {
recyclerView.smoothScrollToPosition(10)
recyclerView.onScrollIdle()
Toast.makeText(context, "scroll end " , Toast.LENGTH_LONG).show()
}
}
}
- 列表滑动
- onScrollIdle 函数挂起
- 滑动结束 ,onScrollIdle函数恢复
- 执行Toast
案例三
例如实现一个简单的登录需求如下:
- 调用登录接口 ,返回用户 token 、头像地址 等
- 调用VIP服务接口,查询用户是否是VIP (假设VIP服务和登录不在一个服务)
- 下载用户头像到本地
Mock 函数接口如下
data class User(val token: String, val logoUrl: String)
interface LoginCallback {
fun onSuccess(user: User)
fun onFail(throws: Throwable)
}
interface VIPCallback {
fun onSuccess(isVip: Boolean)
fun onFail(throws: Throwable)
}
interface LoadImgCallback {
fun onSuccess(logoUrl: String)
fun onFail(throws: Throwable)
}
fun doLoginCallback(loginCallback: LoginCallback) {
// onSuccess / onFail
}
fun doVipCallback(vipCallback: VIPCallback) {
// onSuccess / onFail
}
fun doLoadImageCallback(loadImgCallback: LoadImgCallback) {
// onSuccess / onFail
}
常规方式
fun login() {
doLoginCallback(object : LoginCallback {
override fun onSuccess(user: User) {
// 魔鬼嵌套
doVipCallback(object : VIPCallback {
override fun onSuccess(isVip: Boolean) {
// 魔鬼嵌套 ...
}
override fun onFail(throws: Throwable) {
}
})
}
override fun onFail(throws: Throwable) {
}
})
}
转换为 suspend 函数
suspend fun suspendDoLoginCallback(): User {
return suspendCancellableCoroutine {
doLoginCallback(object : LoginCallback {
override fun onSuccess(user: User) {
it.resume(user)
}
override fun onFail(throws: Throwable) {
it.resumeWithException(throws)
}
})
}
}
suspend fun suspendDoVipCallback(): Boolean {
return suspendCancellableCoroutine {
doVipCallback(object : VIPCallback {
override fun onSuccess(isVip: Boolean) {
it.resume(isVip)
}
override fun onFail(throws: Throwable) {
it.resumeWithException(throws)
}
})
}
}
suspend fun suspendDoLoadImageCallback(): String {
return suspendCancellableCoroutine {
doLoadImageCallback(object : LoadImgCallback {
override fun onSuccess(logoUrl: String) {
it.resume(logoUrl)
}
override fun onFail(throws: Throwable) {
it.resumeWithException(throws)
}
})
}
}
协程方式实现
fun login() {
lifecycleScope.launch {
try {
val user = suspendDoLoginCallback()
val isVip = suspendDoVipCallback()
val logoUrl = suspendDoLoadImageCallback()
} catch (throwable: Throwable) {
// 登录失败
}
}
}
案例四
如果业务发生变更为:
- 调用登录接口
- 调用VIP服务接口 与 下载用户头像到本地 同时请求 , 无论成功/失败都不影响登录结果
fun login() {
lifecycleScope.launch {
val user = suspendDoLoginCallback()
val isVipDeferred = async {
suspendDoVipCallback()
}
val logoUrlDeferred = async {
suspendDoLoadImageCallback()
}
try {
val isVip = isVipDeferred.await()
val logoUrl = logoUrlDeferred.await()
} catch (throwable: Throwable) {
throwable.printStackTrace()
// 忽略失败
}
}
}
异常处理
简单概括为为一句话 : 所有 suspend 类型的函数都可以被正常的 try catch
处理, 更多可以查看 Kotlin协程异常处理
结构化并发
协程的结构化并发设计的核心思想就是在处理 整体与局部
和 局部与局部
之间的关系。 从 CoroutineScope
、Job
中可以窥探一二,CoroutineScope
对 父CoroutineScope 、 兄弟 CoroutineScope 、 子CoroutineScope
的影响和处理, Job
对父Job 、兄弟Job 、子Job
的影响和处理, 其实都是在处理 整体
和 局部
的关系, 如何能够很好的理解CoroutineScope
和 Job
作用域、生命周期、异常分发和处理,将对掌握协程有很大帮助。
总结
- 所有
suspend
类型的函数都可以被try catch
正常捕捉处理 suspendCoroutine & suspendCancellableCoroutine
是Callback回调函数
转换协程suspend函数
, 是处理回调嵌套地狱的关键, 推荐使用suspendCancellableCoroutine
,因为它更加安全supervisorScope
内的子协程发生异常时互不影响,coroutineScope
会取消作用域内所有的子协程。supervisorScope 、coroutineScope
为suspend
函数,可以使用try catch
捕捉处理异常- 如果希望一个任务发生异常时不影响到其他兄弟协程和父协程 ,可以在
launch
或async
时设置为SupervisorJob