Kotlin的协程和线程

464 阅读3分钟

一、 协程是什么?

  • 协程是基于Java的线程框架
  • 可以用看起来同步的代码写出实质上是异步的操作。(在协程里的每一行前后台按顺序切换)
    • 关键亮点一:耗时函数自动在后台执行,从而提高性能
    • 关键亮点二:线程的【自动切回】
  • 协程是一种在程序中处理并发任务的方案;也是这种方案的一个组件;类似于:Adapter是界面和数据对接的方案,Adapter也相当于一种组件。
  • 协程和线程属于一个层级的概念; 并发是相对宏观一点的概念,有多件事要做,并且是一整套事的不同部分;并行是同一时间两件事有没有同时发生。协程不存在线程,也不存在并行;kotlin协程是基于jvm,底层是线程机制

二、协程好处

如果要执行后台任务,接着执行前台任务,再执行前台任务,再前台...,如果这样频繁切换线程,就会非常麻烦,就需要用到协程了

{
GlobalScope.launch {
            ioCode1()//运行在后台   ,挂起函数需要在协程里执行或者在另外一个挂起函数执行
        }
        uiCode1()//运行在前台
  }      
    private fun uiCode1() {
        println(">>>>>>>>>>uiCode1 : ${Thread.currentThread().name}")
    }

    private suspend fun ioCode1() {//suspend :挂起,把函数标记为挂起函数,执行在后台
        println(">>>>>>>>>>ioCode1 : ${Thread.currentThread().name}")
    }
    
    >>>>>>>>>>uiCode1 : main
    >>>>>>>>>>ioCode1 : DefaultDispatcher-worker-1

从打印的日志可以看出来,先执行的uiCode1,再执行的ioCode1,这样的顺序显然是不对的,不符合需求,这就需要把方法都移到协程里,协程控制不了外面的代码

GlobalScope.launch {
            ioCode1()//运行在后台   ,挂起函数需要在协程里执行或者在另外一个挂起函数执行
            uiCode1()//运行在前台
        }
        
>>>>>>>>>>ioCode1 : DefaultDispatcher-worker-1
>>>>>>>>>>uiCode1 : DefaultDispatcher-worker-1

从打印的日志可以看出,两个都成后台线程了,因为协程就是在后台执行的,每一个协程都会提供一个线程池,线程池可能是后台,可能是主线程,都有可能,协程就是创建了一个线程池环境

  • 可以保证协程里的代码都在线程环境里面执行
  • 当挂起函数把线程切走之后,后面的代码会回到线程环境继续执行。 当协程创建的环境是主线程环境,执行完后台环境后,会自动回到主线程环境进行UI更新。
GlobalScope.launch (Dispatchers.Main){//默认是后台线程,Dispatchers.Main主线程环境
            ioCode1()//运行在后台   
            uiCode1()//运行在前台
        }
        
>>>>>>>>>>ioCode1 : main
>>>>>>>>>>uiCode1 : main

出错了,两个都成主线程了!!,suspend是一个标记作用,他本身是不切线程的,需要手动切到主线程。

 private suspend fun ioCode1() {//suspend :挂起,把函数标记为挂起函数,执行在后台,suspend会检查这个函数里有没有别的挂起函数
        withContext(Dispatchers.IO){//手动切换到后台线程
            println(">>>>>>>>>>ioCode1 : ${Thread.currentThread().name}")
        }
    }

三、协程的代码怎么写

  • 用launch()来开启一段协程,一般需要指定Dispatchers.Main。
  • 把要在后台工作的函数,写成suspend函数,需要在内部调用其他suspend函数来真正切线程。
  • 按照一条线写下来,线程自动会切换

四、协程的额外天然优势:性能

协程是自动提高软件的性能,挂起函数,自动切回,协程是链式的

  • 程序什么时候会需要切线程?
    • 工作比较耗时:放在后台,界面不会卡顿
    • 工作比较特殊:放在指定线程-----一般来说,是主线程

五、suspend

  • 并不是用来切换函数的
  • 关键作用:标记和提醒(提醒是挂起函数,需要在协程里执行)

