Android 协程使用指南

协程是什么 协程是我们在 Android 上进行异步编程的推荐解决方案之一,通过挂起和恢复让状态机状态流转实现把层层嵌套的回调代码变成像同步代码那样直观、简洁,协程的出现很好的避免了回调地狱的出现。
所谓挂起,是指挂起协程,而非挂起线程,并且这个操作对线程是非阻塞式的。当线程执行到协程的 suspend 函数的时候,对于线程而言,线程会被回收或者再利用执行其他工作,就像主线程其实是会继续 UI 刷新工作。而对于协程本身,会根据 withContext 传入的 Dispatchers 所指定的线程去执行任务。
关于恢复,当挂起函数执行完毕后,会自动根据 CoroutineContext 切回原来的线程往下执行。
协程怎样集成

dependencies { // -----1---- // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib:1.5.30"// -----2---- // 协程核心库 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1" // 协程 Android 支持库 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1"// -----3---- // lifecycle 对于协程的扩展封装 implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1" }

其中 part 3 主要是对写 view 层的一些库,lifecycle 对于协程的扩展封装在业务开发上非常重要。
下面,介绍一些使用上的一些基本概念
CoroutineScope CoroutineScope 是指协程作用域,它其实是一个接口,作用是使得协程运行在其范围内
public interface CoroutineScope { public val coroutineContext: CoroutineContext }public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] != null) context else context + Job())

执行协程代码块的还有 runBlocking,其只有当内部相同作用域的所有协程都运行结束后,声明在 runBlocking 之后的代码才能执行,即 runBlocking 会阻塞其所在线程,但其内部运行的协程又是非阻塞的,由于对线程有阻塞行为,日常开发中一般不会用到,多用于做单元测试,在此不展开说了。
下面看看官方自带的几种 CoroutineScope
1. GlobalScope
public object GlobalScope : CoroutineScope { override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext }public object EmptyCoroutineContext : CoroutineContext, Serializable { ... }

从源码可以看出,GlobalScope 是一个单例,该实例所用的 CoroutineContext 是一个 EmptyCoroutineContext 实例,且 EmptyCoroutineContext 也是一个单例,GlobalScope 对象没有和 view 的生命周期组件相关联,是全局协程作用域,需要自己管理 GlobalScope 所创建的 Coroutine,所以一般而言我们不直接使用 GlobalScope 来创建 Coroutine
2. Fragment/Activity 的 lifecycleScope
// LifecycleOwner.kt public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope get() = lifecycle.coroutineScope// Lifecycle.kt public val Lifecycle.coroutineScope: LifecycleCoroutineScope get() { while (true) { ... val newScope = LifecycleCoroutineScopeImpl( this, SupervisorJob() + Dispatchers.Main.immediate ) if (...) { newScope.register() return newScope } } }// Lifecycle.kt internal class LifecycleCoroutineScopeImpl( override val lifecycle: Lifecycle, override val coroutineContext: CoroutineContext ) : LifecycleCoroutineScope(), LifecycleEventObserver { ...fun register() { launch(Dispatchers.Main.immediate) { if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) { lifecycle.addObserver(this@LifecycleCoroutineScopeImpl) } else { coroutineContext.cancel() } } }override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { lifecycle.removeObserver(this) coroutineContext.cancel() } } }

从上面的 androidx.lifecycle.LifecycleCoroutineScopeImpl#registerandroidx.lifecycle.LifecycleCoroutineScopeImpl#onStateChanged 我们可以看出 lifecycleScope 使用的生命周期如下
// 开始 override fun onCreate(…)// 结束 override fun onDestroy()

3. Fragment 的 viewLifecycleScope
Fragment 其实并没有 viewLifecycleScope 的拓展属性,这里的 viewLifecycleScope 是指在 FragmentViewLifecycleScope,因为 Fragment 可以没有 View 我们可以给 Fragment 写一个拓展属性
val Fragment.viewLifecycleScope get() = viewLifecycleOwner.lifecycleScope

