Android—Kotiln进阶教程(二)

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

前言

在上一篇中,对Kotlin协程进行了初步认识。在这篇中,将会讲解Kotlin对应取消超时组合挂起函数!

话不多说,直接开始!

1、挂起函数

先看上一篇的例子:

fun main() = runBlocking<Unit> {// this : CoroutineScope
    launch {
        delay(1000L)
        println("Kotlin!")
    }
    println("Hello,")
}
复制代码

这是上一篇的例子,这里我们看到在launch 闭包里,填写了我们实现的逻辑,但是在真实使用中,大多数都是将对应逻辑放在方法里,然后调用对应方法。

因此,我们按照真实使用情况继续试试看效果:

fun main() = runBlocking<Unit> {
    launch { doKotlin() }
    println("Hello,")
}

fun doKotlin(){
    delay(1000L) //这句代码报错
    println("Kotlin!")
}
复制代码

我们发现这样写居然报错了!难道不支持这样写么?上面有提示,点一下看看!

fun main() = runBlocking<Unit> {
    launch { doKotlin() }
    println("Hello,")
}
// 这是你的第⼀个挂起函数
suspend fun doKotlin(){
    delay(1000L)
    println("Kotlin!")
}
复制代码

现在代码没报错了,运行下看看效果:

Hello,
Kotlin!
复制代码

OK,我们看到方法前加了一个额外的suspend 关键字,这样的方法才能在协程里面内部调用!

那如果说,非协程调用这方法会怎样呢?

suspend fun doKotlin(){
    delay(1000L)
    println("Kotlin!")
}


fun main(){
    doKotlin() //这句话报错,
}
复制代码

这样写还是报错!编译器提示我们需要将main函数也额外加上suspend 关键字!

到这,我们应该能清晰的认知到,当遇到带有suspend 关键字的方法时,要第一时间想到,这个方法仅仅只能在协程里面调用的!

2、验证协程轻量性

在上一篇提到过:协程实际上是⼀个轻量级的线程,可以挂起并稍后恢复。

那么在这就来验证一下协程轻量性具体在哪!

fun main() = runBlocking<Unit> {
    repeat(100000){
        launch {
            delay(5000)
            print(".")
        }
    }
}
复制代码

这里使用了repeat关键字,来看看它是什么意思:

public inline fun repeat(times: Int, action: (Int) -> Unit) {
    contract { callsInPlace(action) }

    for (index in 0 until times) {
        action(index)
    }
}
复制代码

通过这段代码,能够清楚的认知到,里面就是一个循环,循环内容就是对应闭包里面的内容!

也就是说:这里通过repeat(100000)启动了十万个协程,让每个协程在5秒后统一打印这个小数点。

来看看运行效果:

1.png

如图所示

当运行等待5秒后,这些小数点是在一瞬间统一出来的。

如果说用java线程来实现,很有可能直接产生内存不足的错误!

所以说,协程是一个轻量性线程!

不仅如此,一个全局协程就像是一个守护线程:在 GlobalScope 中启动的活动协程并不会使进程保活。

3、全局协程像守护线程

fun main() = runBlocking<Unit> {
    GlobalScope.launch {
        repeat(1000){ i ->
            println("I'm sleeping $i")
            delay(10L)
        }
    }
    delay(130L)
    println("over ")
}
复制代码

现在我们看到:通过GlobalScope.launch创建了对应的活动协程,里面每隔10L将打印对应的1000次循环输出。在活动协程外面只挂起了130L,最后执行打印!

来看看运行效果:

I'm sleeping 0
I'm sleeping 1
I'm sleeping 2
I'm sleeping 3
I'm sleeping 4
I'm sleeping 5
I'm sleeping 6
I'm sleeping 7
I'm sleeping 8
I'm sleeping 9
I'm sleeping 10
I'm sleeping 11
over 
复制代码

我们看到:当主线程完毕时,在 GlobalScope 中启动的活动协程并不会使进程保活。

那如果说将GlobalScope 关键字删除将会是怎样呢?

fun main() = runBlocking<Unit> {
    launch {
        repeat(1000){ i ->
            println("I'm sleeping $i")
            delay(10L)
        }
    }
    delay(130L)
    println("over ")
}
复制代码

运行效果

2.png

如图所示

这里我们看到:当主线程执行完毕时,非全局协程依然会继续执行,直到协程执行结束,整个主线程才会完毕。

那如果说,我想要非全局协程在主线程执行完毕时,也强制结束该怎么办呢?

4、取消协程的执行

