Android kotlin 第五篇协程

732 阅读10分钟

一个并发管理工具,

协程几大要素: 挂起函数、作用域、上下文、建造器(启动器)

  • suspend function。即挂起函数,delay() 就是协程库提供的一个用于实现非阻塞式延时的挂起函数

  • CoroutineScope。即协程作用域,GlobalScope 是 CoroutineScope 的一个实现类,用于指定协程的作用范围,可用于管理多个协程的生命周期,所有协程都需要通过 CoroutineScope 来启动

  • CoroutineContext。即协程上下文,包含多种类型的配置参数。Dispatchers.IO 就是 CoroutineContext 这个抽象概念的一种实现,用于指定协程的运行载体,即用于指定协程要运行在哪类线程上

  • CoroutineBuilder。即协程构建器,协程在 CoroutineScope 的上下文中通过 launch、async 等协程构建器来进行声明并启动。launch、async 均被声明为 CoroutineScope 的扩展方法

异步注意点: 结果传递、线程切换、异常处理、取消响应、复杂分支

  1. 结果传递
  2. 线程切换
  3. 异常处理
  4. 取消响应
  5. 复杂分支

结果传递一般通过 Flow 或者 Channel 来处理协程键数据传递

其余几个通过协程几大要素共同完成

协程作用域 CoroutineScope

CoroutineScope(Dispatchers.Main).launch {
    val result = network()//挂起函数
    visibleState(result)
}

suspend fun network():String {
    delay(2000)
    return "json"
}

CoroutineScope 即 协程作用域,用于对协程进行追踪。如果我们启动了多个协程但是没有一个可以对其进行统一管理的途径的话,就会导致我们的代码臃肿杂乱,甚至发生内存泄露或者任务泄露。为了确保所有的协程都会被追踪,Kotlin 不允许在没有 CoroutineScope 的情况下启动协程。CoroutineScope 可被看作是一个具有超能力的 ExecutorService 的轻量级版本。它能启动协程,同时这个协程还具备上文所说的 suspend 和 resume 的优势

所有的协程都需要通过 CoroutineScope 来启动,它会跟踪通过 launchasync 创建的所有协程,你可以随时调用 scope.cancel() 取消正在运行的协程。CoroutineScope 本身并不运行协程,它只是确保你不会失去对协程的追踪,即使协程被挂起也是如此。在 Android 中,某些 ktx 库为某些生命周期类提供了自己的 CoroutineScope,例如,ViewModel 有 viewModelScope,Lifecycle 有 lifecycleScope

CoroutineScope 大体上可以分为三种:

  • GlobalScope。即全局协程作用域,在这个范围内启动的协程可以一直运行直到应用停止运行。GlobalScope 本身不会阻塞当前线程,且启动的协程相当于守护线程,不会阻止 JVM 结束运行
  • runBlocking。一个顶层函数,和 GlobalScope 不一样,它会阻塞当前线程直到其内部所有相同作用域的协程执行结束
  • 自定义 CoroutineScope。可用于实现主动控制协程的生命周期范围,对于 Android 开发来说最大意义之一就是可以在 Activity、Fragment、ViewModel 等具有生命周期的对象中按需取消所有协程任务,从而确保生命周期安全,避免内存泄露

1、GlobalScope

GlobalScope 属于 全局作用域,这意味着通过 GlobalScope 启动的协程的生命周期只受整个应用程序的生命周期的限制,只要整个应用程序还在运行且协程的任务还未结束,协程就可以一直运行

GlobalScope 不会阻塞其所在线程,所以以下代码中主线程的日志会早于 GlobalScope 内部输出日志。此外,GlobalScope 启动的协程相当于守护线程,不会阻止 JVM 结束运行,所以如果将主线程的休眠时间改为三百毫秒的话,就不会看到 launch A 输出日志

 
fun main() {
    log("start")
    GlobalScope.launch {
        launch {
            delay(400)
            log("launch A")
        }
        launch {
            delay(300)
            log("launch B")
        }
        log("GlobalScope")
    }
    log("end")
    Thread.sleep(500)
}

[main] start
[main] end
[DefaultDispatcher-worker-1] GlobalScope
[DefaultDispatcher-worker-3] launch B
[DefaultDispatcher-worker-3] launch A

GlobalScope.launch 会创建一个顶级协程,尽管它很轻量级,但在运行时还是会消耗一些内存资源,且可以一直运行直到整个应用程序停止(只要任务还未结束),这可能会导致内存泄露,所以在日常开发中应该谨慎使用 GlobalScope

2、runBlocking

也可以使用 runBlocking 这个顶层函数来启动协程,runBlocking 函数的第二个参数即协程的执行体,该参数被声明为 CoroutineScope 的扩展函数,因此执行体就包含了一个隐式的 CoroutineScope,所以在 runBlocking 内部可以来直接启动协程


