Kotlin 协程学习笔记

本文介绍了Kotlin协程的基本概念、如何使用GlobalScope和suspend函数进行并发处理,以及协程与线程的区别。作者还探讨了协程在处理网络请求、避免内存泄漏和与RXJava的对比,强调了协程在实际应用中的优势。

Kotlin 协程学习笔记

B站:扔物线-Kotlin协程课程

协程是什么、不是什么

协程是一种在程序中处理并发的一种方案,也是这种方案的一个组件

举例:Adapter适配器模式,即是一种设计模式,也是这个方案的核心组件名称

举例:线程是一种在程序中处理并发的一种方案,也是这种方案的一个组件

协程和线程属于一个层级的概念

p.s. 并发是一段时间内同时处理事情,并行是某一时间点同时处理事情。线程可以处理并行,但是协程不存在并行的概念

kotlin在jvm上的协程,是一个线程框架

在jvm的内核中还是以线程进行的处理


协程怎么用

基本使用

        GlobalScope.launch {
            Log.e("VODLEEEEEE", "Coroutine Demo 1 ${Thread.currentThread().name} ")
        }

日志输出

Coroutine Demo 1 DefaultDispatcher-worker-1

这样,我们就能从日志中看出,这句日志输出时已经不在主线程中。

那协程的优势在哪?在于线程切换


    /**
     * 切换前后台对比演示
     */
    private fun demoChange(){
        // launch 不带参数,默认后台
        GlobalScope.launch(Dispatchers.Main) {
            ioSuspendCode(1)
            uiCode(1)
            ioSuspendCode(2)
            uiCode(2)
            ioSuspendCode(3)
            uiCode(3)
        }
    }

    /**
     * 普通ui方法
     */
    private fun uiCode(num: Int) {
        Log.e("VODLEEEEEE", "Coroutine Demo ui${num} ${Thread.currentThread().name} ")
    }

    /**
     * 挂起io方法 - suspend挂起函数
     */
    suspend fun ioSuspendCode(num: Int) {
        withContext(Dispatchers.IO) {
            Log.e("VODLEEEEEE", "Coroutine Demo suspend io${num} ${Thread.currentThread().name} ")
        }
    }

日志输出

18:57:21.205  E  Coroutine Demo suspend io1 DefaultDispatcher-worker-1 
18:57:21.265  E  Coroutine Demo ui1 main 
18:57:21.266  E  Coroutine Demo suspend io2 DefaultDispatcher-worker-1 
18:57:21.273  E  Coroutine Demo ui2 main 
18:57:21.274  E  Coroutine Demo suspend io3 DefaultDispatcher-worker-1 
18:57:21.275  E  Coroutine Demo ui3 main 

这样,我们在写串行代码的时候,就可以很轻松的按照逻辑顺序从上往下写,不用关心底层是在前台还是后台执行,协程自动执行了前后台切换。

对比之前线程开发,如果函数切换到线程,线程执行结束再回到主线程执行,往往在函数上增加回掉通知线程执行完毕,最常见的就是网络框架。但是我们在调用时,首先不确定回掉函数的执行是在主线程还是子线程,其次在子线程调用方法时,函数还是会额外开启线程,造成线程资源浪费。所以,虽然协程框架本身不如线程框架轻量,但是在实际使用时协程效率更高。

而在上边的代码中,我们看到了这么几个关键,GlobalScope.launchsuspendwithContext

GlobalScope.launch:launch是协程的启动方法,Scope这个单词的意思是范围,GlobalScope的意思就是app全生命周期范围,也就是说,这句的意思是启动一个全生命周期的协程。当然,

suspend:挂起函数标识,表示函数内存在协程前后台切换,需要上下文信息,所以添加关键词的函数必须在协程里头调用。

withContext:挂起函数,用于在协程中切换上下文


网络请求

首先先展示retrofit的基础使用

        val retrofit = Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        val api = retrofit.create(Api::class.java)

        // retrofit基础使用
        api.listRepos("rengwuxian")
            .enqueue(object : Callback<List<Repo>?> {
                override fun onResponse(call: Call<List<Repo>?>, response: Response<List<Repo>?>) {
                    findViewById<TextView>(R.id.tv_main).text = response.body()?.get(0)?.name
                }

                override fun onFailure(call: Call<List<Repo>?>, t: Throwable) {
                }
            })

