程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

2023-06(4)

Android—Kotiln进阶教程(三)

发布于2021-11-21 21:21     阅读(579)     评论(0)     点赞(29)     收藏(2)


前言

在上一篇中,对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协程上下文与调度器的讲解!



所属网站分类: 技术文章 > 博客

作者:javajava我最强

链接:http://www.javaheidong.com/blog/article/326718/78f756dd257b76faa4d6/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

29 0
收藏该文
已收藏

评论内容:(最多支持255个字符)