public fun <T> runBlocking(context: CoroutineContext = 
	EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T

runBlocking 的一个方便之处就是:只有当内部相同作用域的所有协程都运行结束后,声明在 runBlocking 之后的代码才能执行,即 runBlocking 会阻塞其所在线程

看以下代码。runBlocking 内部启动的两个协程会各自做耗时操作,从输出结果可以看出来两个协程还是在交叉并发执行,且 runBlocking 会等到两个协程都执行结束后才会退出,外部的日志输出结果有明确的先后顺序。即 runBlocking 内部启动的协程是非阻塞式的,但 runBlocking 阻塞了其所在线程。此外,runBlocking 只会等待相同作用域的协程完成才会退出,而不会等待 GlobalScope 等其它作用域启动的协程


fun main() {
    log("start")
    runBlocking {
        launch {
            repeat(3) {
                delay(100)
                log("launchA - $it")
            }
        }
        launch {
            repeat(3) {
                delay(100)
                log("launchB - $it")
            }
        }
        GlobalScope.launch {
            repeat(3) {
                delay(120)
                log("GlobalScope - $it")
            }
        }
    }
    log("end")
}

[main] start
[main] launchA - 0
[main] launchB - 0
[DefaultDispatcher-worker-1] GlobalScope - 0
[main] launchA - 1
[main] launchB - 1
[DefaultDispatcher-worker-1] GlobalScope - 1
[main] launchA - 2
[main] launchB - 2
[main] end

所以说,runBlocking 本身带有阻塞线程的意味,但其内部运行的协程又是非阻塞的,读者需要明白这两者的区别

基于是否会阻塞线程的区别,以下代码中 runBlocking 会早于 GlobalScope 输出日志


fun main() {
    GlobalScope.launch(Dispatchers.IO) {
        delay(600)
        log("GlobalScope")
    }
    runBlocking {
        delay(500)
        log("runBlocking")
    }
    //主动休眠两百毫秒,使得和 runBlocking 加起来的延迟时间多于六百毫秒
    Thread.sleep(200)
    log("after sleep")
}

[main] runBlocking
[DefaultDispatcher-worker-1] GlobalScope
[main] after sleep

3、coroutineScope

coroutineScope 函数用于创建一个独立的协程作用域,直到所有启动的协程都完成后才结束自身。runBlockingcoroutineScope 看起来很像,因为它们都需要等待其内部所有相同作用域的协程结束后才会结束自己。两者的主要区别在于 runBlocking 方法会阻塞当前线程,而 coroutineScope不会,而是会挂起并释放底层线程以供其它协程使用。基于这个差别,runBlocking 是一个普通函数,而 coroutineScope 是一个挂起函数


fun main() = runBlocking {
    launch {
        delay(100)
        log("Task from runBlocking")
    }
    coroutineScope {
        launch {
            delay(500)
            log("Task from nested launch")
        }
        delay(50)
        log("Task from coroutine scope")
    }
    log("Coroutine scope is over")
}

[main] Task from coroutine scope
[main] Task from runBlocking
[main] Task from nested launch
[main] Coroutine scope is over

4、supervisorScope

supervisorScope 函数用于创建一个使用了 SupervisorJob 的 coroutineScope,该作用域的特点就是抛出的异常不会连锁取消同级协程和父协程


fun main() = runBlocking {
    launch {
        delay(100)
        log("Task from runBlocking")
    }
    supervisorScope {
        launch {
            delay(500)
            log("Task throw Exception")
            throw Exception("failed")
        }
        launch {
            delay(600)
            log("Task from nested launch")
        }
    }
    log("Coroutine scope is over")
}

[main] Task from runBlocking
[main] Task throw Exception
[main] Task from nested launch
[main] Coroutine scope is over

5、自定义 CoroutineScope

假设我们在 Activity 中先后启动了多个协程用于执行异步耗时操作,那么当 Activity 退出时,必须取消所有协程以避免内存泄漏。我们可以通过保留每一个 Job 引用然后在 onDestroy方法里来手动取消,但这种方式相当来说会比较繁琐和低效。kotlinx.coroutines 提供了 CoroutineScope 来管理多个协程的生命周期

我们可以通过创建与 Activity 生命周期相关联的协程作用域来管理协程的生命周期。CoroutineScope 的实例可以通过 CoroutineScope()MainScope() 的工厂函数来构建。前者创建通用作用域,后者创建 UI 应用程序的作用域并使用 Dispatchers.Main 作为默认的调度器


class Activity {

    private val mainScope = MainScope()

    fun onCreate() {
        mainScope.launch {
            repeat(5) {
                delay(1000L * it)
            }
        }
    }

    fun onDestroy() {
        mainScope.cancel()
    }

}

或者,我们可以通过委托模式来让 Activity 实现 CoroutineScope 接口,从而可以在 Activity 内直接启动协程而不必显示地指定它们的上下文,并且在 onDestroy()中自动取消所有协程


class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {

    fun onCreate() {
        launch {
            repeat(5) {
                delay(200L * it)
                log(it)
            }
        }
        log("Activity Created")
    }

    fun onDestroy() {
        cancel()
        log("Activity Destroyed")
    }

}

fun main() = runBlocking {
    val activity = Activity()
    activity.onCreate()
    delay(1000)
    activity.onDestroy()
    delay(1000)
}

从输出结果可以看出,当回调了onDestroy()方法后协程就不会再输出日志了


[main] Activity Created
[DefaultDispatcher-worker-1] 0
[DefaultDispatcher-worker-1] 1
[DefaultDispatcher-worker-1] 2
[main] Activity Destroyed

已取消的作用域无法再创建协程。因此,仅当控制其生命周期的类被销毁时,才应调用 scope.cancel()。例如,使用 viewModelScope 时, ViewModel 会在自身的 onCleared() 方法中自动取消作用域

挂起函数 suspend

挂起函数表示在不占用当前线程资源, 挂起函数会自动判断是否在主线程,如果在主线程,则切换线程到子线程,不占用主线程的资源。

CoroutineScope(Dispatchers.Main).launch {
    val result = network()//挂起函数
    visibleState(result)
}

suspend fun network():String {
    delay(2000)
    return "json"
}

CoroutineContext

CoroutineContext 使用以下元素集定义协程的行为:

  • Job:控制协程的生命周期

  • CoroutineDispatcher:将任务指派给适当的线程

  • CoroutineName:协程的名称,可用于调试

  • CoroutineExceptionHandler:处理未捕获的异常

Dispatcher

Main

  • 主线程

Dispatcher.Main :主线程

IO、Default

  • 后台线程

Dispatcher.IO : IO密集型 = IO操作或者http操作,磁盘或网络操作,cpu 闲置,64线程

Dispatcher.Default : 计算密集型操作 线程数 = cpu 的线程数

串行切协程 withContext

val scope = CoroutineScope(Dispatchers.Default)
scope.launch(Dispatchers.IO) {
    //串行切线程
    withContext(Dispatchers.MAIN){//CoroutineContext

    }
    val result = network()//挂起函数
    visibleState(result)//主线程
}

以上代码,先定义默认线程,之后执行在IO 线程切换到主线程

val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
    //串行切线程
    val result = withContext(Dispatchers.IO){//CoroutineContext
        "io result"
    }//返回串行程序的结果
    val result = network()//挂起函数
    visibleState(result)
}

