协程
使用协程的优势:让代码更简洁地处理异步操作,可以用写同步代码的方式执行异步代码,避免嵌套回调地狱,提高代码可读性和复用性。
下面是一些使用协程和不使用协程的例子:
倒计时:
// 不使用协程:每隔1秒输出 4 3 2 1 0
private val MSG_COUNT_DOWN = 1
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
if (msg.what == MSG_COUNT_DOWN) {
val second = msg.arg1 //倒计时时间
//使用这个倒计时时间,比如将其展示到UI上,此处为主线程。
Log.i("TEST", "second: $second")
if (second > 0) sendMessageDelayed(Message.obtain().apply {
this.what = MSG_COUNT_DOWN
this.arg1 = second - 1
}, 1000)
}
super.handleMessage(msg)
}
}
private fun startCountDown(second : Int) {
handler.sendMessage(Message.obtain().apply {
this.what = MSG_COUNT_DOWN
this.arg1 = second - 1
})
}
// 使用协程:每隔1秒输出 4 3 2 1 0
private fun startCountDown(second : Int) {
GlobalScope.launch { //启动一个协程,此时运行在子线程
repeat(second) { //重复 second 次
delay(1000) // 挂起 1 秒,此处挂起不会堵塞线程
withContext(Dispatchers.Main) { // 切换到主线程
//使用这个倒计时时间,比如将其展示到UI上,此处为主线程。
Log.i("TEST", "second: ${second - it - 1}")
}
}
}
}
请求网络:
// 不使用协程:
okhttp.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
// 获取结果
}
})
// 使用协程:
GlobalScope.launch { //启动一个协程,此时运行在子线程
val result = okhttp.newCall(request).execute() // 在子线程堵塞耗时
withContext(Dispatchers.Main) { // 切换到主线程
//使用这个result,比如将其展示到UI上,此处为主线程。
Log.i("TEST", "result: $result")
}
}
挂起与堵塞的区别
挂起与线程堵塞不同,挂起是基于协程的运行逻辑的,挂起仅仅是暂停协程任务。
只有挂起函数进行挂起,挂起函数会带有 suspend 标志,比如 delay(),除此之外,只有挂起函数才能调用挂起函数。
// 需要有 suspend 标签才能调用 delay
public suspend fun invodeDelay() {
delay(10000)
}
协程使用
我们可以从上面的例子中拆分出几个使用协程的步骤:
定义上下文
上下文的指的是 CoroutineContext 类,它包含了运行线程、工作状态等信息,我们通常会通过 CoroutineScope 类使用它。
public interface CoroutineScope {
/**
* The context of this scope.
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
* Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
*
* By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
*/
public val coroutineContext: CoroutineContext
}
在上面的例子中,我们使用了 GlobalScope,这个 scope 是进程默认创建的全局 scope,默认使用 Dispatchers.Default 这个线程池,这个线程池里的线程都是子线程,用于处理 cpu 密集型工作。
使用这个 scope 的时候我们需要时刻注意在不需要的时候取消启动的任务,否认容易引起内存泄漏。因为这个 scope 的生命周期是跟随整个应用进程的生命周期的。
除此之外,我们也可以 new 一个 scope,如下:
// 创建了一个默认运行在 Dispatchers.Default 下的 scope,可以通过 scope.cancel() 方法取消基于这个 scope 启动的任务
private val scope = CoroutineScope(Dispatchers.Default)
或者使用协程或者 Android 提供的 scope:
// 默认运行在主线程,且启动的任务的失败不会影响其他任务。
private val scope = MainScope()
// 跟随 Fragment 生命周期的 scope,会在 activity destroy 时取消所有任务。
private val scope = lifecycleScope
// 跟随 Fragment 生命周期的 scope,会在 Fragment destroy 时取消所有任务。
private val scope = lifecycleScope
// 跟随 Fragment view 生命周期的 scope,会在 Fragment view destroy 时取消所有任务。
private val scope = viewLifecycle.coroutineScope
定义运行线程:
运行线程主要有5类:可以在 scope 创建时或者任务启动时传入。
Dispatchers.Default // 子线程池,主要用于处理 cpu 密集任务
Dispatchers.IO // 子线程池,主要用于处理 io 密集任务
Dispatchers.Main // 主线程
Dispatchers.Unconfined // 不指定运行线程,这个使用比较少,了解即可
newSingleThreadContext("TAG") // 创建一个子线程作为任务的运行线程
启动任务:
启动任务的方式主要是两种:launch 和 async
launch:
在调用 launch 时,我们可以传入指定的 CoroutineContext,它会将这个 context 和调用 launch 的 scope 的 context 进行组合。另外,launch 的 block 是运行在挂起函数里的,因此在里面可以随意的调用挂起函数。
调用 launch 后,我们可以获得一个 Job 类的实例,可以根据这个实例与 launch 启动的任务进行协作。
private val scope = CoroutineScope(Dispatchers.Default)
private fun testLaunch() {
val job = scope.launch {
delay(10000)
// 运行在 Dispatchers.Default 线程池中
}
scope.launch(Dispatchers.Main) {
// 运行在主线程中
job.join() //会挂起等待 job 执行完成
// 10 秒后执行到这
}
// job.cancel() // 取消协程任务
// job.isActive // 查询任务是否已完成
// job.invokeOnCompletion { } // 当任务结束时回调
}
async
async 大体与 launch 类似,不同点在于它启动的任务是可以获取结果的,类似于 Java 的 Fuature。它的返回值是一个 Deferred 类实例,它继承自 Job,在 Job 的基础上添加了获取 async 结果的函数。
private val scope = CoroutineScope(Dispatchers.Default)
private fun testAsync() {
val deferred = scope.async {
delay(10000)
return@async System.currentTimeMillis()
}
scope.launch {
val timeMillis = deferred.await() //挂起等待 deferred 完成后获取返回值
}
}
线程切换
协程中线程切换主要是通过 withContext() 函数操作的,它可以传入一个 CoroutineContext 并使 block 中的代码运行在这个 context 环境中。
private val scope = CoroutineScope(Dispatchers.Default)
private fun testWithContext() {
scope.launch {
delay(10000)//模仿耗时操作,比如下载,此时在子线程
withContext(Dispatchers.Main) {
// 根据下载内容显示UI,此时在主线程。
}
//运行在子线程,且会等待 withContext 执行完毕。
}
}