这里我们可以看看 viewLifecycleOwner 是什么
// Fragment.javavoid performCreateView(...) { mViewLifecycleOwner = new FragmentViewLifecycleOwner(this, getViewModelStore()); mView = onCreateView(inflater, container, savedInstanceState); if (mView != null) { // Initialize the view lifecycle mViewLifecycleOwner.initialize(); } else { if (mViewLifecycleOwner.isInitialized()) { throw new IllegalStateException("Called getViewLifecycleOwner() but " + "onCreateView() returned null"); } mViewLifecycleOwner = null; } }public LifecycleOwner getViewLifecycleOwner() { if (mViewLifecycleOwner == null) { throw new IllegalStateException("Can't access the Fragment View's LifecycleOwner when " + "getView() is null i.e., before onCreateView() or after onDestroyView()"); } return mViewLifecycleOwner; }

performCreateView 的调用是在创建 View 的时候,可以看出,如果我们没有复写 onCreateView,那么 mView 就会为 null,从而导致 mViewLifecycleOwnernull 而复写了就会,所以我们不应该在没有 ViewFragment 中使用 viewLifecycleScope,否则在 getViewLifecycleOwner 的时候就会抛异常。所以可以看看在复写 View 时候 viewLifecycleScope 使用的生命周期为
// 开始 override fun onCreateView(…): View?// 结束 override fun onDestroyView()

4. ViewModel 的 viewModelScope
// ViewModel.kt public val ViewModel.viewModelScope: CoroutineScope get() { val scope: CoroutineScope? = this.getTag(JOB_KEY) if (scope != null) { return scope } return setTagIfAbsent( JOB_KEY, CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) ) }internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope { override val coroutineContext: CoroutineContext = contextoverride fun close() { coroutineContext.cancel() } }//----------------------------------------------------// ViewModel.java T setTagIfAbsent(String key, T newValue) { ... synchronized (mBagOfTags) { previous = (T) mBagOfTags.get(key); if (previous == null) { mBagOfTags.put(key, newValue); } } ... return result; }final void clear() { ... if (mBagOfTags != null) { synchronized (mBagOfTags) { for (Object value : mBagOfTags.values()) { // see comment for the similar call in setTagIfAbsent closeWithRuntimeException(value); } } } onCleared(); }private static void closeWithRuntimeException(Object obj) { if (obj instanceof Closeable) { try { ((Closeable) obj).close(); } catch (IOException e) { throw new RuntimeException(e); } } }

从上面源码可以看出,viewModelScopelazy 的,调用的时候进行初始化,而 ViewModel#clear 方法是在 ViewModel 销毁的时候调用的,从而最终走到 CloseableCoroutineScope#close,使得协程被 cancel,所以可以得出,viewModelScope 的使用周期在 ViewModel 的生命周期内
Coroutine Builders Coroutine Builders 是指 kotlinx.coroutines.Builders.kt,其内部有 CoroutineScope 的一些拓展方法等,下面介绍一下 Builders 类中两个重要的拓展方法的作用
1. launch
public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job { val newContext = newCoroutineContext(context) val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine }

context:协程的上下文 start:协程的启动方式,默认值为 CoroutineStart.DEFAULT,即协程会在声明的同时就立即进入等待调度的状态,即可以立即执行的状态,CoroutineStart.LAZY 能实现延迟启动 block:协程的执行体 返回值为 Job,指当前协程任务的句柄
我们在 view 层进行执行协程时候,一般会这样用
viewLifecycleScope.launchWhenStarted { ... }

这其实就是个 launch,我们看看源码
/// Lifecycle.kt public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch { lifecycle.whenStarted(block) }

2. async
public fun CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T ): Deferred { val newContext = newCoroutineContext(context) val coroutine = if (start.isLazy) LazyDeferredCoroutine(newContext, block) else DeferredCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine }

