Kotlin中协程封装与取消

1. 将java中的回调接口封装成挂起函数

  • 以OKHttp请求为例,代码如下 :
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

suspend fun Call.await(): String = suspendCoroutine { block ->
    enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            block.resumeWithException(e)
        }

        override fun onResponse(call: Call, response: Response) {
            if (response.isSuccessful) {
                block.resume(response.body()!!.string())
            }
        }
    })
}

  • 挂起函数的使用如下:
val client: OkHttpClient = OkHttpClient.Builder() .build()
        val request: Request = Request.Builder()
            .get()
            .url("http://xxx")
            .build()
  GlobalScope.async(Dispatchers.Main) {
            val call = client.newCall(request)
            val data = call.await()
        }

因为Call的扩展函数await()是个挂起函数,所以必须运行在协程或者其他的挂起函数中

  • 对OkHttp的Call类继续扩展,添加一个await()函数,函数的返回值为 String类型,在函数中使用挂起函数suspendCoroutine{}或suspendCancellableCoroutine{},await()函数调用时,首先会挂起当前协程,然后执行enqueue将网络请求放入队列中,当请求成功时,通过block.resume(response.body()!!.string())来恢复之前的协程。

  • suspendCancellableCoroutine{}函数实现功能的函数:

delay()函数

yield()函数:挂起当前协程,然后将协程分发到 Dispatcher 的队列,这样可以让该协程所在线程或线程池可以运行其他协程逻辑,然后在 Dispatcher 空闲的时候继续执行原来协程。简单的来说就是让出自己的执行权,给其他协程使用,当其他协程执行完成或也让出执行权时,一开始的协程可以恢复继续运行

2. 父子协程

  • GlobalScope.launch()函数和GlobalScope.async()函数是没有父协程的被称为全局协程。

  • 当一个协程被其它协程在 CoroutineScope 中启动的时候, 它将通过 CoroutineScope.coroutineContext 来承袭上下文,并且这个新协程的 Job 将会成为父协程任务的子任务。当一个父协程被取消的时候,所有它的子协程也会被递归的取消。例子如下:

GlobalScope.launch(Dispatchers.Main) {
            //父协程
            launch { 
                //子协程
            }
            async {
                //子协程 
            }
            withContext(coroutineContext){
                //子协程
            }
        }
  • 协程间父子关系有三种影响:

    • 父协程手动调用cancel()或者异常结束,会立即取消它的所有子协程。

    • 父协程必须等待所有子协程完成(处于完成或者取消状态)才能完成。

    • 子协程抛出未捕获的异常时,默认情况下会取消其父协程。

3. 协程取消

  • 实例1:
fun main() = runBlocking {
    val job = launch {
        launch {
            repeat(6) {
                println("run_child:$it")
                delay(500)
            }
        }
        repeat(6) {
            println("run_parent:$it")
            delay(500)
        }
    }
    delay(1600)
    job.cancel()
    println("job end")

}
  • 结果

  • 上面代码中 job 取消后,delay()会检测协程是否已取消,所以 job.cancel()调用之后上面的父协程和子协程也都会取消

  • 实例2:

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        var nextTime = 0L
        var i = 1
        while (i <= 3) {
            val nowTime = System.currentTimeMillis()
            if (nowTime >= nextTime) {
                println("parent_${i++}")
                nextTime = nowTime + 500L
            }
        }
    }
    delay(800)
    job.cancel()
    println("job end")
}
  • 结果

  • 上面代码中 job 取消后,没有检测协程状态的逻辑,都是计算逻辑,所以 job 的运算逻辑还是会继续运行。

  • 为了可以及时取消协程的运算逻辑,可以检测协程的状态,使用isActive来判断,上面示例中可以将while(i <= 3)替换为while(isActive)。代码如下:

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        var nextTime = 0L
        var i = 1
        while (isActive) {
            val nowTime = System.currentTimeMillis()
            if (nowTime >= nextTime) {
                println("parent_${i++}")
                nextTime = nowTime + 500L
            }
        }
    }

    delay(1200)
    job.cancel()
    println("job end")

}
  • 结果

  • 运行不能取消的代码块当手动取消协程后,像delay()这样的可取消挂起函数会在检测到已取消状态时,抛出 CancellationException 异常,然后退出协程。此时可以使用try { ... } finally { ... }表达式或<T : Closeable?, R> T.use {}函数执行终结动作或关闭资源。但是如果在finally块中调用自定义的或系统的可取消挂起函数,都会再次抛出 CancellationException 异常。通常我们在finally块中关闭一个文件,取消一个任务或者关闭一个通信通道都是非阻塞,并且不会调用任何挂起函数。当需要挂起一个被取消的协程时,可以将代码包装在withContext(NonCancellable) { ... }中。

4.超时取消

  • 协程库中已经提供来withTimeout() { ... }挂起函数来实现在超时后自动取消协程。它会在超时后抛出TimeoutCancellationException,它是 CancellationException 的子类,它是协程结束的正常原因,不会打印堆栈跟踪信息,如果在取消后需要执行一些关闭资源的操作可以使用前面提到的try { ... } finally { ... }表达式。
try {
    withTimeout(1300L) {
        repeat(1000) { i ->
                println("I'm sleeping $i ...")
            delay(500L)
        }
    }
} finally {
    println("I'm running finally")
}
  • 还有一个withTimeoutOrNull() { ... }挂起函数在超时后返回null,而不是抛出一个异常。
val result = withTimeoutOrNull(1300L) {
    repeat(1000) { i ->
            println("I'm sleeping $i ...")
        delay(500L)
    }
    "Done" /* 在它运行得到结果之前取消它,如果循环次数x延迟时间小等于1300则返回Done,否则返回null*/
}
println("Result is $result")
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容