携手创作,共同成长!这是我参与「日新计划 8 月更文挑战」的第5天,点击查看活动详情

前言

之前我们学了几个关于协程的基础知识,本文将继续分享Kotlin协程的知识点~挂起,同时介绍协程在Android开发中的使用。

正文

挂起

suspend关键字

说到挂起,那么就会离不开suspend关键字,它是Kotlin中的一个关键字,它的中文意思是暂停或者挂起。

以下是通过suspend修饰的方法:

suspend fun suspendFun(){
  withContext(Dispatchers.IO){
    //do db operate
   }
}

通过suspend关键字来修饰方法,限制这个方法只能在协程里边调用,否则编译不通过。

suspend方法其实本身没有挂起的作用,只是在方法体力便执行真正挂起的逻辑,也相当于提个醒。如果使用suspend修饰的方法里边没有挂起的操作,那么就失去了它的意义,也就是无需使用。

虽然我们无法正常去调用它,但是可以通过反射去调用:

suspend fun hello() = suspendCoroutine<Int> { coroutine ->
  Log.i(myTag,"hello")
  coroutine.resumeWith(kotlin.Result.success(0))
}
​
//通过反射来调用:
fun helloTest(){
  val helloRef = ::hello
  helloRef.call()
}
//抛异常:java.lang.IllegalArgumentException: Callable expects 1 arguments, but 0 were provided.
fun helloTest(){
  val helloRef = ::hello
  helloRef.call(object : Continuation<Int>{
    override val context: CoroutineContext
      get() = EmptyCoroutineContext
​
    override fun resumeWith(result: kotlin.Result<Int>) {
      Log.i(myTag,"result : ${result.isSuccess} value:${result.getOrNull()}")
     }
   })
}
//输出:hello

挂起与恢复

看一个方法:

public suspend inline fun <T> suspendCancellableCoroutine(
  crossinline block: (CancellableContinuation<T>) -> Unit
): T =
  suspendCoroutineUninterceptedOrReturn { uCont ->
    val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
    block(cancellable)
    cancellable.getResult()
   }

这是Kotlin协程提供的api,里边虽然只有短短的三句代码,但是实现甚是复杂而且关键。

继续跟进看看getResult()方法:

internal fun getResult(): Any? {
  installParentCancellationHandler()
  if (trySuspend()) return COROUTINE_SUSPENDED//返回此对象表示挂起
  
  val state = this.state
  if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)
  
  if (resumeMode == MODE_CANCELLABLE) {//检查
    val job = context[Job]
    if (job != null && !job.isActive) {
      val cause = job.getCancellationException()
      cancelResult(state, cause)
      throw recoverStackTrace(cause, this)
     }
   }
  return getSuccessfulResult(state)//返回结果
}

最后写一段代码,然后转为Java看个究竟:

fun demo2(){
  GlobalScope.launch {
    val user = requestUser()
    println(user)
    val state = requestState()
    println(state)
   }
}

编译后生成的代码大致流程如下:

 public final Object invokeSuspend(Object result) {
     ...
    Object cs = IntrinsicsKt.getCOROUTINE_SUSPENDED();//上面所说的那个Any:CoroutineSingletons.COROUTINE_SUSPENDED
    switch (this.label) {
      case 0:
        this.label = 1;
        user = requestUser(this);
        if(user == cs){
          return user
         }
        break;
      case 1:
        this.label = 2;
        user = result;
        println(user);
        state = requestState(this);
        if(state == cs){
          return state
         } 
        break;
      case 2:
        state = result;
        println(state)
        break;
     }
   }

当协程挂起后,然后恢复时,最终会调用invokeSuspend方法,而协程的block代码会封装在invokeSuspend方法中,使用状态来控制逻辑的实现,并且保证顺序执行。

通过以上我们也可以看出:

  • 本质上也是一个回调,Continuation
  • 根据状态进行流转,每遇到一个挂起点,就会进行一次状态的转移。

协程在Android中的使用

举个例子,使用两个UseCase来模拟挂起的操作,一个是网络操作,另一个是数据库的操作,然后操作ui,我们分别看一下代码上面的区别。