async 返回值 Deferred 继承于 Job 接口,其主要是在 Job 的基础上扩展了 await 方法,是返回协程的执行结果,而 launch 返回的 Job 是不携带结果的
public interface Deferred : Job { public suspend fun await(): T public val onAwait: SelectClause1 public fun getCompleted(): T public fun getCompletionExceptionOrNull(): Throwable? }

CoroutineContext 协程的上下文,使用以下元素集定义协程的行为
  • Job:控制协程的生命周期
  • CoroutineDispatcher:将任务分发给适当的线程
  • CoroutineName:协程的名称,可用于辅助
  • CoroutineExceptionHandler:处理未捕获的异常
1. Job
在源码注释中,Job 有这样的描述 描述 1
State [isActive] [isCompleted] [isCancelled]
New (optional initial state) false false false
Active (default initial state) true false false
Completing (transient state) true false false
Cancelling (transient state) false false true
Cancelled (final state) false true true
Completed (final state) false true false
描述的是一个任务的状态:新创建 (New)、活跃 (Active)、完成中 (Completing)、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)。但我们无法直接方位这些状态,可以通过方位 Job 的几个属性:isActiveisCancelledisCompleted
描述 2
wait children +-----+ start+--------+ complete+-------------+finish+-----------+ | New | -----> | Active | ---------> | Completing| -------> | Completed | +-----++--------++-------------++-----------+ |cancel / fail| |+----------------+ || VV +------------+finish+-----------+ | Cancelling | --------------------------------> | Cancelled | +------------++-----------+

描述的是状态的流转,举个状态流转例子:当任务创建(New)后,协程处于活跃状态(Active),协程运行出错或者调用 job.cancel()(cancel / fail)都会将当前任务置为取消中 (Cancelling) 状态 (isActive = false, isCancelled = true),当所有的子协程都完成后,协程会进入已取消 (Cancelled) 状态,此时 isCompleted = true
我们再来认识一下 Job 的几个常用的方法
/// Job.kt /** * 启动 Coroutine, 当前 Coroutine 还没有执行调用该函数返回 true * 如果当前 Coroutine 已经执行或者已经执行完毕,则调用该函数返回 false */ public fun start(): Boolean/** * 取消当前任务,可以指定原因异常信息 */ public fun cancel(cause: CancellationException? = null)/** * 这个 suspend 函数会暂停当前所处的 Coroutine 直到该 Coroutine 执行完成。 * 所以 join 函数一般用来在另外一个 Coroutine 中等待 job 执行完成后继续执行。 * 当 Job 执行完成后,job.join 函数恢复,这个时候 job 这个任务已经处于完成状态 * 调用 job.join 的 Coroutine 还继续处于 activie 状态 */ public suspend fun join()/** * 通过这个函数可以给 Job 设置一个完成通知 */ public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle

1.1 Deferred Deferred 继承自 Job,是我们使用 async 创建协程的返回值,我们看看 Deferred 基于 Job 拓展的几个方法
public interface Deferred : Job {/** * 用来等待这个 Coroutine 执行完毕并返回结果 */ public suspend fun await(): T/** * 用来获取Coroutine执行的结果 * 如果Coroutine还没有执行完成则会抛出 IllegalStateException * 如果任务被取消了也会抛出对应的异常 * 所以在执行这个函数前可以通过 isCompleted 来判断一下当前任务是否执行完毕了 */ @ExperimentalCoroutinesApi public fun getCompleted(): T/** * 获取已完成状态的 Coroutine 异常信息 * 如果任务正常执行完成了,则不存在异常信息,返回 null */ @ExperimentalCoroutinesApi public fun getCompletionExceptionOrNull(): Throwable? }

1.2 SupervisorJob
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)

SupervisorJob 是一个顶层函数,里面的子 Job 不相互影响,一个子 Job 失败了,不影响其他子 Job,可以看到有个 parent 入参,如果指定了这个参数,则所返回的 Job 就是参数 parent 的子 Job
2. CoroutineDispatcher
定义任务的线程
  • Dispatchers.Default
