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