What is coroutines?
维基百科的定义
维基百科上对协程的定义:
1 | 协程是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复。 |

从线程说起

我们都知道线程是抢占式的,你可以给线程设置优先级,但是 CPU 实际执行的是哪个线程,我们并不能控制,只能通过锁的方式来保证程序的逻辑正确。
协程是协作式的,是编程语言层级的,而非系统层级。当我们切换协程时,不涉及任何系统调用,因此可以把协程看作是轻量的线程。
Kotlin 中的协程
Kotlin 语言从1.3开始支持协程。Kotlin 中的协程在概念上与其他语言类似,但并没有像其他语言提供 async 、await 关键字,而是以扩展库的形式存在,其中最重要的就是挂起函数(可中断函数),即函数可以在某个时候暂停,并在未来某个时候恢复。
挂起函数代码示例:

挂起函数使用 suspend 关键字来修饰,相比于常规函数,挂起函数多了两个操作,suspend 和 resume。
- suspend:挂起(暂停),暂停当前协程,并保存所有的局部变量。
- resume:让暂停的协程从暂停处继续执行。
协程提供了一种全新的处理异步任务的方式。使得我们可以像写同步代码那样去写异步代码,减少了回调地狱,代码的可读性也得到了增强。同时协程还内置了取消操作,这对于 Android 开发者来说无疑是个福音。
How do I use coroutines?
Step 1:引入协程依赖库
1 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' |
Step2:启动一个协程
1 | println("Start") |
可中断函数只允许在协程或者另一个可中断函数中调用。
Step3:使用 async 启动协程
1 | val deferred = GlobalScope.async { "Hello" } |
Step4:使用 Dispather 来进行线程调度
首先我们需要清楚一点:
协程可以在主线程中运行,suspend 函数并不代表后台执行。
那么协程是在哪个线程中运行呢?
1 | GlobalScope.launch { |
协程通过调度器(Dispather)来实现线程的调度,目前 Kotlin 提供了下面三个调度器来给我们使用:


Step5:理解和使用协程作用域
从上面的代码中我们可以看到协程的启动需要一个作用域,这在 Android 开发中是非常有用且必要的。当 Activity 或者 Fragment 销毁时,我们可以通过与该 Activity 或 Fragment 绑定的作用域来取消协程,从而避免产生内存泄漏。
常用的作用域一览表:
Scope | 描述 | 使用场景 |
---|---|---|
GlobalScope | 全局作用域 | 基本不使用,除非协程是全局的 |
viewModelScope | 与 viewModel 绑定的作用域 | App 内大部分场景 |
lifecycleScope | 与 Lifecycle 对象绑定的作用域,如 Activity 或 Fragment | 与生命周期相关的一些场景,使用 Fragment 时,需要区分 lifecycleScope 和 viewLifecycleOwner.lifecycleScope |
自己创建一个新的 scope:

Step6:取消协程
当启动多个协程时,我们可以通过取消启动该协程的作用域来取消协程,而不需要单独管理每个协程。


Step7:协程中的异常处理
Kotlin 中的异常
在学习协程的异常处理之前,我们先来看一下 Kotlin 中的异常。不知道大家有没有使用 String.toInt() 函数。我们先来看一下这个扩展函数的实现:


但是在我们在 Kotlin 调用 String.toInt 时,IDE 并没有提示我们捕获异常,这是为什么?原因很简单,Kotlin 中没有受检查的异常(Checked Exceptions)。碰到这种情况,需要我们自己捕获异常,否则就会 Crash。这可能也是大部分人诟病 Kotlin 的一点。
关于 Checked Exceptions 的设计好坏与否,这里不做讨论,有兴趣的同学可以参考这篇文章。
如果协程发生了异常
当一个协程由于异常运行失败,它会传播该异常到它的父级,随后它的父级会取消所有相关的子协程,并继续向上传递。
这个行为看上去是比较合理的,但有的时候我们并不希望这么做。假如我们有一个和 UI 相关的 Scope,这个 scope 的一个子协程抛出了异常,导致整个 scope 被取消,由于取消后的作用域无法启动新的协程,这显然是不能被接受的。
怎么解决这个问题呢?只需要在创建 scope 时,使用 SupervisorJob 而不是 Job。

怎么处理异常
使用 try/cath 处理
Why should I use coroutines?
轻量级:可以同时启动成百上千个协程,但却不会阻塞主线程
更少的内存泄漏:使用结构化的并发在同一作用域内执行多个任务
内置取消操作:方便在页面销毁时释放资源,减少内存泄漏
Jetpack 支持:Room、Paging 等官方支持库全面支持协程
Coroutines ❤️️ View
大部分情况下,我们都会使用协程来处理多线程问题,比如网络、数据库等 IO 任务。其实协程也可以处理单线程的异步任务。单线程的异步任务听上去很陌生,但是你肯定用过它。Android 中最典型的单线程任务就是与 View 相关的一些操作,比如 View 的绘制和动画 。
Jetpack 中提供了很多扩展函数来帮我们简化代码,如 View.doOnPreDraw(),Animator.doOnEnd()
这些扩展函数只是帮我们将老的回调方式转化成了 Kotlin 友好的 lambda API,虽然看上去很优雅,但是并没有解决嵌套回调的问题。
suspendCancellableCoroutine
suspendCancellableCoroutine 可以从两个维度来执行协程的取消操作
- 在异步操作完成之前取消协程
- 在协程被挂起的时候,异步 UI 操作被取消或抛出异常
等待 View 布局完成

等待动画执行完成

实战演练
可能你会说这有什么用,不就是换了一种写法吗,不用协程也可以实现。是的,你确实可以实现,但碰到下面这种情况你就会觉得有用了。
需求:动画A和动画B同时执行,等两个动画都执行完成后,再执行动画C,动画C结束后再重复上述过程。
我们来看下不使用协程该怎么实现:


QA:
- 我能在 Java 代码中使用协程吗?
- 协程稳定吗,可以在生产环境中使用吗?
- 协程难不难,学习成本是不是很大?