前言
在上一篇中,对Kotlin协程对应取消组合挂起函数进行了初步的认识。在这篇中,将会讲解Kotlin协程对应的释放资源、超时、组合挂起函数相关知识点!
话不多说,直接开始!
先看上一篇的例子:
fun main() = runBlocking<Unit> {
val job = launch {
repeat(1000){ i ->
println("job:I'm sleeping $i")
delay(10L)
//分析点1 ,如果说在这释放资源??会有什么后果??
}
}
delay(130L)
println("main: I'm tired of waiting!")
job.cancelAndJoin()
println("main:Now I can quit!")
}
运行效果
job:I'm sleeping 0
job:I'm sleeping 1
...略
job:I'm sleeping 10
main: I'm tired of waiting!
main:Now I can quit!
首先我们通过repeat(1000)
是想让闭包内部的代码循环1000次,但是主线程因为调用job.cancelAndJoin()
而强制终止了内部循环。
试想一下,我们在请求网络连接网络的时候,以及读取文件流的时候,都是通过耗时操作都是在线程里面执行的。
如果说,像这样通过job.cancelAndJoin()
,造成没有执行完或者说对应的流没有正常释放关闭时,将会造成大量的内存泄露。(也就是分析点1位置)
因此,Kotlin给我们对应的解决方案:
1、在 finally 中释放资源
刚刚的代码因此被改造成:
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job:I'm sleeping $i ...")
delay(10L)
//就不在这里释放资源
}
} finally {
//在这里释放资源
// 任何尝试在 finally 块中调⽤挂起函数的⾏为都会抛出 CancellationException
// 因为这⾥ 持续运⾏的代码是可以被取消的
println("job:I'm running finally")
}
}
delay(130L)
println("main:I'm tired of waiting!")
job.cancelAndJoin()
println("main:Now I can quit.")
}
在这里加了一个finally{}
,先来看看运行效果:
job:I'm sleeping 0 ...
job:I'm sleeping 1 ...
...略
job:I'm sleeping 10 ...
main:I'm tired of waiting!
job:I'm running finally
main:Now I can quit.
注意看这个运行效果:
- 先是运行的是:
main:I'm tired of waiting!
,随后job:I'm running finally
; - 也就是说,当主线程执行
job.cancelAndJoin()
将会一直等待协程里面的finally{}
运行完后才会继续往下执行! - 因此不管对应的协程是否正常完成,都会先执行这代码块,随后才是真正的完成,所以可以将释放资源部分放在
finally{}
这里
然而,在真实释放资源过程中,往往需要一个被取消的协程,那试试在finally{}
里调用挂起函数呢?
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job:I'm sleeping $i ...")
delay(10L)
}
} finally {
delay(20L)
println("job:I'm running finally")
}
}
delay(130L)
println("main:I'm tired of waiting!")
job.cancelAndJoin()
println("main:Now I can quit.")
}
代码没有报任何错!运行看看效果:
...略
job:I'm sleeping 9 ...
job:I'm sleeping 10 ...
main:I'm tired of waiting!
main:Now I can quit.
注意看最后几行!我的job:I'm running finally
释放资源并没有执行!
这个时候就需要新的东西了! withContext(NonCancellable){}
1.1 withContext(NonCancellable){}
我们先来看看这个代码块具体是什么意思!
如图所示
总结下就一句话:当执行取消时,对应 withContext
内的代码块不会随着取消而取消,还是会执行对应代码块的内容。更重要的是不会影响原有协程的逻辑!
所以再次改造代码:
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job:I'm sleeping $i ...")
delay(10)
}
} finally {
withContext(NonCancellable){
println("job:I'm running finally")
delay(20L)
println("可以在这释放对应的资源了")
}
}
}
delay(130L)
println("main:I'm tired of waiting!")
job.cancelAndJoin()
println("main:Now I can quit.")
}
运行效果
job:I'm sleeping 0 ...
job:I'm sleeping 1 ...
...略
job:I'm sleeping 10 ...
job:I'm sleeping 11 ...
main:I'm tired of waiting!
job:I'm running finally
可以在这释放对应的资源了
main:Now I can quit.
当我们执行取消或者请求网络耗时操作时,往往可能会面对超时的情况,那么超时该如何处理呢?
2、超时处理
2.1 withTimeout
fun main() = runBlocking {
withTimeout(1330L){
repeat(1000){ i ->
println("I'm sleeping $i")
delay(500L)
}
}
}
从这个代码块看出:在1330L
毫秒里循环了1000次,每一次都挂起了500毫秒!
运行效果
I'm sleeping 0
I'm sleeping 1
I'm sleeping 2
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1330 ms
我们发现,当超过1330L
毫秒时,就直接崩溃了!在实际使用中肯定不允许程序奔溃,所以有withTimeoutOrNull
来替代超时操作!
2.2 withTimeoutOrNull
fun main() = runBlocking {
//withTimeoutOrNull 通过返回 null 来进⾏超时操作,从⽽替代抛出⼀个异常:
val result = withTimeoutOrNull(1330L){
repeat(1000){ i ->
println("I'm sleeping $i")
delay(500L)
}
"OK" //当返回OK时,表示上面的循环执行完了,否则就为NULL
}
println(result ?: "Done")
}
这里我们看到使用了withTimeoutOrNull
来代替withTimeout
,从方法上看,好像超时的话就为Null!
来看看运行效果:
I'm sleeping 0
I'm sleeping 1
I'm sleeping 2
Done
这没有好多可说的,很简单!下一个!
现在讲了释放资源,以及超时。模拟一个实战看看!
3、模拟实战
我们就模拟一个瀑布流加载本地图片文件操作
3.1 没有加finally{}
import kotlinx.coroutines.*
var acquired = 0
class Resource {
init {
acquired++
}
fun close() {
acquired--
}
}
fun main() {
runBlocking {
repeat(1000){
launch {
val resource = withTimeout(60){ //设置60毫秒加载时间
delay(30) //这里可以随意改 只有不超过60 ,不让它超时就行
Resource()
}
//使用完了关闭对应资源
resource.close()
}
}
}
//非0,有内存泄漏
//0代表资源都释放了,没有内存泄漏
println(acquired) //期待值是0
}
这里我们看到,一开始创建了模拟资源文件,每次使用全局自增一,每次关闭全局自减一。
如果说最后打印为0,则说明资源全部释放完毕,否则就会出现内存泄露!
注意:这里我故意没有用finally
,来看看没有用finally
是什么样子!
运行效果
51 //每次运行都还不一样!如果每次都为0的话,可以改上面备注的那个挂起时间!
那加上finally
试试!
3.2 加上finally{}
import kotlinx.coroutines.*
var acquired = 0
//伪代码
class Resource {
init {
// println("init $acquired")
acquired++
}
fun close() {
// println("close $acquired")
acquired--
}
}
fun main() {
runBlocking {
repeat(1000) {
launch {
var resource: Resource? = null
try {
withTimeout(60) {
delay(30)
resource = Resource()
}
} finally {
resource?.close()
}
}
}
}
//0代表资源都释放了,没有内存泄漏
println(acquired) //期待值是0
}
这里我们看到,在try
上面定义了可空变量,每次实例化资源的时候就赋值,在finally
里面如果不为空就关闭资源。
运行效果
0
所有资源完美释放完毕,当然读者可以将上面的注释打开再次运行下,看是否执行过对应的方法。
4、组合挂起函数
在上一篇中,讲解了何为挂起函数!那么现在来看看组合挂起函数怎么使用的!
4.1 默认使用
suspend fun doSomethingUsefulOne(): Int {
println("doSomethingUsefulOne")
//所有kotlinx.coroutines中的挂起函数都是可被取消的。
delay(1000L)
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
println("doSomethingUsefulTwo")
delay(1000L)
return 29
}
fun main() = runBlocking {
val time = measureTimeMillis { // 这个方法仅仅只是统计闭包里代码块总共运行的时间!
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
}
这个measureTimeMillis {}
仅仅表示统计闭包里运行总时长。其余的没啥可说的!
直接看运行效果
doSomethingUsefulOne
doSomethingUsefulTwo
The answer is 42
Completed in 2070 ms
可以看到这两个方法都是按照顺序执行的!上面执行完了才执行下一个!
那如果说,想要这两个一起执行(同步执行)该怎样呢?
4.2 同步执行
suspend fun doSomethingUsefulOne(): Int {
println("doSomethingUsefulOne")
//所有kotlinx.coroutines中的挂起函数都是可被取消的。
delay(1000L)
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
println("doSomethingUsefulTwo")
delay(1000L)
return 29
}
//使⽤ async 并发
fun main() = runBlocking {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
//这⾥快了两倍,因为两个协程并发执⾏。请注意,使⽤协程进⾏并发总是显式的。
println("Completed in $time ms")
}
现在发现在调用每个挂起函数时,额外加了async {}
闭包,使用时额外多了.await()
方法。
来看看运行效果:
doSomethingUsefulOne
doSomethingUsefulTwo
The answer is 42
Completed in 1073 ms
从这个运行效果来看,可以发现这俩方法是同步执行的!
那如果说,不想让它这么快执行,我想先初始化好,等我想执行的时候再来执行它!
Koltin也给我们安排上了!
4.3 惰性执行
suspend fun doSomethingUsefulOne(): Int {
println("doSomethingUsefulOne")
//所有kotlinx.coroutines中的挂起函数都是可被取消的。
delay(1000L)
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
println("doSomethingUsefulTwo")
delay(1000L)
return 29
}
fun main() = runBlocking {
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
//执行two之前,这里随意添加任何逻辑判断
two.start()
//执行one之前,这里随意添加任何逻辑判断
//two.join() 加上这句话的意思就是,等待two执行完毕时,才调用下面的代码
one.start()
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}
这里我们看到,在async
后面额外增加了(start = CoroutineStart.LAZY)
代码块,表示对应的挂起函数为惰性函数,需要后面手动调用对应的.start()
才会执行对应的挂起函数。
这里我故意将two
放在one
之前!
来看看运行效果
doSomethingUsefulTwo
doSomethingUsefulOne
The answer is 42
Completed in 1098 ms
因为这在两个挂起函数启动之间没有加任何逻辑代码,所以运行时间和同步差不多。但可以看出,已经可以通过代码来控制对应挂起函数的执行顺序!达到了惰性执行的效果!
结束语
好了,本篇到这里就结束了!相信你对Kotlin有了更深一步的认知!在下一篇中,将会开启对Kotlin协程上下文与调度器的讲解!