用了kt已经3年多,协程也一直在用,但是并没有好好的整体盘一遍。现在整个完全学习版,总算是又通畅了不少,加深了理解。
计划分成3篇完成讲解。
上篇,基本用法,全讲解,本文。
中篇, kotlin协程2025 通俗易懂三部曲之中篇 常用函数和并发处理。
下篇,kotlin协程2025 通俗易懂三部曲之下篇 异常处理。
1. Kotlin 协程概述
对于协程的底层实现是极其复杂庞大的,我们要先学会使用,再去领悟设计思路和原理。
-
协程
轻量级线程被称为“协程”(Coroutine)。
底层实现,Kotlin 编译器会将挂起函数转换为状态机。当一个挂起函数被调用时,它会检查当前的状态,如果需要挂起,它会保存当前的状态(包括局部变量和调用栈),并在稍后恢复执行。
Kotlin 协程通过提供轻量级、易于使用的异步编程模型,极大地简化了异步编程的复杂性。这意味着协程的开销非常小,可以快速创建和销毁,非常适合执行密集的异步任务。
其实在JVM平台上,协程的实现,是尽量循环使用线程,我们将它称作一个优秀的多线程任务调度框架更为合适,实现减少回调地狱的目的。 -
组成
主要包括几大要素,协程作用域CorountineScope
,他的参数协程上下文CorountineContext
, 以及如何发起协程launch/async
等。
协程上下文又包括:Dispatchers线程模型,Job/SupervisorJob
,CoroutineExceptionHandler
异常处理器等。
发起协程主要包括:launch
/async
, 他们的参数也有上下文可以输入,以及CoroutineStart
。
在协程块中可以变更的东西:如何并发,并发方式,异常处理不同,切线程模型等。
接下来将一一讲解。
文中,我会给出标记**#最佳实践或者#建议**,以便大家写出高质量的,可维护的,团队模板化的代码。
2. CorountineScope (协程作用域)
它限制和控制协程的作用范围(生命周期)。通过调用cancel()函数,取消当前协程和所有子协程。
常用作用域为:GlobalScope,viewModelScope,lifecycleScope,MainScope(自行创建)
使用示例:
lifecycleScope.launch {
ALogJ.t("test run1")
withContext (Dispatchers.IO) { //切线程 withContext 会在下篇中讲解
ALogJ.t("test run2")
}
ALogJ.t("test run3")
}
//输出:
//MainThread: test run1
//SubThread63: test run2
//MainThread: test run3
lifecycleScope.launch(Dispatchers.Default) {
ALogJ.t("test run1")
}
//输出:
//SubThread64: test run1
不指定launch的具体参数的情况下运行如下:
scope | 运行线程 | 是否阻塞当前线程 | 是否阻塞当前协程 | 生命周期 |
---|---|---|---|---|
GlobalScope | 子线程 | 否 | 否 | 永远不终结 |
MainScope | 主线程 | 否 | 否 | 永远不终结,需手动调用cancel() |
lifecycleScope&viewModelScope | 主线程 | 在主线程中阻塞 | 否 | 生命周期自动cancel() |
可以通过launch(Dispatchers.IO)
的方式来修改执行上下文;
也可以通过withContext(Dispatchers.IO)
的方式在协程体内修改执行上下文。withContext
会在下篇中讲解。
#建议
- 尽量使用fragment/activity的lifecycleScope,或者ViewModel的viewModelScope, 他们会随着生命周期结束,自动取消没有完成的任务;
- 在某些类中,如果你能把控它的初始化和销毁。则可以自定义MainScope,手动cancel(), 如下:
class SomeClass { private laterinit var scope:CoroutineScope fun init() { scope = MainScope() } fun destroy() { scope.cancel() } }
- 在不理解Scope的创建之前,避免到处new MainScope(), CoroutineScope()。因为需要手动管理协程的取消,无法把控生命周期。
3. Dispatchers (执行线程环境)
可以指定协程的执行线程环境,强调的是线程的类别。
Dispatchers.IO
(IO密集型) 比如网络请求,读取文件等;
Dispatchers.Default
(cpu密集型) 比如排序算法,计算等;
Dispatchers.Main
(主线程) 切回主线程更新UI;
Dispatchers.Unconfined
: 构建完协程立刻在当前线程执行(很少使用)
Dispatchers.Main
通过主线程Handler的post执行;
Dispatchers.Main.immediate
会先检查Looper.myLooper() == Looper.getMainLooper()
立刻直接执行,否则通过主线程Handler.post执行
4. CoroutineContext (协程上下文)
它是CoroutineScope
的唯一成员,会传递给子Job或者子Scope。
包括如下参数,可以通过+号,直接拼接:
Job()
: 一般可选的就是Job()
或者SupervisorJob()
(第6章节SupervisorJob vs Job
详细介绍);Dispatchers
:强调的就是线程模型,选择你期待的线程,一般可选为IO,Default,Main;ExceptionHandler
:异常处理,参考【下篇】。CoroutineName
: 协程名字。
Demo:
private val subScope = CoroutineScope(Job() + Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable ->
logd { "${throwable.message}" }
})
lifecycleScope的上下文是什么呢?
kotlin LifecycleCoroutineScopeImpl(this, SupervisorJob() + Dispatchers.Main.immediate)
可以看出,主要是包括SupervisorJob
和Dispatchers主线程。
5. launch/async ( Job/Deferred )
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
可以通过launch创建出Job。async创建得到Deffered。
Job和Deferred二者他们都可以调用cancel函数。
使用async,调用deferred.await()将会挂起协程,如果不await,与launch无区别。
5.1 参数:context和start
- CoroutineContext参数:可以修改执行的上下文,最常用的修改线程,也可以传入异常处理捕获。
- CoroutineStart参数:
- 优先使用
DEFAULT
:满足大多数异步场景需求。 - 谨慎使用
UNDISPATCHED
:需确保协程体内无阻塞操作,避免主线程卡顿。 -
LAZY
适用场景:如延迟加载资源或按条件触发任务。val job = launch(start = CoroutineStart.LAZY) { println("需手动触发执行") } job.start() // 显式启动协程
- 优先使用
5.2 返回值:Job/Deferred
全称为:可被取消的协程引用。通过CoroutineScope
创建的协程,就是Job,协程的实例。
一个Job可以有多个子Job,即协程可以有子协程。Job可以cancel,也会结束内部创建的子Job。
Job:
isActive
协程是否存活(注意懒启动)
isCancelled
协程是否取消
isCompleted
协程是否完成
cancel()
取消协程
start()
启动协程
join()
阻塞等候协程完成
cancelAndJoin()
取消并等候协程完成
invokeOnCompletion( onCancelling: Boolean = false, invokeImmediately: Boolean = true, handler: CompletionHandler)
监听协程的状态回调
attachChild(child: ChildJob)
附加一个子协程到当前协程上。
Deferred:
Deferred继承了Job,自然能调用Job的函数;
同时还有额外的函数:
await()
阻塞等待结果
Demo:
mJob?.cancel()
var job = viewModelScope.launch {
try {
//挂起函数,withContext(Dispatchers.IO)切换线程执行
val info = api.signUp(email, password)
signData.safeSetValue(SignSuccess(info.toData...))
} catch (e: Throwable) {
signData.safeSetValue(SignError(...))
}
}
mJob = job
async Demo:
lifecycleScope.launch {
val deferred = async(Dispatchers.IO) {
//指定运行到子线程
Thread.sleep(3000)
""" {"data":"request successfully."} """
}
ALogJ.t("运行在主线程")
val data = deferred.await()
ALogJ.t("运行在主线程得到结果 $data")
}
特性 | launch | async |
---|---|---|
返回值 | 无(Job ) | 有(Deferred<T> ) |
用途 | 后台任务、无返回值的操作 | 需要结果的异步任务 |
阻塞性 | 非阻塞 | 非阻塞(但 await() 可同步阻塞) |
异常存储 | 存储在 Deferred ,await() 时抛出,继续向上抛 | 立即向上抛,传播到父协程或处理器 |
错误处理 | CoroutineExceptionHandler 或 try-catch | 通过 try-catch 自身block 或 捕获await() |
异常处理详情在【下篇】。
6. SupervisorJob() vs Job()
是Scope级别,申明协程作用域的上下文。
注意与【中篇】的coroutineScope & supervisorScope 2个挂起函数,功能相似,这是这2个是在协程块级别。
特性 | Job | SupervisorJob |
---|---|---|
子协程异常影响 | 子协程异常会取消父协程及其他子协程 | 子协程异常仅影响自身,不传播 |
适合场景 | 严格依赖的并行任务, 需要保证所有子协程要么全部成功, 要么全部失败 | 独立任务(如多个网络请求) lifecycleScope,viewModelScope, MainScope属于此类 |
代码实例:
private val mScope = CoroutineScope(Job() + Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable ->
logd { "${throwable.message}" }
})
mScope.launch {
logt { "Run1......" }
delay(100)
throw RuntimeException("Error Exception1")
}
mScope.launch {
logt { "Run2......" }
delay(200)
logt { "Run2......over" }
}
mScope.launch {
logt { "Run3......" }
delay(300)
logt { "Run3......over" }
}
//执行结果
//SubThread[58]: Run1......
//SubThread[58]: Run2......
//SubThread[58]: Run3......
//Error Exception2
//换成SupervisorJob()的执行结果
//SubThread[59]: Run2......
//SubThread[60]: Run3......
//SubThread[58]: Run1......
//Error Exception
//SubThread[58]: Run2......over
//SubThread[58]: Run3......over
上述代码中:
由于第一个协程抛出了异常,其他协程都会跟着被取消。
如果申明mScope
的参数Job()
->SupervisorJob()
,则是单个协程异常被捕获,不会导致其他协程被取消。
注意:不论哪种,申明的Scope没有CoroutineExceptionHandler,则都会抛向线程异常,进而app崩溃。
【下篇】会继续再次深入讲解Job()/SupervisorJob()的异常处理。
7. 总结
本文是Kotlin协程系列教程的上篇,主要介绍协程的基本用法。内容包括:协程的概念,核心组成要素(CoroutineScope作用域、CoroutineContext上下文、launch/async启动方式等),以及最佳实践建议。