而协程,就可以直接请求,如果请求失败直接走的try catch,唯一的问题是kotlin 没有强制检查异常,所以不加try也不会报错,这里需要记得手动加上

        GlobalScope.launch(Dispatchers.Main) {
            try {
                // 网络请求
                val repos = api.listReposKt("rengwuxian")
                findViewById<TextView>(R.id.tv_main).text = repos[0].name
            } catch (e: Exception) {
                findViewById<TextView>(R.id.tv_main).text = e.message
                e.printStackTrace()
            }
        }

对比rxjava,rx 需要手动切换前后台,虽然两个都可以切换线程,但是rx需要回掉和包装,协程不需要

        val retrofit = Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava3CallAdapterFactory.createWithScheduler(Schedulers.io())) // rxjava 把网络请求手动放到后台
            .build()

        api.listReposRx("rengwuxian")
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(object : SingleObserver<List<Repo>> {
                override fun onSubscribe(d: Disposable) {
                }

                override fun onError(e: Throwable) {
                    findViewById<TextView>(R.id.tv_main).text = e.message
                }

                override fun onSuccess(t: List<Repo>) {
                    findViewById<TextView>(R.id.tv_main).text = t[0].name
                }
            })

对于多网络请求,rxjava固然好用,但是协程也不差,通过 async 和 await 组合,我们也可以很轻易的处理异步请求。

        // 多请求并发处理对比
        // rx 直接用zip等事件流操作就可以直接合并
        val t = Single.zip(
            api.listReposRx("rengwuxian"),
            api.listReposRx("google")
        ) { list1, list2 -> "${list1[0].name} - ${list2[0].name}" }
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { combined -> findViewById<TextView>(R.id.tv_main).text = combined }

        // 协程需要一个新的东西,async
        GlobalScope.launch(Dispatchers.Main) {
            // async 异步的意思
            val one = async { api.listReposKt("rengwuxian") }
            val two = async { api.listReposKt("google") }
            // await 表示被挂起了,需要等前面先执行完。但是这样主线程是不会被卡出的
            findViewById<TextView>(R.id.tv_main).text = "${one.await()?.get(0)?.name} - ${two.await()?.get(0)?.name}"
        }

协程泄漏

首先协程泄漏的原因是什么。GC root 有好几种,活跃线程就是一种,而被 GC root 直接或间接持有的对象不会被回收。协程在JVM上依然是线程,所以协程的泄漏本质就是线程泄漏

协程提供了回收方式,并且lifecycle等也对协程提供了支持

就比如单个的协程,launch 会返回一个 Job 对象,而这个对象可以被cancel,所以我们可以放在activity被回收时对协程进行回收。

同时,协程采用了结构化并发,通过CoroutineScope集中管理,而GlobalScope、MainScope等,都是他的子类。对于通过 scope.launch() 去启动协程,所有通过scope启动的协程生命周期由scope管理,当scope销毁时,里头的协程也全部cancel。

当然,由于 lifecycle 的支持,我们也可以直接 lifecycleScope.launch 去启动协程,而这样启动的协程在activity destroy的时候自动cancel,就不需要我们手动cancel了。至于launchWhenStarted几个方法,目前已经废弃,还是尽量不要使用了。

    private fun lifecycleSupport() {
        // 如果协程存在范围大于了activity,就可能存在协程泄漏
        // 单个协程回收,可以通过job记录,然后在onDestroy回收
        job = GlobalScope.launch {
            ioSuspendCode(1)
            uiCode(1)
        }

        // 多协程回收,可以记录到一个scope上,然后在onDestroy回收
        scope.launch {
            ioSuspendCode(1)
            uiCode(1)
        }
        scope.launch {
            ioSuspendCode(2)
            uiCode(2)
        }

        // 如果直接用lifecycle,就会自动回收,都不用 onDestory
        lifecycleScope.launch {
            ioSuspendCode(1)
            uiCode(1)
        }

//        lifecycleScope.launchWhenStarted {  }
//        lifecycleScope.launchWhenStarted {  }
//        lifecycleScope.launchWhenCreated {  }
    }

    // kotlin 提供的主线程环境
    private val scope = MainScope()

    // 协程 launch 会返回 job,为了放置内存泄漏,需要在onDestroy 中cancel
    private var job: Job? = null

    override fun onDestroy() {
        super.onDestroy()
        job?.cancel()
        scope.cancel()
    }

最后,对于协程切到主线程,还有rx往主线程切,根据源码看,还是handler


总结

协程是什么–线程框架

协程怎么用 – launch withContext 以及其他一些函数

协程性能更高吗? – 本质上不会,但是实操时会,因为suspend标记功能

CoruntineScope – 结构化管理线程 – 避免泄漏

协程的本质是什么 – 线程

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值