Kotlin协程基本用法

88 阅读5分钟

kotlin-协程基本操作_kotlin mainscope-CSDN博客

怎么理解kotlin的挂起函数? Kotlin协程进阶使用

Kotlin协程在工作中有用过吗? (qq.com)

一. 协程是什么

kotlin协程是一个线程框架,就是对线程的封装,提供了一种轻量级的并发处理方式,通过非阻塞挂起和恢复实现了用同步代码的方式编写异步代码。把原本运行在不同线程的代码写在一个代码块{}里面,看起来就像是同步代码。 协程的目的是,简化复杂的异步代码逻辑,用同步的代码编写方式实现复杂异步代码逻辑。

二.几种封装好的协程

1.viewModelScope启动协程(默认运行在主线程,生命周期和ViewModel绑定)
val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if (scope != null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
        }

image.png

2.lifecycleScope启动协程(默认运行在主线程,生命周期和Activity或者Fragment绑定,实现了LifeycleOwner接口)
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

image.png

3.MainScope()启动协程(默认运行在主线程,没有绑定Activity|Frament生命周期,需要手动在onDestory中通过job.cancel()取消协程.
    public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

image.png

4.GlobalScope启动协程(默认不是主线程,是一个静态单例,和应用的生命周期相同,一般不使用,容易导致内存泄漏)

    public object GlobalScope : CoroutineScope {
        override val coroutineContext: CoroutineContext
            get() = EmptyCoroutineContext
    }

image.png

5.CoroutineScope(Dispatchers.IO)构造函数启动协程 ,通过Dispatchers指定运行的线程。

image.png

注意:await() 只有在 async 未执行完成返回结果时,才会挂起协程。若 async 已经有结果了,await() 则直接获取其结果并赋值给变量,此时不会挂起协程。

三.协程的基本使用.

1.启动协程的几种方式.

(1).launch{} CoroutineScope接口的扩展方法,启动一个携程,不阻塞当前线程,返回一个Job对象,通过Job.cancel()可以取消协程,控制协程的生命周期.


    public fun CoroutineScope.launch(
         context:CoroutineScope=EmptyCoroutineContext,
         start:CoroutineStart = CoroutineStart.DEFAULT,
         block:suspend CoroutineScope.()->Unit
    ):Job {  //Job是返回值, {} 是launch的方法体实现 【不要搞混淆了】
     //launch方法体实现,返回Job对象
          val newcontenxt=newCoroutineContext(context)
         val coroutine=if(start.isLazy){
              LazyStandaloneCoroutine(newContext,block);//实现了job接口
         }else{
             StandaloneCoroutine(newContext,active=true)  //实现了job接口
        }
         coroutine.start(start,coroutine,block)  //启动job
        return  coroutine;//返回实现了Job接口的协程对象
    }

(2).async{} CoroutienScope接口的扩展方法,启动一个协程,不阻塞当前线程,返回一个Deferred类,可以通过await获取T对象.


    public fun <T> CoroutineScope.async(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> T
    ): Deferred<T> {
        val newContext = newCoroutineContext(context)
        val coroutine= if (start.isLazy){
            LazyDeferredCoroutine<T>(newContext, block)  //实现了Deferred接口
        } else{
           DeferredCoroutine<T>(newContext, active = true) //实现了Deferred接口
        }
        coroutine.start(start, coroutine, block)   //启动协程
        return coroutine //返回实现了Deferred接口的协程对象
    }
    

(3). runBlocking{} 全局方法,创建并启动协程,返回值是lambda表达式block的最后一行的返回值.(Unit或者其他具体类型)


    public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
        val currentThread = Thread.currentThread()
        val contextInterceptor = context[ContinuationInterceptor]
        val eventLoop: EventLoop?
        val newContext: CoroutineContext
        if (contextInterceptor == null) {
            eventLoop = ThreadLocalEventLoop.eventLoop
            newContext = GlobalScope.newCoroutineContext(context + eventLoop)
        } else {
            eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
                ?: ThreadLocalEventLoop.currentOrNull()
            newContext = GlobalScope.newCoroutineContext(context)
        }
        val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
        coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
        return coroutine.joinBlocking()
    }
    //这一段代码会阻塞主线程,直接导致黑屏。
    runBlocking {
         delay(10000)
     }
    //即使指定启动的协程运行在IO线程,也会阻塞主线程,导致黑屏
    runBlocking(Dispatchers.IO) {
       delay(10000L)
    }
    //依然会阻塞主线程导致黑屏
     GlobalScope.launch(Dispatchers.Main){
           runBlocking {
                    delay(10000)
            }

     }
    //不会阻塞主线程
     GlobalScope.launch(Dispatchers.IO){
           runBlocking {
                    delay(10000)
            }
     }