fun main() = runBlocking<Unit> {
    val job = launch {
        repeat(1000){ i ->
            println("job:I'm sleeping $i")
            delay(10L)
        }
    }
    delay(130L)
    println("main: I'm tired of waiting!")
    job.cancel()
    job.join()
    println("main:Now I can quit!")
}
复制代码

运行效果

job:I'm sleeping 0
job:I'm sleeping 1
job:I'm sleeping 2
job:I'm sleeping 3
job:I'm sleeping 4
job:I'm sleeping 5
job:I'm sleeping 6
job:I'm sleeping 7
job:I'm sleeping 8
job:I'm sleeping 9
job:I'm sleeping 10
job:I'm sleeping 11
main: I'm tired of waiting!
main:Now I can quit!
复制代码

我们这里看到:

  • 在代码里加了job.cancel()job.join()
  • 顾名思义job.cancel()肯定是对应取消操作
  • job.join()在上一篇中提过,这个是等待协程执行完毕
  • 也就是说,这里先是取消对应的协程,然后等待协程所有操作全部执行完毕(包括取消)执行完毕时
  • 就可以执行最后那句main:Now I can quit!打印了
  • 当然官方额外提供了 job.cancelAndJoin()方法,将两者合成一个方法
fun main() = runBlocking<Unit> {
    val job = launch {
        repeat(1000){ i ->
            println("job:I'm sleeping $i")
            delay(10L)
        }
    }
    delay(130L)
    println("main: I'm tired of waiting!")
//    job.cancel()
//    job.join()
    job.cancelAndJoin()
    println("main:Now I can quit!")
}
复制代码

运行结果和上面一致!

那是不是所有的协程都能像刚刚那样还没执行完就立马取消掉?

当然肯定不是的!

5、协程的取消是协作的

fun main() = runBlocking<Unit> {
    val startTime = System.currentTimeMillis()
    val job = launch {
        var nextPrintTime = startTime
        var i = 0
        //这里没有挂起函数
        while(i < 50){  // ⼀个执⾏计算的循环,只是为了占⽤ CPU
            if(System.currentTimeMillis() >= nextPrintTime){
                println("job:I'm sleeping ${i++} ...")
                nextPrintTime += 10L
                //delay(1L) //分析1
            }
        }
    }
    delay(130L)
    println("main:I'm tired of waiting!")
    job.cancelAndJoin()
    println("main:Now I can quit.")
}
复制代码

运行效果

job:I'm sleeping 0 ...
...略
...略
job:I'm sleeping 49 ...
main:I'm tired of waiting!
main:Now I can quit.
复制代码

这里没有使用delay挂起函数,发现主线程只能等待协程执行完了后才能关闭。

那这时取消分析1的注释试试看效果:

job:I'm sleeping 0 ...
...略
job:I'm sleeping 15 ...
main:I'm tired of waiting!
main:Now I can quit.
复制代码

这里可以看到当取消分析1的注释,即使挂起1毫秒,取消时也会像上一个例子一样强制取消;

所以说:当没有使用delay挂起函数时,如果协程正在执⾏计算任务,并且没有检查取消的话,那么它是不能被取消的。

这里提到,没有检查取消。 那看看检查取消是怎么使用的呢?

6、检查取消

fun main() = runBlocking<Unit> {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        //isActive 是⼀个可以被使⽤在 CoroutineScope 中的扩展属性
        while(i < 50 && isActive){
            if(System.currentTimeMillis() >= nextPrintTime){
                println("job:I'm sleeping ${i++} ...")
                nextPrintTime += 10L
            }
        }
    }
    delay(130L)
    println("main:I'm tired of waiting!")
    job.cancelAndJoin()
    println("main:Now I can quit.")
}
复制代码

可以看到这里launch()里面多了Dispatchers.Default,随后在while多了isActive判断。

先来看看运行效果

运行效果

job:I'm sleeping 0 ...
...略
...略
job:I'm sleeping 15 ...
main:I'm tired of waiting!  //这句打印,可能在16 后面,可能在16 前面
job:I'm sleeping 16 ...
main:Now I can quit.
复制代码

从这运行效果可以看到:

  • 当执行了job.cancelAndJoin()代码时,对应通过launch(Dispatchers.Default) 创建的协程里面isActive属性将为false
  • 这样主线程结束时,对应无挂起函数的非主协程也能结束运行!

结束语

好了,本篇到这里差不多结束了!相信你对协程挂起以及取消操作有了一定的认知。在下一篇中,将会继续深一步讲解协程!

Guess you like

Origin juejin.im/post/7031838392992153631