默认的调度器,适合处理后台计算,是一个CPU密集型任务调度器,使用一个共享的后台线程池来运行里面的任务,任务执行在子线程
  • Dispatchers.IO
和 Default 共用一个共享的线程池来执行里面的任务,区别在最大并发数不同,用途在阻塞 IO 操作
  • Dispatchers.Unconfined
未定义线程池,所以执行的时候默认在启动线程,也就是在哪个线程启动就在哪个线程执行
  • Dispatchers.Main
主线程
协程项目使用场景 1. 回调变协程
以执行多个动画为例,场景是点击某个按钮要切换到其他图标。 首先将 suspendCancellableCoroutine 封装一下,这个方法的作用是将回调变协程,但是我们需要控制其释放
class ContinuationHolder(continuation: CancellableContinuation) { var continuation: CancellableContinuation? private setinit { this.continuation = continuation continuation.invokeOnCancellation { this.continuation = null } } }/** * 避免continuation泄漏 */ suspend inline fun suspendCancellableCoroutineRefSafe( crossinline block: (ContinuationHolder) -> Unit ): T = suspendCancellableCoroutine { val continuationHolder = ContinuationHolder(it) block(continuationHolder) }

接下来就可以使用 suspendCancellableCoroutineRefSafe,看看怎样来将一个回调处理改装成协程
private suspend fun viewScaleAnimator(view: View, duration: Long, vararg values: Float): Boolean { return suspendCancellableCoroutineRefSafe { holder -> val animatorSet = AnimatorSet() animatorSet.play(ObjectAnimator.ofFloat(view, "scaleX", *values)) .with(ObjectAnimator.ofFloat(view, "scaleY", *values)) animatorSet.duration = duration animatorSet.addListener(object : Animator.AnimatorListener { override fun onAnimationStart(animation: Animator?) { }override fun onAnimationEnd(animation: Animator?) { holder.continuation?.resume(true) }override fun onAnimationCancel(animation: Animator?) { holder.continuation?.resume(false) }override fun onAnimationRepeat(animation: Animator?) { } }) animatorSet.start() } }

【Android 协程使用指南】viewScaleAnimator 方法是将一个缩放动画变成协程的处理,返回动画执行的结果 这样,我们就可以顺序的执行多个动画了
val animator1End = viewScaleAnimator(imageView, 100, 1f, 0.75f) if (animator1End) { imageView.setImageResource(nextImage) val animator2End = viewScaleAnimator(imageView, 100, 0.75f, 1f) if (animator2End) { onAllAnimationEnd.invoke() } }

2. IO 异步处理
以下载了文件后需要解压为例
/** * 异步解压文件 */ suspend fun unZipFolderAsync(zipFileString: String, outPathString: String) = withContext(Dispatchers.IO) { unZipFolder(zipFileString, outPathString) } internal fun unZipFolder(zipFileString: String, outPathString: String) { // FileInputStream、ZipInputStream 等的一些操作 ... }

5. 自定义 CoroutineScope
官方的 CoroutineScope 并不能满足所有场景,所以这时候我们可以自定义 CoroutineScope
class MyRepository {private var mScope: CoroutineScope? = null/** * 打开的时候调用 */ fun initScope() { mScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) }/** * 操作 */ private fun handle() { mScope?.launch { ... } }/** * 退出时候调用 */ fun exit() { mScope?.cancel() mScope = null ... } }

协程项目踩坑案例 1. 在 Fragment 中,lifecycleScope 和 viewLifecycleScope 分不清用哪个
viewLifecycleScope 强调的是 View 生命周期内的协程执行范围
  • 在无 UI 的逻辑 fragment 中使用 viewLifecycleScope 会抛异常
  • 在不考虑 View 回收,如横竖屏切换,需要 keep 住一些状态可以使用 lifecycleScope
  • 需要跟 Fragment 生命周期的用 lifecycleScope
  • View 创建回收时机有关系的用 viewLifecycleScope
  • 大多数情况下使用 viewLifecycleScope
