首页 > 技术文章 > Kotlin Coroutine(协程): 一、样例

mymy-android 2021-07-13 18:17 原文

@

前言

你还在用 Hanlder + Message? 或者 AsyncTask? 你还在用 Rxjava?
有人说RxjavaCoroutine是从不同维度解决异步, 并且Rxjava的强大不止于异步问题.
好吧, 管它呢. 让我们拥抱 Coroutine(协程) 吧.

协程的篇幅有点长, 需要自己编写大量的测试例子. 从入门到都学完, 难度还是挺大的. 所以:我们直接把例子摆出来. 至于其他的, 我们从浅到深,日后..日后..再说 [奸笑]

没有描述


Kotlin中文站: 请看官网


准备:
lifecycle 都有吧? 例子中需要用到 lifecycleScope;

//coroutines: 协程
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
//coroutines for Android
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'

//lifecycle
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'

没有lifecycle 怎么办? 当页面销毁时,那些延时任务, 例如Handler, 网络请求 啥的, 需要及时关闭, 它们会影响 Activity 的及时销毁, 带来内存泄漏的风险. 协程也是如此.

此时我们必须自己管理协程的作用域; 所以最好还是用 lifecycleScope.

//自己定义作用域, 页面销毁时, 取消协程.  替换lifecycleScope即可;
val mainScope = MainScope()
override fun onDestroy(){
    super.onDestroy()
    mainScope.cancel()
}

什么是协程作用域呢? 类似于生命周期, 当页面销毁时, 协程应当随之停止.

提示:以下是本篇文章正文内容,下面案例可供参考


一、直接上例子

协程能干嘛? 延时任务, 异步任务, 定时任务, 并行任务, 协程+Retrofit. 异步流? 通道?

1.延时任务.

比较常见的业务场景, 几秒钟后执行...

还记得handler怎么写吗? 定义 Handler 实现类, 然后

mHandler.sendMessageDelayed()
//或者
mHandler.postDelayed(Runnable{...})

使用协程:

doDelayed()

fun doDelayed() = lifecycleScope.launch {
    delay(2000L)	//协程挂起2秒
    binding.tvTitle.text = "变变变, 我是百变小魔女"
}

  • lifecycleScope: 默认主线程. 它会监听Activity生命周期, 不需要手动 cancel()
  • launch: 非阻塞形式启动协程.
  • delay(2000): 挂起协程, 2秒后恢复.

然后封装一下:

//执行延时任务
fun CoroutineScope.doDelayed(timeMillis: Long, block: suspend () -> Unit) = this.launch {
    delay(timeMillis)
    block.invoke()
}

//使用
lifecycleScope.doDelayed(2000L){
    binding.tvTitle.text = "变变变, 我是百变小魔女"
}

2.异步任务

在子线程执行任务, 完事回调主线程
代码如下:

doOnBack()

// 模拟子线程中执行了2秒钟的任务,
private fun doOnBack() = lifecycleScope.launch {
    val result = withContext(Dispatchers.IO){
        delay(2000) // 假装努力干活中..
        "变变变, 我是百变小魔女"
    }
    binding.tvTitle.text = result
}

//或者. async .
private fun doOnBack() = lifecycleScope.launch {
   val deferred = async (Dispatchers.IO){
        delay(2000) // 假装努力干活中..
        "变变变, 我是百变小魔女"
    }
    binding.tvTitle.text = deferred.await()
}

  • withContext(): 不新建协程, 它只指定 执行当前代码块 所需的线程;
  • async: 创建一个协程; 它返回一个Deferred, 而lanuch返回Job对象. 多个async可以支持并发, await()是等待任务执行完毕,并返回结果.
  • Dispatchers.IO: 协程调度器. 几种调度器如下所示

参数 意义
不指定 它从启动了它的 CoroutineScope 中承袭了上下文
Dispatchers.Main 用于Android. 在UI线程中执行
Dispatchers.IO 子线程, 适合执行磁盘或网络 I/O操作
Dispatchers.Default 子线程,适合 执行 cpu 密集型的工作
Dispatchers.Unconfined 管它呢, 不常用, 先不管