没有使用协程:

//伪代码
mNetworkUseCase.run(object: Callback {
    onSuccess(user: User) {
      mDbUseCase.insertUser(user, object: Callback{
        onSuccess() {
          MainExcutor.excute({
         tvUserName.text = user.name
         })
       }
     })
   }
})

我们可以看到,用回调的情况下,只要对数据操作稍微复杂点,回调到主线程进行ui操作时,很容易就嵌套了很多层,导致了代码难以看清楚。那么如果使用协程的话,这种代码就可以得到很大的改善。

使用协程:

private fun requestDataUseGlobalScope(){
  GlobalScope.launch(Dispatchers.Main){
    //模拟从网络获取用户信息
    val user = mNetWorkUseCase.requireUser()
    //模拟将用户插入到数据库
    mDbUseCase.insertUser(user)
    //显示用户名
    mTvUserName.text = user.name
   }
}

对以上函数作说明:

  • 通过GlobalScope开启一个顶层协程,并制定调度器为Main,也就是该协程域是在主线程中运行的。
  • 从网络获取用户信息,这是一个挂起操作
  • 将用户信息插入到数据库,这也是一个挂起操作
  • 将用户名字显示,这个操作是在主线程中。

由此在这个协程体中就可以一步一步往下执行,最终达到我们想要的结果。

如果我们需要启动的线程越来越多,可以通过以下方式:

private fun requestDataUseGlobalScope1(){
  GlobalScope.launch(Dispatchers.Main){
    //do something
   }
}
​
private fun requestDataUseGlobalScope2(){
  GlobalScope.launch(Dispatchers.IO){
    //do something
   }
}
​
private fun requestDataUseGlobalScope3(){
  GlobalScope.launch(Dispatchers.Main){
    //do something
   }
}

但是平时使用,我们需要注意的就是要在适当的时机cancel掉,所以这时我们需要对每个协程进行引用:

private var mJob1: Job? = null
private var mJob2: Job? = null
private var mJob3: Job? = nullprivate fun requestDataUseGlobalScope1(){
  mJob1 = GlobalScope.launch(Dispatchers.Main){
    //do something
   }
}
​
private fun requestDataUseGlobalScope2(){
  mJob2 = GlobalScope.launch(Dispatchers.IO){
    //do something
   }
}
​
private fun requestDataUseGlobalScope3(){
  mJob3 = GlobalScope.launch(Dispatchers.Main){
    //do something
   }
}

如果是在Activity中,那么可以在onDestroy中cancel掉

override fun onDestroy() {
  super.onDestroy()
  mJob1?.cancel()
  mJob2?.cancel()
  mJob3?.cancel()
}

可能你发现了一个问题:如果启动的协程不止是三个,而是更多呢?

没错,如果我们只使用GlobalScope,虽然能够达到我们的要求,但是每次我们都需要去引用他,不仅麻烦,还有一点是它开启的顶层协程,如果有遗漏了,则可能出现内存泄漏。所以我们可以使用kotlin协程提供的一个方法MainScope()来代替它:

private val mMainScope = MainScope()
​
private fun requestDataUseMainScope1(){
  mMainScope.launch(Dispatchers.IO){
    //do something
   }
}
private fun requestDataUseMainScope2(){
  mMainScope.launch {
    //do something
   }
}
private fun requestDataUseMainScope3(){
  mMainScope.launch {
    //do something
   }
}

可以看到用法基本一样,但有一点很方便当我们需要cancel掉所有的协程时,只需在onDestroy方法cancel掉mMainScope就可以了:

override fun onDestroy() {
  super.onDestroy()
  mMainScope.cancel()
}

MainScope()方法:

@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

使用的是一个SupervisorJob(制定的协程域是单向传递的,父传给子)和Main调度器的组合。

在平常开发中,可以的话使用类似于MainScope来启动协程。

结语

本文的分享到这里就结束了,希望能够给你带来帮助。关于Kotlin协程的挂起知识远不止这些,而且也不容易理清,还是会继续去学习它到底用哪些技术点,深入探究原理究竟是如何。