2.withContext()用于切换线程,并挂起协程(suspend,返回值lambda表达式最后一行的返回值,只能在suspend方法或者协程种调用).

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T
   
 lifecycleScope.launch(Dispatchers.IO) {
            delay(1000)
           //切换到主线程,此时启动协程的IO线程可以去执行其他任务.
            withContext(Dispatchers.Main){                   
                println("Test001:withContext:${Thread.currentThread().name}")
            }
        }

3.协程上下文,一般用调度器Dispatchers来切换线程,它是CoroutineContext接口的实现类.

  • Dispatchers.Main 协程代码执行在Android主线程

  • Dispatcher.IO IO线程池分配线程执行协程代码

  • Dispatchers.Unconfined 不限制协程代码执行在哪个线程【主线程或者其他空闲线程】

  • Dispatcher.Default JVM共享线程池分配线程执行协程代码

4.协程的挂起和阻塞

(1).协程都被挂起了,那么挂起函数的函数体由谁执行呢?

协程执行到挂起函数式,协程的执行会被暂停(即协程被挂起),但它并不会阻塞执行该挂起函数的线程,挂起函数的函数体任然由当前线程继续执行。

为了更清楚的解释这一点,我们可以使用一下步骤:

  • 遇到挂起函数:当协程遇到挂起函数时,协程的执行会被暂停。

  • 挂起函数执行:尽管协程被暂停了,挂起函数的函数体任然会在原始线程上执行。(除非该挂起函数明确指定了其他的执行上下文)

  • 挂起函数完成:一旦挂起函数完成其工作,它会通知协程库,然后协程会恢复执行。 注意:当协程被挂起时,主线程并没有被阻塞,而是可以执行其他的任务,等到挂起函数执行完毕,协程恢复时,又可以在主线程中继续执行。

(2).suspend表示挂起(非阻塞式挂起,不影响主线程的执行),例如下面的例子:

fun testCoroutineInActivity() {
       GlobalScope.launch(Dispatchers.Main) { //1.启动协程1
           println("Test001:执行在协程中...")
           GlobalScope.launch (Dispatchers.IO){//2.启动协程2
               println("Test001:异步执行result1")
               delay(1000)   
               println("Test001:result1:1234")
           }
           GlobalScope.launch(Dispatchers.IO) {//3.启动协程3
               println("Test001:异步执行result2.")
               delay(1000) 
               println("Test001:result2:123456")
           }
           println("Test001:执行完毕...")
       }
}

执行结果如下: image.png

注意:协程2和协程3中的delay函数只是挂起了协程2和协程3,不会影响协程1中的主线程的执行.
总结一下:在协程中使用挂起函数时,任何可能得的"阻塞"操作都会转移到其他线程上执行,这样启动协程的原始线程(例如主线程)就不会被实际阻塞,这样使得协程特别适合UI线程编程,因为它可以确保UI线程保持响应。

四.协程处理并发问题

