二十六、Android-kotlin协程

240 阅读6分钟

26. kotlin-协程

协程是一种轻量级并发编程框架,用于处理异步操作和多线程任务。它具有许多特点,使得在编写异步和并发代码时更加方便和可读。

前提知识:

  • 并行:并行是指同时执行多个任务,每个任务都在不同的处理器核心上独立运行。可通过线程、进程来达。
  • 并发:并发是指多个任务在同一时间段内交替执行,它们通过时间片轮转或事件驱动的方式共享处理器时间。并发的目标是提高系统的响应能力、资源利用率和程序结构的可维护性,而不一定追求同时执行多个任务。

协程允许在单线程模式下模拟多线程的效果,代码执行时的挂起与恢复完全是由编程语言来控制的,和操作系统无关。这种特性使得高并发程序的运行效率得到极大的提升。

26.1 协程的基本用法

添加依赖

 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
 // android项目需要引用
 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"

顶级协程

GlobalScope.launch可以创建一个协程的作用域

    GlobalScope.launch {
        println("codes run in coroutine scope")
    }

延迟协程

delay()函数:可以让当前协程延迟指定时间后再运行,非阻塞式挂起函数,只会挂起当前协程,并不会影响其他协程的运行。只能在协程的作用于或其他挂起函数中调用。

    GlobalScope.launch {
        println("codes run in coroutine scope")
        delay(1500)
        println("codes run in coroutine scope finished")
    }

runBlocking

runBlocking函数同样会创建一个协程的作用域,但是它可以保证在协程作用域的所有代码和子协程没有全部执行完之前一直阻塞当前线程。

注意:runBlocking函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题

    runBlocking {
        println("codes run in coroutine scope")
        delay(1500)
        println("codes run in coroutine scope finished")
    }

子协程

launch函数必须在协程的作用域中才能调用,它会在当前协程的作用域下创建子协程,子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程也会一同结束。

    runBlocking {
        launch {
            println("launch1")
            delay(1000)
            println("launch1 finished")
        }
        launch {
            println("launch2")
            delay(1000)
            println("launch2 finished")
        }
    }

挂起函数

suspend关键字

suspend fun printDot() {
    println(".")
    delay(1000)
}

coroutineScope

coroutineScope函数也是一个挂起函数,因此可以在任何其他挂起函数中调用,它的特点是会继承外部协程的作用域,并创建一个子协程。

coroutineScope函数和runBlocking函数有点类似,可以保证其作用域内的所有代码和子协程在全部执行完之前,外部的协程会一直被挂起

suspend fun printDot2() = coroutineScope { 
    launch { 
        println(".")
        delay(1000)
    }
}

总结: GlobalScope.launchrunBlocking函数是可以在任意地方调用的,coroutineScope函数可以在协程作用域或挂起函数中调用,而launch函数只能在协程作用域中调用。

26.2 更多的作用域构建起

取消协程

    val job = GlobalScope.launch { 
        // 业务逻辑
        println("...")
    }
    job.cancel()

取消协程-常用方式

    val job = Job()
    val scope = CoroutineScope(job)
    scope.launch { 
        println("business handle")
    }
    job.cancel()

async函数-普通使用

async函数必须在协程作用域中才能调用,它会创建一个新的子协程,并返回一个Deferred对象,如果想要获取async函数代码块的执行结果,只需要调用Deferred对象的await()方法即可。

类似IOS的栅栏函数,在并发中等待之前的任务先完成

    runBlocking {
        val result1 = async {
            println("async1 start")
            var re = 5 + 5
            delay(1000)
            re += 10
            println("async1 finished")
            return@async re
        }.await()
        val result2 = async {
            println("async2 start")
            var re = 1 + 1
            delay(1000)
            re += 1
            println("async2 finished")
            return@async re
        }.await()
        println("result: $result1 + $result2")
    }

async函数-进阶使用

事实上,在调用async函数后,代码块中的代码就会立刻开始执行,当调用await()方法时,如果代码块中代码还没执行完,那么await()方法会将当前协程阻塞住,直到可以获取async函数的执行结果。

现在修改下:不在async函数之后立刻调用await()方法获取结果了,改成在用到async()结果时才调用await()方法进行获取。这样两个async函数就变成一种并行关系了

类似IOS的asyncNotify

    runBlocking {
        val result1 = async {
            println("async1 start")
            var re = 5 + 5
            delay(1000)
            re += 10
            println("async1 finished")
            return@async re
        }
        val result2 = async {
            println("async2 start")
            var re = 1 + 1
            delay(1000)
            re += 1
            println("async2 finished")
            return@async re
        }
        println("result: ${result1.await()} + ${result2.await()}")
    }

withContext函数

withContext函数是一个挂起函数,可以理解成async函数的一种简化版写法。

    runBlocking {
        val result = withContext(Dispatchers.Default) {
            var r = 5
            r += 5
            return@withContext r
        }
        println("result $result")
    }

调用withContext()函数之后,会立即执行代码块中的代码,同时将外部协程挂起。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为withContext()函数的返回值返回,因此基本上相当于val result = async{ 5 + 5·}.await()的写法。唯一不同的是,withContext()函数强制要求我们指定一个线程参数。

线程参数主要有以下3种值可选:

Dispatchers.Default:表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,此时就可以使用Dispatchers.Default。

Dispatchers.IO:表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中,比如说执行网络请求时,为了能够支持更高的并发数量。

Dispatchers.Main:则表示不会开启子线程,而是在Android主线程中执行代码,但是这个值只能在Android项目中使用,纯Kotlin程序使用这种类型的线程参数会出现错误。

上面的协程,除了coroutineScope函数之外,其他所有的函数都是可以指定这样一个线程参数的,只不过withContext()函数是强制要求指定的,而其他函数则是可选的。

26.3 使用协程简化回调的写法

以前封装的网络请求使用时

HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
    override fun onFinish(response: String) {
     // 得到服务器返回的具体内容
    }
    override fun onError(e: Exception) {
     // 在这里对异常情况进行处理
    }
})

使用suspendCoroutine函数简化

suspendCoroutine函数必须在协程作用域或挂起函数中才能调用,接收一个Lambda表达式参数,主要作用是将当前协程立即挂起,然后在一个普通的线程中执行Lambda表达式中的代码。

Lambda表达式的参数列表上会传入一个Continuation参数,调用它的resume()方法或resumeWithException()可以让协程恢复执行。

suspend fun request(address: String): String {
    return suspendCoroutine { continuation ->
        HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
        override fun onFinish(response: String) {
            continuation.resume(response)
        }
        override fun onError(e: Exception) {
            continuation.resumeWithException(e)
        }
        })
    }
}

然后使用时

suspend fun getBaiduResponse() {  
    try {
        val response = request("https://www.baidu.com/")
        // 对服务器响应的数据进行处理
    } catch (e: Exception) {
    // 对异常情况进行处理
    }
}