Android Kotlin 协程(挂起函数+协程作用域)

470 阅读3分钟

本文简洁明了的介绍了协程的基础概念、挂起函数、作用域,附有演示代码以加深理解。

协程关键字

  • CoroutineScope 协程的作用域
  • CoroutineDispatcher 协程的调度器
  • CoroutineContext 协程上下文

挂起函数

概念:使用关键词suspend修饰

delay

将当前协程挂起指定时间,但不会阻塞线程,必须在协程的作用域或者其他挂起函数中执行

withContext

必须在协程的作用域中调用,必须指定协程的上下文,函数的最后一行是返回值

GlobalScope.launch {
    withContext(Dispatchers.Default) {
        delay(2000)
        "return string"
    }
}

//运行结果:
return string

yield

挂起当前的协程,将当前协程分发到CoroutineDispatcher队列(当前协程的父协程),等待其他协程执行完/挂起,再执行先前的协程。

fun main(args: Array<String>) = runBlocking {
    launch {
        println("1")
        yield()
        println("2")
    }
    launch {
        println("3")
        yield()
        println("4")
    }
    delay(5000)
}

//运行结果:
1
3
2
4

协程上下文

NonCancellable

始终处于活动状态的不可取消的任务,配合withContext一起使用。

使用场景:已经被cancel的协程,可以使用withContext(NonCancellable)继续执行协程中的任务, 用于重置对象及清理工作。

fun main(args: Array<String>) = runBlocking {
    val job1 = GlobalScope.launch {
        try {
            delay(1000)
            println("job 1")
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            withContext(NonCancellable) {
                delay(1000)
                println("job: I'm running finally")
            }
        }
    }
    job1.cancel()
    delay(5000)
}

//运行结果:
job: I'm running finally

可以看到

  • 正在运行的协程被cancel后,会报异常JobCancellationException,可看到println("job 1")没有执行到
  • finally中的代码有正常执行,有打印日志(job: I'm running finally),可使用withContext(NonCancellable)创建协程做一些清理工作

协程作用域

父子协程

A协程是使用B协程的CoroutineContext创建的,那么B就是A的父协程。协程默认是串行执行的,父协程会等待子协程执行完毕。

val job = GlobalScope.launch {
    val job1 = launch {
        println("job 1 start")
        delay(1000)
        println("job 1 end")
    }
    val job2 = GlobalScope.launch {
        println("job 2 start")
        delay(1000)
        println("job 2 end")
    }
}
job.cancel()

//运行结果:
job 1 start
job 2 start
job 2 end

结论:父协程取消后,子协程的任务也会被取消

以下三种方式创建的都是子协程

  • withContext(Dispatchers.Default) { }
  • launch { }
  • launch(coroutineContext) { }

以下两种方式创建的则不是子协程

  • CoroutineScope(Dispatchers.Default)
  • GlobalScope.launch { }

CoroutineContext +

CoroutineContext使用+,使一个协程具有多个CoroutineContext特性

val job = GlobalScope.launch {
    val job1 = GlobalScope.launch(Dispatchers.Default + coroutineContext) {
        println("job 1 start")
        delay(500)
        println("job 1 end")
    }
}
job.cancel()

//运行结果:
job 1 start

说明:使用 + 后,job1变成job的子协程,job被cancel后,job1也被停止了

去掉 + coroutineContext试一下

val job = GlobalScope.launch {
    val job1 = GlobalScope.launch(Dispatchers.Default) {
        println("job 1 start")
        delay(500)
        println("job 1 end")
    }
}
job.cancel()

//运行结果:
job 1 start
job 1 end

结论:两个协程没有父子关系,互不影响,job取消后job1还可以继续执行

CoroutineContext+Job

使用CoroutineContext+Job后,可以使用Job统一管理协程

val job = Job()
val job1 = GlobalScope.launch(Dispatchers.Default + job) {
    println("job 1 start")
    delay(500)
    println("job 1 end")
}
job.cancel()

//运行结果:
job 1 start

结论:job取消后,job1也被取消了。在 Android 开发中,Activity/Fragment 可以通过创建一个 Job 对象,并让该 Job 管理其他的协程。等到退出 Activity/Fragment 时,调用 job.cancel() 来取消协程的任务。

GlobalScope

主要分享GlobalScope的易错点,以下是错误的写法:

fun main(args: Array<String>) {
    try {
        GlobalScope.launch {
            println(doSomething())
        }
    } catch (e:Exception) {

    }
    //一定要加这行代码,不然main都执行结束了,代码还没执行到doSomething()方法
    Thread.sleep(100)
}

suspend fun doSomething(): String = withContext(Dispatchers.Default) {
    throw RuntimeException("this is an exception")
    "doSomething..."
}

此代码会抛出异常,try catch并没有捕获到异常,应该写在GlobalScope.launch {}里面

正确的写法:

fun main(args: Array<String>) {
    GlobalScope.launch {
        try {
            println(doSomething())
        } catch (e:Exception) {
        }
    }
    Thread.sleep(100)
}

suspend fun doSomething(): String = withContext(Dispatchers.Default) {
    throw RuntimeException("this is an exception")
    "doSomething..."
}

参考文档:

[Kotlin协程取消] (juejin.cn/post/704371…)

Android 进阶:基于 Kotlin 的 Android App 开发实践