1.协程就是一个线程框架,是对线程的封装,提供了一种轻量并发的处理方式.
    
      fun testCoroutineInActivity() {
          //1.launch启动协程,返回Job对象,通过job.cancel()取消任务
         val job:Job=CoroutineScope(Dispatchers.IO).launch() { 
                println("Test001:查看运行的线程1:"+Thread.currentThread().name)
                val start= System.currentTimeMillis()
                val result1=async {//运行在主线程,非异步
                    println("Test001:查看运行的线程2:"+Thread.currentThread().name)
                    delay(1000)
                    "123"
                }
             //2.async  启动协程,得到有返回值的Deferred对象
                val result2:Deferred<String> = async(Dispatchers.IO) { //异步 
                    println("Test001:查看运行的线程3:"+Thread.currentThread().name)
                    delay(2000)
                     "456"

                }
                val result3=result1.await()+result2.await();
                println("Test001:并发耗时:"+ (System.currentTimeMillis() - start)+"||result3:"+result3)
                withContext(Dispatchers.IO){
                    println("Test001:查看运行的线程4"+ Thread.currentThread().name)
                    delay(1000)
                }
                println("Test001:查看运行的线程5"+ Thread.currentThread().name)
            }
        }

2.通过非阻塞式挂起和恢复来实现用同步代码的方式编写异步代码

    // 注意:在真实开发过程中,MainScope作用域用的非常常用
    MainScope().launch(){     // 注意:此协程块默认是在UI线程中启动协程
        // 下面的代码看起来会以同步的方式一行行执行(异步代码同步获取结果)
        val token = apiService.getToken()   // 网络请求:IO线程,获取用户token
        val user = apiService.getUser(token)// 网络请求:IO线程,获取用户信息
        nameTv.text = user.name             // 更新 UI:主线程,展示用户名
        val articleList = apiService.getArticleList(user.id)// 网络请求:IO线程,根据用户id获取用户的文章集合哦
        articleTv.text = "用户${user.name}的文章页数是:${articleList.size}页"   // 更新 UI:主线程
    }
    
3.传统并发处理方式

如果一个页面需要同时并发请求多个接口,当所有的接口都请求完成需要做一些合并处理,然后更新UI,如何并发处理呢?

  • 方法一:为每个接口设置一个boolean值,每当接口请求成功,boolean值设置为true,当最后一个接口请求成功后,在更新UI,这样就达到了并发的目的 【管理多个boolean值,不够优雅,太累了】

  • 方法二:RXjava的zip操作符【达到发射一次,将结果合并处理的目的】


       fun testRxjavaZip(){
            println("-------------")
            val observable1: Observable<HttpResult<List<Banner>>> = HttpRetrofit.apiService.getBanners().subscribeOn(Schedulers.io()).observeOn(
                AndroidSchedulers.mainThread())
            val observable2:Observable<HttpResult<ArrayList<HomeData.DatasBean>>> =HttpRetrofit.apiService.getTopArticles().subscribeOn(Schedulers.io()).observeOn(
                AndroidSchedulers.mainThread())
            val observable3: Observable<HttpResult<List<KnowledgeData>>> = HttpRetrofit.apiService.getKnowledgeTree().subscribeOn(Schedulers.io()).observeOn(
                AndroidSchedulers.mainThread())
            var result1: HttpResult<List<Banner>>? =null
            var result2: HttpResult<ArrayList<HomeData.DatasBean>>? =null
            var result3: HttpResult<List<KnowledgeData>>? =null
            Observable.zip(observable1, observable2, observable3,
                object: Function3<
                        HttpResult<List<Banner>>,
                        HttpResult<ArrayList<HomeData.DatasBean>>,
                        HttpResult<List<KnowledgeData>>,
                        Boolean>{
                    override fun apply(t1: HttpResult<List<Banner>>, t2: HttpResult<ArrayList<HomeData.DatasBean>>, t3: HttpResult<List<KnowledgeData>>): Boolean {
                        result1=t1
                        result2=t2
                        result3=t3
                        return t1!=null&&t2!=null&&t3!=null
                    }
                }).subscribe(object:Observer<Boolean>{
                override fun onSubscribe(d: Disposable) {

                }

                override fun onError(e: Throwable) {

                }

                override fun onComplete() {

                }

                override fun onNext(t: Boolean) {
                    if(t){//对结果进行处理
                        println("成功获取结果");
                        println(Gson().toJson(result1))
                        println(Gson().toJson(result2))
                        println(Gson().toJson(result3))
                    }
                }
            });

        }