前言
在上一篇中,对Kotlin协程对应取消组合挂起函数进行了初步的认识。在这篇中,将会讲解Kotlin协程对应的释放资源、超时、组合挂起函数相关知识点!
话不多说,直接开始!
先看上一篇的例子:
fun main() = runBlocking<Unit> {
val job = launch {
repeat(1000){ i ->
println("job:I'm sleeping $i")
delay(10L)
}
}
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 {
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 {
val result = withTimeoutOrNull(1330L){
repeat(1000){ i ->
println("I'm sleeping $i")
delay(500L)
}
"OK"
}
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){
delay(30)
Resource()
}
resource.close()
}
}
}
println(acquired)
}
这里我们看到,一开始创建了模拟资源文件,每次使用全局自增一,每次关闭全局自减一。
如果说最后打印为0,则说明资源全部释放完毕,否则就会出现内存泄露!
注意:这里我故意没有用finally ,来看看没有用finally 是什么样子!
运行效果
51 //每次运行都还不一样!如果每次都为0的话,可以改上面备注的那个挂起时间!
那加上finally 试试!
3.2 加上finally{}
import kotlinx.coroutines.*
var acquired = 0
class Resource {
init {
acquired++
}
fun close() {
acquired--
}
}
fun main() {
runBlocking {
repeat(1000) {
launch {
var resource: Resource? = null
try {
withTimeout(60) {
delay(30)
resource = Resource()
}
} finally {
resource?.close()
}
}
}
}
println(acquired)
}
这里我们看到,在try 上面定义了可空变量,每次实例化资源的时候就赋值,在finally 里面如果不为空就关闭资源。
运行效果
0
所有资源完美释放完毕,当然读者可以将上面的注释打开再次运行下,看是否执行过对应的方法。
4、组合挂起函数
在上一篇中,讲解了何为挂起函数!那么现在来看看组合挂起函数怎么使用的!
4.1 默认使用
suspend fun doSomethingUsefulOne(): Int {
println("doSomethingUsefulOne")
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")
delay(1000L)
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
println("doSomethingUsefulTwo")
delay(1000L)
return 29
}
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")
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.start()
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协程上下文与调度器的讲解!
|