2. CoroutineScope 和 Job 的 cancel 问题
CoroutineScope cancelJob 会跟着 cancelJob cancelCoroutineScope 未必需要 cancelCoroutineScope cancelJob 就不活跃了。 Jobcancel 场景其中要注意的有:例如我们 collect 一个返回值为 StateFlow 的方法,其实该方法在执行了 trymit 处理完状态后,该协程并未执行完毕,而是始终在等待中,所以我们可以在 collect 内部检测到任务执行完了,就主动将当前 Job cancel 掉,可以避免浪费内存开销。结合上面提到的回调变协程,例子如下
private fun ...(...) { ... mAnimatorJob = mAnimatorScope?.launch { val animator1End = viewScaleAnimator(imageView, 100, 1f, 0.75f) if (animator1End) { imageView.setImageResource(nextImage) val animator2End = viewScaleAnimator(imageView, 100, 0.75f, 1f) if (animator2End) { onAllAnimationEnd.invoke() } // 注意!这里做了 Job 的 cancel mAnimatorJob?.cancel() mAnimatorJob = null } } }override fun onDestroy() { ... // fragment 销毁,未处理完任务也应该销毁 mAnimatorJob?.cancel() mAnimatorJob = null mAnimatorScope?.cancel() mAnimatorScope = null }

挂起和切线程的原理 挂起原理
前面介绍挂起的时候提到挂起操作是非阻塞式的,那么我们来看看协程是怎样做到的。 我们先看看一个小例子
class TestClass { suspend fun test1() { test2() }suspend fun test2() { } }

我们看看这个类的字节码
public final class TestClass { @Nullable public final Object test1(@NotNull Continuation $completion) { Object var10000 = this.test2($completion); return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE; }@Nullable public final Object test2(@NotNull Continuation $completion) { return Unit.INSTANCE; } }

可以看到,挂起函数主要用到了 Continuation
public interface Continuation { public val context: CoroutineContext public fun resumeWith(result: Result) }

这么看,实际挂起函数用到了类似于 callback 的逻辑了,resumeWith 相当于 callback 中一个回调函数,其作用是执行接下来要执行的代码,可以理解成在 resumeWith 回调里面继续执行下一步。 而我们在协程外是无法调用的,这里可以看出因为需要传递一个 NotNullContinuation
切线程原理
接下来讲下切线程,在项目开发中,遇到切线程的比较多的做法 withContext,下面讲述其中原理
public suspend fun withContext( context: CoroutineContext, block: suspend CoroutineScope.() -> T ): T { return suspendCoroutineUninterceptedOrReturn sc@ { uCont -> // 创建新的context val oldContext = uCont.context val newContext = oldContext + context .... // 使用新的Dispatcher,覆盖外层 val coroutine = DispatchedCoroutine(newContext, uCont) coroutine.initParentJob() //DispatchedCoroutine作为了complete传入 block.startCoroutineCancellable(coroutine, coroutine) coroutine.getResult() } }private class DispatchedCoroutine( context: CoroutineContext, uCont: Continuation ) : ScopeCoroutine(context, uCont) { // 在complete时会会回调 override fun afterCompletion(state: Any?) { afterResume(state) }override fun afterResume(state: Any?) { // uCont就是父协程,context 仍是老版 context, 因此可以切换回原来的线程上 uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont)) } }

传入的新的 CoroutineContext 会覆盖原来所在的 CoroutineContextDispatchedCoroutine 作为 complete: Continuation 传入协程体的创建函数中,因此协程体执行完成后会回调到 afterCompletion 中,DispatchedCoroutine 中传入的 uCont 是父协程,它的拦截器仍是外层的拦截器,因此会切换回原来的线程中
后话 思考:协程设计思想
  • 我认为,协程可以使得一个复杂的操作变得可追踪结果,如果这个复杂操作既涉及到异步操作场景,更为显著,将一个完整的操作变得可追踪,业务逻辑上很清晰。
参考:
  • developer.android.com/kotlin/coro…
  • juejin.cn/post/695061…

    推荐阅读