协程是什么
协程是咱们在 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#register
和 androidx.lifecycle.LifecycleCoroutineScopeImpl#onStateChanged
咱们能够看出 lifecycleScope
应用的生命周期如下
// 开始 override fun onCreate(…) // 完结 override fun onDestroy()
3. Fragment 的 viewLifecycleScope
Fragment
其实并没有 viewLifecycleScope
的拓展属性,这里的 viewLifecycleScope
是指在 Fragment
对 View
的 LifecycleScope
,因为 Fragment
能够没有 View
咱们能够给 Fragment
写一个拓展属性
val Fragment.viewLifecycleScope get() = viewLifecycleOwner.lifecycleScope
这里咱们能够看看 viewLifecycleOwner
是什么
// Fragment.java void 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
,从而导致 mViewLifecycleOwner
为 null
而复写了就会,所以咱们不应该在没有 View
的 Fragment
中应用 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 = context override fun close() { coroutineContext.cancel() } } //---------------------------------------------------- // ViewModel.java <T> 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); } } }
从下面源码能够看出,viewModelScope
是 lazy
的,调用的时候进行初始化,而 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 <T> CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T ): Deferred<T> { val newContext = newCoroutineContext(context) val coroutine = if (start.isLazy) LazyDeferredCoroutine(newContext, block) else DeferredCoroutine<T>(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine }
async
返回值 Deferred
继承于 Job
接口,其次要是在 Job
的根底上扩大了 await
办法,是返回协程的执行后果,而 launch
返回的 Job
是不携带后果的
public interface Deferred<out T> : Job { public suspend fun await(): T public val onAwait: SelectClause1<T> 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 的几个属性:isActive 、isCancelled 和 isCompleted |
形容 2
wait children +-----+ start +--------+ complete +-------------+ finish +-----------+ | New | -----> | Active | ---------> | Completing | -------> | Completed | +-----+ +--------+ +-------------+ +-----------+ | cancel / fail | | +----------------+ | | V V +------------+ 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<out T> : 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<T>(continuation: CancellableContinuation<T>) { var continuation: CancellableContinuation<T>? private set init { this.continuation = continuation continuation.invokeOnCancellation { this.continuation = null } } } /** * 防止continuation透露 */ suspend inline fun <T> suspendCancellableCoroutineRefSafe( crossinline block: (ContinuationHolder<T>) -> 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() } }
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 cancel
了 Job
会跟着 cancel
,Job cancel
了 CoroutineScope
未必须要 cancel
,CoroutineScope cancel
后 Job
就不沉闷了。 Job
的 cancel
场景其中要留神的有:例如咱们 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<in T> { public val context: CoroutineContext public fun resumeWith(result: Result<T>) }
这么看,理论挂起函数用到了相似于 callback
的逻辑了,resumeWith
相当于 callback
中一个回调函数,其作用是执行接下来要执行的代码,能够了解成在 resumeWith
回调外面继续执行下一步。 而咱们在协程外是无奈调用的,这里能够看出因为须要传递一个 NotNull
的 Continuation
。
切线程原理
接下来讲下切线程,在我的项目开发中,遇到切线程的比拟多的做法 withContext
,上面讲述其中原理
public suspend fun <T> 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<in T>( context: CoroutineContext, uCont: Continuation<T> ) : ScopeCoroutine<T>(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…