1.前言
前面两节,我们运用了kotlin提供的简单协程去实现了一套更易用的复合协程,这些基本上是以官方协程框架为范本进行设计和实现的。虽然我们还没有直接接触kotlin官方协程框架,但对它的绝大多数功能已经了如指掌了。本节,我们来探讨一下官方协程框架的更多功能,并将其运用到实际的生产当中,在这里,我以在Android中使用kotlin官方协程框架为例进行讲述。
2.launch函数启动一个协程
在Android开发中,我们一般将协程的作用域和Android组件的lifeCycle绑定在一起,这样,当组件销毁的时候,协程的作用域就会取消,协程也就销毁了,这样不会造成内存泄漏。在ViewModel中,我们可以直接使用viewModelScope这个作用域去创建协程,在Activity/Fragment这些拥有生命周期的组件中,我们可以使用lifecycleScope去创建协程,这里我们使用lifecycleScope进行讲述。
这里我们先给出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
}
我们先补充一个知识点,协程的启动模式,也就是start参数所设置的,总共有四种启动模式,如下所示:
- DEFAULT:创建协程之后,立即开始调度,在调度前如果协程被取消,其将直接进入取消响应状态
- ATOMIC:协程创建后,立即开始调度,协程执行到第一个挂起点之前不响应取消
- LAZY:只有协程被需要时,包括主动调用start,join,await等函数时才会开始调度,如果调度前被取消协程就会进入异常结束状态
- UNDISPATCHED:协程创建之后立即在当前函数的调用栈中执行,直到遇到第一个真正挂起的点
这里我们要搞清楚立即调度和立即执行的区别,立即调度表示协程的调度器会立即接收调度指令,但具体执行的时机以及在哪个线程上执行还需要根据调度器的情况而定,也就是说立即调度到立即执行前通常会隔一段时间。这里,我们给出一段代码,每隔一段时间打印一个数字:
lifecycleScope.launch{
for(a in 1..10){
Log.i("lifecycleScope",a.toString())
delay(1000)
}
}
这里需要注意的是,如果不指定调度器,那么该协程默认运行在UI线程上,指定调度器可以通过context参数指定,和上一节我们实现的一样,这里不再赘述。
lauch函数的返回值是Job对象,Job对象常用的属性和函数如下:
- isActive:判断Job是否处于活动状态
- isCompleted:判断Job是否属于完成状态
- isCancelled:判断Job是否被取消
- start():开始Job
- cancel():取消Job
- join():将当前协程挂起,直到该协程完成
- cancelAndJoin():前两个函数的结合体
3.async函数启动一个协程
async和launch函数的不同点在于launch函数启动的协程是没有返回值的,而async函数启动的协程是有返回值的。async函数返回一个Deferred对象,它继承自Job对象,我们可以通过Deferred对象中的await函数获取协程的执行结果,代码如下:
lifecycleScope.launch{
val deferred=async{
"计算结果"
}
val result=deferred.await()
Log.i("lifecycleScope",result)
}
async函数和launch函数的共同点是他们不会等待协程执行结束,会立马往下执行,测试如下:
lifecycleScope.launch {
lastTime = System.currentTimeMillis()
async {
delay(1000)
}
async {
delay(1000)
}
Log.i("耗时", (System.currentTimeMillis() - lastTime).toString())
}
打印的结果是1ms,并不是2000毫秒,也就是说多个async函数是并行执行的,当然,这里换成launch结果也是一样的。当然,如果你在后面加一个await函数,那么结果就是2000ms左右了,也就是这样:
lifecycleScope.launch {
lastTime = System.currentTimeMillis()
async {
delay(1000)
}.await()
async {
delay(1000)
}.await()
Log.i("耗时", (System.currentTimeMillis() - lastTime).toString())
}
使用launch加上join函数的结果也是一样的。如果我再换一种写法:
lifecycleScope.launch {
lastTime = System.currentTimeMillis()
val job1=launch {
delay(1000)
}
val job2=launch {
delay(1000)
}
job1.join()
job2.join()
Log.i("耗时", (System.currentTimeMillis() - lastTime).toString())
}
这里的执行结果是1000毫秒左右,可以自行尝试,换成async函数加await也是一样的。
通过上面的测试,我们可以得出结论,launch函数和async函数启动的协程是并行执行的,并且启动协程之后会立马往下执行,不会等待协程完成,除非调用join或await函数。launch函数和async函数的唯一区别就是async函数启动的协程有返回值,如果不需要获取协程的执行结果,那么没必要用async函数。
4.withContext函数的作用
官方框架中还为我们提供了一个好用的api,withContext(),它的定义如下:
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
withContext会将参数中的lambda表达式调度到由context指定的调度器上运行,并且它会返回协程体当中的返回值,它的作用几乎和async{}.await()等价,但和async{}.await()相比,它的内存开销更低,因此对于使用async后立即要调用await的情况,应当优先使用withContext函数。而且有了withContext之后,在Android开发的时候,就可以不再使用Handler了,我们可以在需要进行耗时操作(网络请求,数据库读写,文件读写)时,使用withContext切换到IO线程上,在得到想要的结果后要更新UI时又可以切换到UI线程上,非常的方便。测试如下:
lifecycleScope.launch(Dispatchers.IO) {
delay(1000)
val result="JJLin"
withContext(Dispatchers.Main){
tv_display.text=result
}
}
这段代码模拟了在IO线程上进行耗时操作,可以是数据库访问,网络请求之类的;拿到结果后,用withContext切换到主线程,进行UI的更新。
5.协程的超时取消
kotlin官方协程框架为我们提供了一个withTimeout()函数用于执行超时取消设置,这个api的定义如下:
public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T
这个函数可以设置一个超时时间,超过这个时间后就会通过抛出异常来取消这个协程,如果不想抛出异常,可以使用withTimeoutOrNull,这个函数在超时之后会返回null,而不会抛出异常。