六、Retrofit对协程的支持

对协程的suspend关键字的支持。 对网络请求加上suspend关键字,就会自动在后台执行。 kotlin 中取消了强制try catch

七、Kotlin的协程和RxJava

interface Api{

    @GET("users/{user}/repos")
    fun listRepos(@Path("user")user : String): Call<List<Repo>>

    @GET("users/{user}/repos")
    suspend fun listReposKt(@Path("user")user : String): List<Repo>

    @GET("users/{user}/repos")
    fun listReposRx(@Path("user")user : String): Single<List<Repo>>//Single单事件流
}

data class Repo(
    val name: String)
    
    
    val retrofit = Retrofit.Builder()
            .baseUrl("https://xxx.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava3CallAdapterFactory.createWithScheduler(Schedulers.io()))//网络请求放在后台处理
            .build()

        val api = retrofit.create(Api::class.java)
        
        
        //retrofit 普通请求
        api.listRepos("xxx")
            .enqueue(object : Callback<List<Repo>>{
                override fun onResponse(call: Call<List<Repo>>, response: Response<List<Repo>>) {
                    Log.e(">>>>>请求结果",">>>: ${response.body()?.get(0)?.name}")
                    binding.tvText.text = response.body()?.get(0)?.name
                }

                override fun onFailure(call: Call<List<Repo>>, t: Throwable) {
                    Log.e(">>>>>>请求错误",">>>>>>${t.message}")
                }

            })
            
         //协程请求
        GlobalScope.launch {
            try {
                var repos = api.listReposKt("xxx")
                binding.tvText.text = repos[0]?.name
            }catch (e:Exception){
                binding.tvText.text = e.message

            }

        }
        
        
        
        //通过RxJava请求
        api.listReposRx("xxx")
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(object : SingleObserver<List<Repo>>{
                override fun onSubscribe(d: Disposable) {

                }

                override fun onSuccess(t: List<Repo>) {
                    binding.tvText.text = t[0].name
                }

                override fun onError(e: Throwable) {
                    binding.tvText.text = e.message
                }

            })

RxJava是什么?

  • 管理并发任务
  • 管理事件流 RxJava好在哪?
  • 都可以切线程
  • 都不需要嵌套
  • Rxjava需要回调,协程不需要回调 flatmap

如果同时进行两个请求,并把结果合并

Single.zip(
            api.listReposRx("xxx"),
            api.listReposRx("xxx"),
            { list1, list2 -> "${list1[0].name} - ${list2[0].name}" }
        ).observeOn(AndroidSchedulers.mainThread())
            .subscribe { result ->
                binding.tvText.text = result
            }
            
 //通过协程
 GlobalScope.launch { //launch 会创建一个协程,async也会创建一个协程出来,async是一个有结果的协程,通过await返回
            val one = async { api.listReposKt("xxx") }
            val two = async { api.listReposKt("xxx") }
            binding.tvText.text = "${one.await()[0].name}  ->  ${two.await()[0].name}"
        }
  • 协程泄漏 正常情况下网络请求图片,并对请求到的图片进行裁切等后续操作,但是如果刚请求网络图片,用户就把界面关了,协程还会把后续的裁切等一系列操作完,就造成了协程泄漏,洁面关闭的时候,协程内部的线程海在运转,就造成了泄露。
    • 解决方案:调用协程对象的cancel()
 var job = GlobalScope.launch { }
        job.cancel()
        
        CoroutineScope //管理多个协程的集中地,他一取消,他管理的协程都取消了
        lifecycleScope//lifecycleScope随Activity的生命周期走,不用手动取消,会自动在onDestory中取消
  • 内存泄露 例如:asyncTask 为什么会内存泄露 因为AsyncTask内部有一个活跃的线程,持有外部的Activity,垃圾回收会不定时出现,活跃的线程是被保护的,不会被垃圾回收器回收
  • 协程是怎么切线程的? 协程是通过handler来切换线程
  • 协程为什么可以【挂起】却不卡主线程? 协程底层用的handler.post()
  • 协程的delay()和Thread.sleep()