由于异步任务需要两段代码块. 子线程代码,及主线程响应; 所以就不封装了; 并且里面其实就一句 withContext(Dispatchers.IO) 或者 val deferred = async (Dispatchers.IO)

3.并行任务:

有时多个任务异步进行, 而我们不清楚哪一个会先完成, 我们需要等待它们的最终结果时.

private suspend fun taskOne(): Int {
    delay(2000)	//模拟任务执行 2秒
    return 1
}

private suspend fun taskTwo(): Int {
    delay(1500) 	//模拟任务执行 1.5秒
    return 2
}

private suspend fun taskExecute(): Int = coroutineScope {
    val result1 = async(Dispatchers.IO) { taskOne() }
    val result2 = async(Dispatchers.IO) { taskTwo() }
    result1.await() + result2.await()
}

//使用
lifecycleScope.launch {
    val sum = taskExecute()
    binding.tvTitle.text = "变变变, 我是百变小魔女$sum"
}

taskOne 执行需要2秒; taskTwo 执行需要1.5秒; 并行, 任务总共耗费约 2秒时间.
suspend: 标志挂起函数, 挂起函数只能在协程中运行.

4.定时任务:

定时任务简单啊, delay()不阻塞线程啊.

lifecycleScope.launch {
    repeat(Int.MAX_VALUE){
        delay(1000L)
        binding.tvTitle.text = "变变变, 我是百变小魔女$it"
    }
}

这个定时任务, 每次执行, 只设置TextView的text.
我们假设我们的业务比较复杂, 每次需要耗费300ms; 如下所示:

lifecycleScope.launch {
    repeat(Int.MAX_VALUE){
        delay(1000L)

        //假设我们的任务比较繁重, 每次需要消耗300ms
        delay(300L)
        binding.tvTitle.text = "变变变, 我是百变小魔女$it"
    }
}

显而易见, 此时至少要 1300ms 才能循环一次. 所以上面写的1000ms只能算是间隔时间. 而任务执行需要时间, 协程挂起,恢复需要时间, 任务进入队列到线程执行也需要时间.

因此:
当定时任务的精确度要求不高, 每次执行的代码任务比较轻便. 耗时较少时, 可以用这种方式.

有的时候, 我们锁屏了, 或者跳到别的页面了, 我们不需要定时一直执行, 即便更新了UI, 我们也看不到啊! 所以,我们希望页面离开时, 定时取消. 页面显示时,重新启动定时即可:
我们就以秒杀倒计时为例;

private var endTime: Long = 0	//秒杀截止时间

//秒杀剩余 5分钟
endTime = System.currentTimeMillis() + 300 * 1000

lifecycleScope.launchWhenResumed {  //onPause 的时候会暂停.
    repeat(Int.MAX_VALUE){
        val now = System.currentTimeMillis()
        binding.tvTitle.text = if(now >= endTime){
            "秒杀结束"
        }else{
            val second = (endTime - now) / 1000 + 1
            "秒杀倒计时($second); it=$it"
        }
        delay(1000L)
    }
}

launchWhenResumed: 当页面 Resumed 时, 启动协程. onPause()时自动停止. 页面重新显示时恢复. 通过观察这里的 it, 可以断定 协程只是被挂起, 而不是销毁重建.
同样的还有: launchWhenCreated; launchWhenStarted 当然这是 lifecycle 的知识了.

有时, 我们需要精确的定时器.

可以用, java.util 包下的 Timer, 如下所示; 也可以用 CountDownTimer

val timer = timer("定时Thread_name", false, 2000, 1000){
    letUsPrintln("定时器, 这是个子线程")
}

当然, 它没有自动暂停恢复的功能; 取消执行时, 别忘了 cancel();

协程+Retrofit 的封装还是放到后面吧..


总结

没啥好总结的, 复制粘贴就完事了. 下一篇我们再深入浅出的了解协程 [奸笑]

推荐阅读