注册

分享Kotlin协程在Android中的使用

前言


之前我们学了几个关于协程的基础知识,本文将继续分享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? = null

private 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协程的挂起知识远不止这些,而且也不容易理清,还是会继续去学习它到底用哪些技术点,深入探究原理究竟是如何。


作者:敖森迪
链接:https://juejin.cn/post/7130427677432872968
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册