什么是协程
对于android中kotlin的协程来说,协程就是由kotlin 官方提供的一套线程切换的API,和线程池的功能是一样的,只不过他的优势在于用同步的方式写出了异步的代码。因为协程是运行在jvm上的,jvm没有协程这个概念。
kotlin的协程如果是运行在当前的线程,相当于往当前的线程的handler队列中,post了一个任务,也会造成阻塞,记住如果协程没有切换线程的话,那个有可能也会造成阻塞。这一点我待会会验证。
好,现在开始从使用
协程如果使用
在kotlin中协程并没有被纳入标准库,所以使用的时候还是得引入:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
第二个库是android环境使用的。
开启协程的两个方法:
launch { }
async { }
我们看下这两个方法
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
}
async
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
发现这两个方法都是 CoroutineScope(协程作用域)类的扩展函数,所以说这两个函数必须得在用协程作用域对象才能调用。
区别在于,lanch不会返回运行结果,而async返回的Deferred对象,通过Deferred对象的await()
方法可以获得协程的执行结果。
还有一个
withContext(Dispatchers.Main){ }
他其实是
async { }.await()
的简写,所以会阻塞协程,因为await()方法是挂起函数
解释下几个名词:
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
}
协程作用域中就一个属性,就是协程上下文,说明协程coroutineScope是对CoroutineContext的一个封装。 是指一个协程的运行范围(或者说生命周期)用来让用户继承,来实现取消协程和停止操作。自己可以控制
CoroutineContext:
协程的上下文可以理解为协程运行的配置信息,执行协程需要的那个配置,比如:协程的名字,协程运行在那个线程上。
我看看下coroutineContext类,其实就相当于一个容器,里面存放这各种属性,类似于hash那种存储。
public interface CoroutineContext {
//重写了+号运算符
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
public interface Key<E : Element>
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}
}
所以他可以这样用
我们看launch函数,可以传一个coroutineContext
我们可以这样使用
launch(Dispatchers.Main+CoroutineName("协程名称")) { }
CoroutineDispatcher 协程调度器
kotlin给我们提供了四种调度器
public actual object Dispatchers {
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
public val IO: CoroutineDispatcher = DefaultScheduler.IO
}
Default:默认调度器,CPU密集型任务调度器,通常处理一些单纯的计算任务,或者执行时间较短任务。例如数据计算
IO:IO调度器,IO密集型任务调度器,适合执行IO相关操作。比如:网络请求,数据库操作,文件操作等
Main:UI调度器,只有在UI编程平台上有意义,用于更新UI,例如Android中的主线程
Unconfined:非受限调度器,无所谓调度器,当前协程可以运行在任意线程上
CoroutineStart:协程的启动模式
public enum class CoroutineStart {
DEFAULT,
LAZY,
ATOMIC,
UNDISPATCHED;
}
可以看到CoroutineStart是一个枚举类,有四种类型。
DEFAULT默认启动模式,协程创建后立即开始调度,注意是立即调度而不是立即执行,可能在执行前被取消掉。
LAZY懒汉启动模式,创建后不会有任何调度行为,直到我们需要它执行的时候才会产生调度。需要我们手动的调用Job的start、join或者await等函数时才会开始调度。
ATOMIC 在协程创建后立即开始调度,但它和DEFAULT模式是有区别的,该模式下协程启动以后需要执行到第一个挂起点才会响应cancel操作。
UNDISPATCHED协程在这种模式下会直接开始在当前线程下执行,直到运行到第一个挂起点。和ATOMIC很像,但UNDISPATCHED很受调度器的影响
Job
launch()方法返回的是一个job对象,这个对象可以检测协程的生命周期,也可以取消协程的执行
public interface Job : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<Job>
//判断协程是否是活的
public val isActive: Boolean
//判断协程是否完成
public val isCompleted: Boolean
//判断协程是否取消
public val isCancelled: Boolean
public fun getCancellationException(): CancellationException
//开始协程
public fun start(): Boolean
//取消协程
public fun cancel(cause: CancellationException? = null)
Deferred 实际上是继承job
所以也有对协程进行操作的方法,其中还提供了 一个重要的方法,await()方法,获得协程返回的值,await是一个挂起函数,所以执行到这里的时候会阻塞当前的协程。
public interface Deferred<out T> : Job {
public suspend fun await(): T
suspend 中文 暂停的意思
suspend函数会阻塞当前的协程,但是不会阻塞开启协程的线程
在函数的前面申明一个suspend的时候,这个函数就变成了挂起函数,挂起函数只能在挂起函数中调用,但是这个只是起到一个提醒的作用,真正起到挂起作用的是函数中的代码。所以说如果你申明一个挂起函数,但是里面没有实际的挂起操作,编译器会给你置灰,意思是告诉你这个函数没必要申明。那么要这个申明有什么用呢?用于创建者给调用者一个提醒,告诉调用者说这个函数是一个挂起函数,可能是一个耗时的函数
挂起函数作用:
协程运行到挂起函数的时候,会暂停到这,切出去运行挂起的函数,运行完挂起函数后,再切回来重新运行,其实就是一个callback+状态机的机制
如果里面加上一个系统提供的真正的挂起函数就没事了
父协程和子协程的关系
1.子协程中的协程上下文的信息会copy父协程,除非自己修改
2.父协程结束后,他里面的子协程都会结束
证明第一点:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GlobalScope.launch(Dispatchers.Main+CoroutineName("父协程名字")) {
Log.e("---","协程上下文-1=${this.coroutineContext}")
launch {
Log.e("---","协程上下文-2=${this.coroutineContext}")
}
withContext(Dispatchers.IO+CoroutineName("子协程")){
Log.e("---","协程上下文-2=${this.coroutineContext}")
}
}
}
打印的结果:
E/---: 协程上下文-1=[CoroutineName(父协程名字), StandaloneCoroutine{Active}@d2a3906, Dispatchers.Main]
E/---: 协程上下文-3=[CoroutineName(子协程), DispatchedCoroutine{Active}@8fe4bc7, Dispatchers.IO]
E/---: 协程上下文-2=[CoroutineName(父协程名字), StandaloneCoroutine{Active}@24715f4, Dispatchers.Main]
我们发现协程协程2和父协程的协程上下文的配置信息是一样的,而协程3不一样,说明子协程会继承父协程的协程配置信息
CoroutineExceptionHandler 协程异常处理
我们在写代码的时候,肯定会遇到异常的情况,正常我们处理异常使用try …catch捕获,但难免会出现遗漏。CoroutineExceptionHandler 就是协程专门捕获异常的类,协程中出现的异常都会被捕获并由CoroutineExceptionHandler的handleException方法返回给我们进行处理。
public interface CoroutineExceptionHandler : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
public fun handleException(context: CoroutineContext, exception: Throwable)
}
handleException会返回两个参数,第一个参数是出现异常的协程,第二个参数是出现的异常。
示例如下:我们手动抛出一个NullPointerException异常,然后创建一个CoroutineExceptionHandler赋给协程。
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e("捕获异常", "${coroutineContext[CoroutineName]} :$throwable")
}
GlobalScope.launch(Dispatchers.Main + CoroutineName("主协程")+exceptionHandler) {
Log.e("协程的coroutineContext",this.coroutineContext.toString())
throw NullPointerException()
}
打印结果:
协程的coroutineContext: [CoroutineName(主协程), com.jf.simple.ThirdActivity$onCreate$$inlined$CoroutineExceptionHandler$1@288ff9, StandaloneCoroutine{Active}@b95b3e, Dispatchers.Main]
捕获异常: CoroutineName(主协程) :java.lang.NullPointerException
问题解析
1.如果运行的协程就是运行在当前的开启协程的线程是相当于post一个任务到线程的handler任务栈吗?
测试代码:
@SuppressLint("MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GlobalScope.launch(Dispatchers.Main+CoroutineName("父协程名字")) {
Log.e("---","111")
}
Log.e("---","222")
}
打印结果:
com.example.mytest2 E 222
com.example.mytest2 E 111
打印结果 222却在111的前面,说明确实是post了一个任务
如何创建一个协程作用域
我们知道开启协程的方法有两种,其实还有一种 runBlocking,但是这种开启的协程会阻塞当前的线程,只有当协程结束后,开启协程的线程才会结束,会造成内存泄露,一般项目中不使用。
lanch()
async()
他两属于CoroutineScope的扩展函数,所以必须的有CoroutineScope的对象才能调用
方法1:
GlobalScope 就是一个协程作用域的单例,所以可以直接使用
@DelicateCoroutinesApi
public object GlobalScope : CoroutineScope {
/**
* Returns [EmptyCoroutineContext].
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
GlobalScope.launch() { }
方法2:
class MainActivity : AppCompatActivity() {
lateinit var coroutine:CoroutineScope
@SuppressLint("MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
coroutine= CoroutineScope(Dispatchers.IO) //创建一个协程作用域
coroutine.launch {
Log.e("---","开始了协程")
}
}
override fun onDestroy() {
super.onDestroy()
coroutine.cancel() //协程取消
}
}
但是这里有一个问题,假如我需要开启很多协程作用域,我是不是得调用很多cancel
如下所示:
package com.example.mytest2
import android.annotation.SuppressLint
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.Settings.Global
import android.util.Log
import android.view.View
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.math.log
import kotlin.system.measureTimeMillis
import kotlin.time.measureTimedValue
class MainActivity : AppCompatActivity() {
lateinit var coroutine:CoroutineScope
lateinit var coroutine1:CoroutineScope
@SuppressLint("MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
coroutine= CoroutineScope(Dispatchers.IO)
coroutine.launch {
Log.e("---","开始了协程")
}
coroutine1= CoroutineScope(Dispatchers.IO)
coroutine1.launch {
Log.e("---","开始了协程")
}
}
override fun onDestroy() {
super.onDestroy()
coroutine.cancel()
coroutine1.cancel()
}
}
优化:我们知道job是管理协程的,那么CoroutineScope是如何管理协程的呢?
public fun CoroutineScope.cancel(cause: CancellationException? = null) { val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this") job.cancel(cause) }
发现他其实也是调用了job的cancel,所以我们创建一个job,让一个job对象管理多个协程的取消
class MainActivity : AppCompatActivity() {
val job=Job()
@SuppressLint("MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CoroutineScope(Dispatchers.IO+job).launch {
Log.e("---","开始了协程")
}
CoroutineScope(Dispatchers.IO+job).launch {
Log.e("---","开始了协程")
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
要注意上面的
CoroutineScope(Dispatchers.IO+job)是一个方法,不是创建一个对象,而是一个方法返回一个对象
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
方法3
实现CoroutineScope 接口,因为CoroutineScope 接口中有一个未实现的属性,所以继承者得初始化这个属性,我们可以采用委托类的方式实现
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
实现CoroutineScope 的接口,协程的上下文让MainScope()方法创建的类实现实现
class MainActivity : AppCompatActivity() ,CoroutineScope by MainScope() {
@SuppressLint("MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launch { //这个协程是运行在主线程的,因为mainScope创建的协程上下文中的线程是主线程
}
}
override fun onDestroy() {
super.onDestroy()
cancel()
}
}
我们看下MainScope()方法,
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
注意这个方法创建的协程上下文的指定线程是主线程,所以说如果子协程不指定线程的话,子协程也是在主线程中运行
方式4:
如果我们自己去管理协程的话,感觉麻烦,有时候又会忘记取消,容易造成内存泄露,所以android为我们提供了一些库
//第四种: android提供的和lifecycle相关的库
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
lifecycleScope.launch { } 用在activity和fragment中
viewModelScope.launch{} 用在viewModel中
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
官网地址
协程的原理解析
协程的非阻塞原理其实就是利用 callback + 状态机机制实现的
借鉴文章:
kotlin语法进阶 - 协程(二)挂起函数原理_飞过那时的城镇的博客-CSDN博客
suspend :暂停的意思
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutine)
val launch = GlobalScope.launch(Dispatchers.Main) { //实质上相当于往主线程中post了一个新任务,这个任务就是{}闭包的内容
println("thread name= ${Thread.currentThread().name}")
println("======12")
delay(500)
println("======23")
}
println("--end--")
}
我们看下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
}
分析这个代码:
class CoroutineActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutine)
运行到挂起函数的时候,相当于线程执行到这里切断了,一部分去执行挂起函数,一部分去按顺序去执行
val launch = GlobalScope.launch(Dispatchers.Main) { //挂起函数
println("thread name= ${Thread.currentThread().name}")
println("======12")
delay(500)
println("======23")
}
println("--end--") //接下来的内容
}
}
要注意的是:suspend 关键字只是一个提示的作用,不能起到挂起的作用,有挂起作用的方法有withcontent(),launch等