1.协程coroutine
协程是一种并发设计模式,在Kotlin中使用协程可以简化异步执行的代码,把异步回调代码同步化。
协程,其实就是相互协作的子程序,多个子程序之间通过一定的机制相互关联、协作地完成某项任务。比如一个协程在执行上可以被分为多个子程序,每个子程序执行完成后主动挂起,等待合适的时机再恢复;一个协程被挂起时,线程可以执行其它子程序,从而达到线程高利用率的多任务处理目的——协程在一个线程上执行多个任务,而传统线程只能执行一个任务,从多任务执行的角度,协程自然比线程轻量。
协程主要用来解决两个问题:
①处理耗时任务,这种任务常常会阻塞住主线程;
②保证主线程安全,即确保安全地从主线程调用任何suspend函数。
协程的优点:
①轻量。可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
②内存泄漏更少。协程使用结构化并发机制在一个作用域内执行多项操作。
③内置取消支持。取消操作会自动在运行中的整个协程层次结构内传播。
④Jetpack集成。许多Jetpack库都包含提供全面协程支持的扩展。ViewModel等还提供了协程作用域(比如viewModelScope),用于结构化并发。
看一个简单的协程:
class CoroutineTestActivity : Activity(){
private val scope = MainScope()
private val textContent by lazy {
findViewById<TextView>(R.id.tv_corout_name)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_corout)
//通过MainScope,启动一个协程
scope.launch(Dispatchers.Main) {
val data = obtainCacheData() //异步获取数据
textContent.text = data //更新UI
}
}
private suspend fun obtainCacheData(): String{
//通过Dispatchers调度器,切换到子线程执行耗时操作
return withContext(Dispatchers.IO) {
delay(8000)
"缓存数据" //返回的数据
}
}
}
该代码执行过程:
①执行onCreate函数,通过MainScope.launch在主线程上创建新协程,然后协程开始执行。
②在协程内,调用obtainCacheData()方法,这是一个suspend挂起方法,所以现在会挂起协程的进一步操作。直到obtainCacheData()里面的withContext块执行结束。
③withContext块执行结束后,onCreate中通过launch创建的协程恢复执行操作,返回数据。
可以看到,相对于一般的函数,协程多了2个状态:挂起(suspend)和恢复。挂起与阻塞不同,代码阻塞之后会一直停留在这里。而挂起是指到挂起点被挂起后,保存挂起点,主线程继续执行,当挂起函数执行完了,再恢复执行操作。
注:挂起函数只能在协程体内或者其它挂起函数内调用。
2.创建协程coroutine
首先在build.gradle中添加依赖 :
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
协程需要运行在协程上下文环境,在非协程环境中启动协程,有三种方式:
①runBlocking{}
启动一个新协程,并阻塞当前线程,直到其内部所有逻辑及子协程逻辑全部执行完成。
该方法的设计目的是让suspend风格编写的库能够在常规阻塞代码中使用,常在main方法和测试中使用。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.e(TAG, "主线程id:${mainLooper.thread.id}")
test()
Log.e(TAG, "协程执行结束")
}
private fun test() = runBlocking {
repeat(8) {
Log.e(TAG, "协程执行$it 线程id:${Thread.currentThread().id}")
delay(1000)
}
}
runBlocking启动的协程任务会阻断当前线程,直到该协程执行结束。当协程执行结束之后,页面才会被显示出来。
②GlobalScope.launch{}
在应用范围内启动一个新协程,协程的生命周期与应用程序一致。这样启动的协程并不能使线程保活,就像守护线程。不会阻塞当前线程。
由于这样启动的协程存在启动协程的组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,因此并不推荐这样启动,尤其是在客户端这种需要频繁、创建销毁组件的场景。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.e(TAG, "主线程id:${mainLooper.thread.id}")
val job = GlobalScope.launch {
delay(6000)
Log.e(TAG, "协程执行结束 -- 线程id:${Thread.currentThread().id}")
}
Log.e(TAG, "主线程执行结束")
}
运行结果:
主线程id:1
主线程执行结束
协程执行结束 -- 线程id:480
因此,从执行结果看出,launch不会阻断主线程。
③async/await:Deferred
async跟launch的用法基本一样,区别在于:async有返回值,返回值类型是是Deferred。async不会阻塞当前线程。async支持并发,并且一般跟await一起使用。
async和await是两个函数,这两个函数在使用过程中一般都是成对出现的。
async用于启动一个异步的协程任务,await用于得到协程任务结束时返回的结果,结果是通过一个Deferred对象返回的。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch {
val result = GlobalScope.async {
delay(6000)
Log.e(TAG, "协程执行结束,线程id:${Thread.currentThread().id}")
"hello world"
}
Log.e(TAG, "async结果result = " + result.await())
}
Log.e(TAG, "主线程执行结束")
}
运行结果:
主线程id:1
主线程执行结束
协程执行结束,协程id:48
async结果result=hello world
所以,async是不阻塞线程的,可以通过await()获取到async启动协程的结果。
多个async一般是可以同时进行的,即异步执行。
GlobalScope.launch {
val asyncJob1 = async {
Log.d(TAG, "任务1,线程id:${Thread.currentThread().id}")
"第 1 个任务完成"
}
//asyncJob1.await()
val asyncJob2 = async {
Log.d(TAG, "任务2,线程id:${Thread.currentThread().id}")
"第 2 个任务完成"
}
}
没有中间的asyncJob1.await()时,运行结果可能任务2在前,也可能任务1在前,因为两个任务是在两个不同的线程里并行执行的。
加上中间的asyncJob1.await()后,就可以实现两个任务串行执行,永远先执行任务1,任务1完成后才会执行任务2。
有些时候可能需要调用多个接口,然后根据返回值再调用其他的接口。比如首先任务1和任务2并发执行,得到任务1和任务2的执行结果后,再去执行任务3,这时候也可以使用async+await实现。
GlobalScope.launch {
//任务1和任务2并行执行
val asyncJob1 = async {
delay(2000) //模拟任务1耗时
Log.d(TAG,"----第 1 个任务")
"第 1 个任务结果" //任务1返回基地国
}
val asyncJob2 = async {
delay(2000)
Log.d(TAG,"----第 2 个任务")
"第 2 个任务结果"
}
//在这里使用await就是并行执行,不要在上面添加await(),否则就成了串行执行
val resultJob1 = asyncJob1.await()
val resultJob2 = asyncJob2.await()
//根据任务1和任务2的执行结果再去执行其他任务
withContext(Dispatchers.IO){
Log.d(TAG,"$resultJob1,$resultJob2,执行完成了----执行最后的任务")
}
}
运行结果:
----第 1 个任务
----第 2 个任务
第 1 个任务结果,第 2 个任务结果,执行完成了----执行最后的任务
大部分时候,执行耗时的串行操作的话,会使用WithContext,使异步代码同步化。并且它也不用开启一个新的协程:
return withContext(Dispatchers.IO) {
delay(8000)
"缓存数据"
}
注意:await 的调用时机很重要,这影响到多个async任务是串行执行还是并发执行。下面通过测试耗时来验证一下:
第一种情况:
GlobalScope.launch {
val time = measureTimeMillis {
val asyncJob1 = async {
delay(2000)
Log.d(TAG, "----第 1 个任务")
"第 1 个任务完成"
}
val asyncJob2 = async {
delay(1000)
Log.d(TAG, "----第 2 个任务")
"第 2 个任务完成"
}
asyncJob1.await()
asyncJob2.await()
}
Log.d(TAG, "--measureTime耗时: $time")
}
第二种情况:
GlobalScope.launch {
val time = measureTimeMillis {
async {
delay(2000)
Log.d(TAG, "----第 1 个任务")
"第 1 个任务完成"
}.await()
async {
delay(1000)
Log.d(TAG, "----第 2 个任务")
"第 2 个任务完成"
}.await()
}
Log.d(TAG, "--measureTime2耗时: $time")
}
运行结果:
--measureTime耗时: 2180
--measureTime2耗时: 3178
通过上面结果可以看出,await()执行的时机很重要:measureTime()方法,就是并行执行的;measureTime2()方法,就是串行执行的。
3.launch
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() 是CoroutineScope的一个扩展函数,CoroutineScope就是协程的作用范围。
launch方法有三个参数:协程下上文;协程启动模式;协程体(block是一个带接收者的函数字面量,接收者是CoroutineScope)。
①协程下上文
上下文可以有很多作用,包括携带参数、拦截协程执行等。多数情况下不需要自己实现上下文,只需要使用现成的就好。
上下文有一个重要的作用就是线程切换,Kotlin协程使用调度器来确定哪些线程用于协程执行。调度器实现了CoroutineContext接口。
Kotlin提供的调度器:
1)Dispatchers.Main:在主线程上运行一个协程。可以用来更新UI 、调用挂起函数等。在UI线程中执行。
2)Dispatchers.IO:在主线程之外执行I/O操作(缓存、文件、数据库等数据)。在线程池中执行。
3)Dispatchers.Default:在主线程之外执行cpu密集型的工作(数据解析、数据计算等)。在线程池中执行。
4)Dispatchers.Unconfined:在调用的线程直接执行。
②启动模式
在Kotlin协程当中,启动模式定义在一个枚举类中。一共定义了4种启动模式:
public enum class CoroutineStart {
DEFAULT, //默认的模式,立即执行协程体。
LAZY, //只有在需要的情况下运行
ATOMIC, //立即执行协程体,但在开始运行之前无法取消
UNDISPATCHED; //立即在当前线程执行协程体,直到第一个suspend调用
}
DEFAULT模式:创建协程后立即开始调度。在调度之前如果协程被取消,那么它就不会执行,而是以抛出异常来结束。
ATOMIC模式:创建协程后,根据协程的上下文,立即开始调度。协程在执行到第一个挂起点(挂起函数)之前,不能取消。
LAZY模式:只有协程被需要时,包括主动调用协程的start()/join()/await()等函数时,才开始。如果协程在被执行前取消,那么它就不会执行,而是以抛出异常来结束。
runBlocking {
val job = launch(start = CoroutineStart.LAZY) {
Log.d(TA, "LAZY start")
}
Log.d(TAG, "开始一些计算")
delay(3000)
Log.d(TAG, "耗时操作完成")
job.start()
}
运行结果:
开始一些计算
耗时操作完成
LAZY start
可以看到,只有调用了start()后才会打印。
UNDISPATCHED模式:协程创建后,立即在当前函数调用栈执行(在哪个线程创建,在哪个线程执行)。
这些模式特点:
1)DEFAULT、ATOMIC创建后,会立即调度(并不是立即执行);LAZY是只有触发了,才会执行;UNDISPATCHED会立即执行。
2)UNDISPATCHED执行的线程是创建它的函数所在线程,哪怕指定线程,也无效。
3)DEFAULT取消时,会被立即取消。
③协程体
协程体是一个用suspend关键字修饰的一个无参、无返回值的函数类型。被suspend修饰的函数称为挂起函数,与之对应的是关键字resume(恢复)。注意:挂起函数只能在协程中和其他挂起函数中调用,不能在其他地方使用。
suspend函数会将整个协程挂起,而不仅仅是这个suspend函数,也就是说一个协程中有多个挂起函数时,它们是顺序执行的。
举例:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch {
val token = getToken()
val userInfo = getUserInfo(token)
setUserInfo(userInfo)
}
repeat(3){
Log.e(TAG,"主线程执行$it")
}
}
private fun setUserInfo(userInfo: String) {
Log.e(TAG, userInfo)
}
private suspend fun getToken(): String {
delay(2000)
return "token"
}
private suspend fun getUserInfo(token: String): String {
delay(2000)
return "$token - userInfo"
}
运行结果:
主线程执行0
主线程执行1
主线程执行2
token - userInfo
可见,getToken方法将协程挂起,协程中其后面的代码永远不会执行,只有等到getToken挂起结束恢复后才会执行。同时协程挂起后不会阻塞其他线程的执行。