乐趣区

关于android: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.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,从而导致 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 = 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);
            }
        }
    }

从下面源码能够看出,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 <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 的几个属性:isActiveisCancelledisCompleted

形容 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 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<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

这么看,理论挂起函数用到了相似于 callback 的逻辑了,resumeWith 相当于 callback 中一个回调函数,其作用是执行接下来要执行的代码,能够了解成在 resumeWith 回调外面继续执行下一步。而咱们在协程外是无奈调用的,这里能够看出因为须要传递一个 NotNullContinuation

切线程原理

接下来讲下切线程,在我的项目开发中,遇到切线程的比拟多的做法 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…
退出移动版