携手创作,共同成长!这是我参与「日新计划 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? = 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协程的挂起知识远不止这些,而且也不容易理清,还是会继续去学习它到底用哪些技术点,深入探究原理究竟是如何。
评论(0)