Android 的Kotlin语言的协程
一:协程的依赖
Kotlin 协程提供了一种全新处理并发的方式,你可以在 Android 平台上使用它来简化异步执行的代码。
如果是用于 Android 平台的话,可以只引用以下的 coroutines-android,当中已经包含了 coroutines-core
//协程依赖
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
协程优势:
1.轻量:单个线程上可以运行多个协程,协程支持挂起,不会使正在运行的线程阻塞
2.内存泄漏更少:协程支持结构化并发,从而避免了内存泄漏
3.Jetpact集成:Jetpack库都包含提供全面协程的支持的扩展。如:ViewModelSocpe,LifecycleScope,LiveData
二:协程使用
1.简单使用
//开启协程
fun runCoroutine() {
Log.i("SecondActivity", "协程开始执行")
Log.i("SecondActivity", "thread=${Thread.currentThread().name}")
CoroutineScope(Dispatchers.IO).launch {
delay(2000)
Log.i("SecondActivity", "协程内部")
Log.i("SecondActivity", "thread11=${Thread.currentThread().name}")
}
Log.i("SecondActivity", "协程下面")
}
结果:
协程开始执行
thread=main
协程下面
协程内部
thread11=DefaultDispatcher-worker-1
CoroutineContext
协程中使用 CoroutineScope(Dispatchers.IO)的Dispatchers.IO 是CoroutineContext的子类实现
CoroutineContext。即协程上下文,包含多种类型的配置参数。Dispatchers.IO 就是 CoroutineContext 这个抽象概念的一种实现,用于指定协程的运行载体,即用于指定协程要运行在哪类线程上
@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
Kotlin 协程库提供了四个 Dispatcher 用于指定在哪一类线程中执行协程:
Dispatchers.Default。默认调度器,适合用于执行占用大量 CPU 资源的任务。例如:对列表排序和解析 JSON
Dispatchers.IO。适合用于执行磁盘或网络 I/O 的任务。例如:使用 Room 组件、读写磁盘文件,执行网络请求
Dispatchers.Unconfined。对执行协程的线程不做限制,可以直接在当前调度器所在线程上执行
Dispatchers.Main。使用此调度程序可用于在 Android 主线程上运行协程,只能用于与界面交互和执行快速工作,例如:更新 UI、调用 LiveData.setValue
CoroutineScope
CoroutineScope 即 协程作用域,用于对协程进行追踪。如果我们启动了多个协程但是没有一个可以对其进行统一管理的途径的话,就会导致我们的代码臃肿杂乱,甚至发生内存泄露或者任务泄露。为了确保所有的协程都会被追踪,Kotlin 不允许在没有 CoroutineScope 的情况下启动协程。CoroutineScope 可被看作是一个具有超能力的 ExecutorService 的轻量级版本。它能启动协程,同时这个协程还具备上文所说的 suspend 和 resume 的优势
suspend
suspend 是协程中很重的关键字,它用来修饰函数,表示此函数是一个会挂起的函数,并且 挂起函数只有在协程中使用或者被另一个挂起函数调用,可以暂停和进行恢复,什么情况下需要用到挂起函数
- 线程切换,挂起本身是线程切换不同的协程去工作,所以当需要进行线程切换时可以使用挂起函数
- 延迟,暂停往往代表在等待一些结果,当我们在等待一些返回结果时,协程可以通过挂起的方式等待,而不是阻塞线程
suspend只是对函数的一个标识别,它不像inline,refied等关键字一样会对代码造成影响,而是提醒使用者这是一个挂起函数,具体的挂起业务还是需要函数内部自己实现
withContext
withContext是一个挂起函数,表明它只能在协程或者其他suspend函数调用
public suspend fun withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
}
launch
lauch是最常见的启动一个协程的方法,可以通过GlobalScope.launch开启一个全局生命周期的协程,也可以通过CoroutineScope(CoroutineContext).launch 来开启一个在指定的 CoroutneContext 范围的协程。也可以记录这个Job并通过Job.cancel()随时取消
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
val apiService by lazy { RetrofitClient.instance.create() }
//开启协程
fun runCoroutine(name: String, password: String, resultListener: (String, String) -> Unit) {
Log.i("SecondActivity", "协程开始执行")
Log.i("SecondActivity", "thread=${Thread.currentThread().name}")
CoroutineScope(Dispatchers.IO).launch {
Log.i("SecondActivity", "协程内部")
Log.i("SecondActivity", "thread11=${Thread.currentThread().name}")
val request = HttpAccountLoginRequest(name, password, null)
val block :suspend CoroutineScope.()->BaseResult ={
apiService.requestAccountLogin(request,"android","3.5.4")
}
var result:BaseResult =block()
if (result.code=="200"&&result.datas!=null){
withContext(Dispatchers.Main){
}
}
}
Log.i("SecondActivity", "协程下面")
}
launch
- 不会阻塞直到结果返回
- 不会阻塞线程
- 并行执行
withContext:
- 会阻塞当前协程直到函数返回
- 从指定的Dispatcher执行函数
- 当执行函数的时候不会阻塞线程
- 串行执行
async
- 当使用awiat函数时,会阻塞直到结果返回
- 如果不使用await,其效果与launch一样
- 适用于多个并行任务但需要等待结果返回情形
- 并行执行
二:什么是 Job ?
Job 翻译作任务,Job 赋予协程可取消,赋予协程以生命周期,赋予协程以结构化并发的能力。其中平常使用中最为重要的是可取消、结构化并发的特点。尤其 在日常 Android 开发过程中,协程配合 Lifecycle 可以做到自动取消。
Job 的生命周期
Job 的生命周期分为 6 种状态,分为 New、Active、Completing、Cancelling、Cancelled、Completed,通常外界会持有 Job 接口会作为引用被协程调用者所持有,
Job 接口提供 isActive、isCompleted、isCancelled 3 个变量使外界可以感知 Job 内部的状态,这3个变量和 Job 生命周期的6种状态的对应关系如下图所示
栗子:
CoroutineScope(Dispatchers.IO).async {
}
源码:
public fun CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
栗子:
CoroutineScope(Dispatchers.IO).launch {
}
源码:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
一种是通过 launch 启动,一种是通过 async 启动,前者会返回一个 Job 类型的对象,后者会返回一个 Deferred 类型的对象
Job的接口定义
Job 顾名思义就是“工作”的意思,每个协程可以想象成是一个工作任务,启动一个协程就是启动一个工作任务,来看看 Job 接口的主要定义:
//Job 也是继承自 Element,所以它本身也是一个协程上下文 context
public interface Job : CoroutineContext.Element {
//Key对象,如果你看到 context[Job] 的写法, 就知道其实指的是这里的这个伴生对象 Key
public companion object Key : CoroutineContext.Key {
init {
CoroutineExceptionHandler
}
}
//是否活动状态,必须满足几个条件:该协程已经启动、没有完成、没有被取消
public val isActive: Boolean
//是否完成状态
public val isCompleted: Boolean
//是否被取消状态
public val isCancelled: Boolean
//启动协程,开始调度。如果已经启动了,则返回false。与线程的Thread.start()挺类似
public fun start(): Boolean
//挂起当前正在运行的协程,等待该 Job 执行完成。与线程的Thread.join()挺类似
public suspend fun join()
//取消该 Job
public fun cancel(cause: CancellationException? = null)
//该 Job 的子 Job
public val children: Sequence
}
小知识:
Kotlin空指针检查
在Kotlin里,可以用“?”表示可以为空,也可以用“!!”表示不可以为空。
给变量加上?标识,会通告所有使用该变量的地方,必须给出为空的补救措施。
var info: String? = null
println(info?.length) //第一种补救:如果info为null,就不执行后面的.length代码
println(info!!.length) //第二种补救:这里如果为null,我自己负责info,会报出空指针,这种处理需慎用
if (info != null) { //第三种补救措施,如下这种同java写法
println(info.length)
}
println(info?.length ?: "空数据") //第四种补救措施,如果真的为null,则改为返回"空数据"