结构化并发

coroutineScope 结构化并发

async 先并行再串行切线程

lifecycleScope.launch {
    val deferred1 = async {
        delay(1000)
    }
    val deferred2 = async {
        delay(2000)
    }
    //并行切到串行主线程
    deferred1.await()
    deferred2.await()

}

例子2:

 
suspend fun business1(result: String): String {
    delay(2000)
    return result
}

suspend fun business2(result: String): String {
    delay(2000)
    return result
}
 

lifecycleScope.launch {
    coroutineScope {
        val result = network()
        val deferred1 = async { business1(result) }
        val deferred2 = async { business2(result) }
        val  b1Result = deferred1.await()// business1 的返回值 T
        val  b2Result = deferred2.await()// business2 的返回值 T
    }
}

lifecycleScrope.cancel 结构化协程取消和其他取消

 
private var job: Job? = null
// deferred 是 Job 的子接口。
private var deferred: Deferred<Unit>? = null
 
deferred = lifecycleScope.async {
    delay(1000)//请求网络
}
job = lifecycleScope.launch {
    delay(1000)//请求网络
    deferred?.await()
}
 
override fun onDestroy() {
    super.onDestroy()
    disposable?.dispose()// rxjava 的关闭
    lifecycleScope.cancel()// 这一步 lifecycle 结构化并发做了,不需要自己处理
    job?.cancel() // launch 关闭
    deferred.cancel() // async 关闭
}

launch + join 实现串行

 
lifecycleScope.launch {
    coroutineScope {
        val result = network()
        val  job = launch { delay(1000) }
        val deferred1 = async { business1(result) }
        val deferred2 = async { business2(result) }
        val  b1Result = deferred1.await()// business1 的返回值 T
        val  b2Result = deferred2.await()// business2 的返回值 T
        // 第二种写法
        job.join()// 类似 await 但是没有返回值, 如果只是流程上依赖一个而不需要结果 则可以使用 launch + join 来实现串行
    }
}
 

suspendCoroutine 和回调函数混合使用

和传统回调函数转成挂起函数

suspendCancellableCoroutine 支持取消

it.resume(T)

it.resumeWithException(throwable)

it.invokeOnCancellation{ //协程取消的时候调用 }

和传统回调函数

val job1= lifecycleScope.launch {
    println("job1 start")
    Thread.sleep(500)
    println("job2 end")
    it.resumeWithException()
}
lifecycleScope.launch {
    delay(100)
    job1.cancel()
}

以上做法是取消不掉 job1 的协程的;

Android 的特定使用

lifecycleScope viewModelScope

  • 这个两个都是主线程下发起协程
  • 并且绑定了生命周期
  • 可以在Activity 、Fragment 和 ViewModel 中直接使用