Kotlin协程

Posted 小图包

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin协程相关的知识,希望对你有一定的参考价值。

目录

一 使用协程

二 协程的阻塞与挂起​​​​​​​

三 CoroutineContext上下文

四 CoroutineScope协程作用域


一 使用协程

我们使用一个简单的协程:

   GlobalScope.launch 
            Log.d("lvhj","执行在协程中...")
            delay(2000)
            Log.d("lvhj","执行完毕...")
        

       Kotlin中,有几种方式能够启动协程,如下所示:

  • launch CoroutineScope的扩展方法,启动一个协程,不阻塞当前协程,并返回新协程的Job。

  • async CoroutineScope的扩展方法,启动一个协程,不阻塞当前协程,返回一个Deffer,除包装了未来的结果外,其余特性与launch一致

  • runBlocking 是一个裸方法,创建一个协程,并阻塞当前线程,直到协程执行完毕。

  • withContext() 一个suspend方法,在给定的上下文执行给定挂起块并返回结果,它并不启动协程,只会(可能会)导致线程的切换。用它执行的挂起块中的上下文是当前协程的上下文和由它执行的上下文的合并结果。 withContext的目的不在于启动子协程,它最初用于将长耗时操作从UI线程切走,完事再切回来。

  • coroutineScope 一个suspend方法,创建一个新的作用域,并在该作用域内执行指定代码块,它并不启动协程。其存在的目的是进行符合结构化并发的并行分解(即,将长耗时任务拆分为并发的多个短耗时任务,并等待所有并发任务完成后再返回)

      上面 前三种方法是创建或启动一个协程的,后面那种方式都是切换线程,或者创建作用域的一 个方法。

 举个例子查看使用

        runBlocking 
            coroutineScope      //正常使用,因为runBlocking创建了协程
            Log.d("lvhj","执行在协程中...")
            delay(1000L)
            Log.d("lvhj","执行完毕...")
        

源码查看

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

launch返回的是Job对象,用于控制协程的生命周期

异步的启动

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

async返回的是Deferred用于等待未来结果的返回。一般使用 await 来调用获取结果。

public suspend fun await(): T

切换线程withContext

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T 

内部需要传入一个协程上下文,我们一般使用调度器Dispatchers来切换线程,它是协程上下文CoroutineContext的实现类之一。

  • Dispatchers.Main android主线程
  • Dispatchers.Unconfined 当前CoroutineScope的线程策略
  • Dispatchers.Default 默认值,为JVM共享线程池
  • Dispatchers.IO IO线程池,默认为64个线程

  GlobalScope.launch
                Log.d("lvhj","执行在协程中...")
    
                delay(1000L)
    
                val deferred = async 
                    Log.d("lvhj","切换到另一个协程")
                    Thread.sleep(2000)
                    return@async "response data"
                
    
                val response = deferred.await()
                Log.d("lvhj","response:$response")
    
                val result = withContext(Dispatchers.IO) 
                    //同步执行
                    delay(1000L)
                    Log.d("lvhj","切换到另一个协程")
                    return@withContext "1234"
                
                Log.d("lvhj","result:$result")
                delay(1000L)
                Log.d("lvhj","执行完毕...") 

常用的两种方式:async是异步执行,withContext是同步执行。

当它们的代码块执行完毕,就会回到主协程的线程中,换句话说就是通过调度器实现切换线程,执行完就回到当前线程

二 协程的阻塞与挂起

阻塞会阻断当前线程后面的代码不会执行。挂起的全名应该叫非阻塞式挂起,其意思是为不会阻塞其他协程,只是当前自己所在协程会挂起等待不执行,但是其他协程还是能继续执行的

2.1 suspend非阻塞挂起函数

而挂起的方法调用都是需要 suspend 标记的,比如之前的delay

public suspend fun delay(timeMillis: Long) 
...

一个简单例子

GlobalScope.launch
            Log.d("lvhj","执行在协程中...")

            val result1 = withContext(Dispatchers.IO) 
                //异步执行
                Log.d("lvhj","执行result1")
                delay(500L)
                return@withContext "123478910"
            

            Log.d("lvhj","result1:$result1")

            val result2 = withContext(Dispatchers.IO) 
                //异步执行
                Log.d("lvhj","执行result2")
                delay(500L)
                return@withContext "123456"
            
            Log.d("lvhj","result2:$result2")

            Log.d("lvhj","执行完毕...")

        

看执行的log

2022-09-14 23:36:52.688 19629-19670/D/lvhj: 执行在协程中...
2022-09-14 23:36:52.691 19629-19670/D/lvhj: 执行result1
2022-09-14 23:36:53.196 19629-19672/D/lvhj: result1:123478910
2022-09-14 23:36:53.197 19629-19672/ D/lvhj: 执行result2
2022-09-14 23:36:53.698 19629-19671/ D/lvhj: result2:123456
2022-09-14 23:36:53.698 19629-19671/ D/lvhj: 执行完毕...

这是正常的,顺序执行的。为什么‘阻塞’了?不是说 delay 函数是挂起函数,是非阻塞的吗?OK,再次强调一点,此阻塞的概念并非是说阻塞这个线程,阻塞这段代码不让执行,此阻塞是针对其他 协程 的。上面的 withContext 它创建/启动了了协程吗?没有,它只是切换了线程,它本身其实也是 suspend 的函数而已。所以上面的代码是顺序执行的。

修改一下代码把代码启动协程:

GlobalScope.launch
                Log.d("lvhj","执行在协程中...")
    
                GlobalScope.launch(Dispatchers.IO) 
                    //异步执行
                    Log.d("lvhj","异步执行result1")
                    delay(1000L)
                    Log.d("lvhj","result1:1234")
                
    
                GlobalScope.launch(Dispatchers.IO) 
                    //异步执行
                    Log.d("lvhj","异步执行result2")
                    delay(1000L)
                    Log.d("lvhj","result2:123456")
                
    
                Log.d("lvhj","执行完毕...")
    
            

挂起函数是不会阻塞协程的,打印Log如下:   

2022-09-15 00:15:09.914 21705-21741/D/lvhj: 执行在协程中...
2022-09-15 00:15:09.915 21705-21742/D/lvhj: 异步执行result1
2022-09-15 00:15:09.915 21705-21741/D/lvhj: 执行完毕...
2022-09-15 00:15:09.916 21705-21743/ D/lvhj: 异步执行result2
2022-09-15 00:15:10.922 21705-21745/ D/lvhj: result2:123456
2022-09-15 00:15:10.922 21705-21742/ D/lvhj: result1:1234  

  而我们自定义的函数方法,也可以通过标记 suspend 而在协程中使用

GlobalScope.launch
            Log.d("lvhj","执行在协程中...")

            saveLocal()

            val result1 = withContext(Dispatchers.IO) 
                //异步执行
                Log.d("lvhj","异步执行result1")
                delay(1000L)

                saveLocal()

                return@withContext "1234"
            

            Log.d("lvhj","result1:$result1")



            Log.d("lvhj","执行完毕...")

        

        suspend fun saveLocal() 
            Log.d("lvhj","执行完毕...2")
        

使用我们定义的 saveLocal 挂起方法的时候,在哪个作用域使用就是在哪个线程执行,如上面的 saveLocal 方法是在主线程执行,withContext 中的 saveLocal 方法则是在子线程中使用。

2.2 runBlocking阻塞协程

之前我们讲到的是 suspend 挂起函数的阻塞与非阻塞的概念,而我们启动函数launch 和 runBlocking 也是区分阻塞与非阻塞的。

launch的是非阻塞的,runBlocking就是阻塞的,它会阻止其他协程的运行。launch的是非阻塞的,runBlocking就是阻塞的,它会阻止其他协程的运行,我们把 GlobalScope.launch 改为 runBlocking 试试

GlobalScope.launch
            Log.d("lvhj","执行在协程中...")

            runBlocking(Dispatchers.IO) 
                //异步执行
                Log.d("lvhj","异步执行result1")
                delay(1000L)
                Log.d("lvhj","result1:1234")
            

            runBlocking(Dispatchers.IO) 
                //异步执行
                Log.d("lvhj","异步执行result2")
                delay(1000L)
                Log.d("lvhj","result2:123456")
            

            Log.d("lvhj","执行完毕...")

        

2022-09-18 20:23:29.322 11473-11537 D/lvhj: 执行在协程中...
2022-09-18 20:23:29.324 11473-11538 D/lvhj: 异步执行result1
2022-09-18 20:23:30.331 11473-11538 D/lvhj: result1:1234
2022-09-18 20:23:30.333 11473-11538 D/lvhj: 异步执行result2
2022-09-18 20:23:31.336 11473-11538 D/lvhj: result2:123456
2022-09-18 20:23:31.336 11473-11537 D/lvhj: 执行完毕...

可以看到运行的结果 runBlocking 真的就阻止了其他协程的运行,得自己运行完成了,才能继续运行其他协程。这就是阻塞式的。

不管是不是一个作用域,不管是同级的兄弟协程还是父子协程,都是会被阻塞的。        

 CoroutineScope(Dispatchers.Main).launch 
            Log.d("lvhj","执行在协程中...")

            withContext(Dispatchers.IO) 
                //异步执行
                Log.d("lvhj","异步执行result1")
                delay(1000L)
                Log.d("lvhj","result1:1234")
            

            withContext(Dispatchers.IO) 
                //异步执行
                Log.d("lvhj","异步执行result2")
                delay(1000L)
                Log.d("lvhj","result2:123456")
            

            delay(1000L)

            Log.d("lvhj","执行完毕...")

        


        GlobalScope.launch(Dispatchers.Main) 
            Log.d("lvhj","执行在另一个协程中...")

            delay(1000L)

            Log.d("lvhj","另一个协程执行完毕...")
        
2022-09-18 20:30:25.939 12001-12001/ D/lvhj: 执行在协程中...
2022-09-18 20:30:25.946 12001-12001/ D/lvhj: 执行在另一个协程中...
2022-09-18 20:30:25.946 12001-12066/ D/lvhj: 异步执行result1
2022-09-18 20:30:26.951 12001-12001/ D/lvhj: 另一个协程执行完毕...
2022-09-18 20:30:26.952 12001-12066/ D/lvhj: result1:1234
2022-09-18 20:30:26.953 12001-12066/ D/lvhj: 异步执行result2
2022-09-18 20:30:27.958 12001-12066/ D/lvhj: result2:123456
2022-09-18 20:30:28.960 12001-12001/ D/lvhj: 执行完毕...

上述克制,2个父作用域的协程同时执行,一旦第一个协程内部没有被阻塞,那么下面的协程就能执行了,此时2个协程就是并发的

上面的协程阻塞住,那么下面的协程就需要等待阻塞的代码执行完毕才能执行,此时两个协程就是非并发的串联顺序执行的

CoroutineScope(Dispatchers.Main).launch 
            Log.d("lvhj","执行在协程中...")

            runBlocking(Dispatchers.IO) 
                //异步执行
                Log.d("lvhj","异步执行result1")
                delay(1000L)
                Log.d("lvhj","result1:1234")
            

            runBlocking(Dispatchers.IO) 
                //异步执行
                Log.d("lvhj","异步执行result2")
                delay(1000L)
                Log.d("lvhj","result2:123456")
            

            delay(1000L)

            Log.d("lvhj","执行完毕...")

        

非并发的打印结果如

2022-09-18 20:39:46.918 12472-12472/ D/lvhj: 执行在协程中...
2022-09-18 20:39:46.925 12472-12516/ D/lvhj: 异步执行result1
2022-09-18 20:39:47.935 12472-12516/ D/lvhj: result1:1234
2022-09-18 20:39:47.937 12472-12516/ D/lvhj: 异步执行result2
2022-09-18 20:39:48.939 12472-12516/ D/lvhj: result2:123456
2022-09-18 20:39:48.945 12472-12472/ D/lvhj: 执行在另一个协程中...
2022-09-18 20:39:49.942 12472-12472/ D/lvhj: 执行完毕...
2022-09-18 20:39:49.945 12472-12472/ D/lvhj: 另一个协程执行完毕...

三 CoroutineContext上下文

而 CoroutineContext 协程上下文则是可以控制协程,调度协程,捕获异常等。

协程总是运行在 CoroutineContext 类型的上下文环境中的。

前文中我们得知,启动/创建一个协程有三种方法

launch async runBlocking 我们查看他们的源码可以看到第一个参数都是需要传入一个协程上下文,当然默认的都是EmptyCoroutineContext。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job 
public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> 
public fun <T> runBlocking(context: CoroutineContext
 = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T 

下面就讲解几个常用的上下文实现类看看如何使用

3.1、调度器 Dispatchers

CoroutineScope(Dispatchers.Main).launch 

            GlobalScope.launch(Dispatchers.Default) 
                Log.d("lvhj","执行在另一个协程中...")

                delay(1000L)

                Log.d("lvhj","另一个协程执行完毕...")
            

            val deferred = async(Dispatchers.IO) 
                Log.d("lvhj","切换到另一个协程")
                Thread.sleep(2000)
                return@async "response data"
            

            val response = deferred.await()
            Log.d("lvhj","response:$response")

            runBlocking(Dispatchers.IO) 
                //异步执行
                Log.d("lvhj","异步执行result1")
                delay(1000L)
                Log.d("lvhj","result1:1234")
            

            withContext(Dispatchers.IO) 
                //异步执行
                Log.d("lvhj","异步执行result2")
                delay(1000L)
                Log.d("lvhj","result2:123456")
            
        
2022-09-18 20:58:47.538 13319-13386/ D/lvhj: 执行在另一个协程中...
2022-09-18 20:58:47.539 13319-13387/ D/lvhj: 切换到另一个协程
2022-09-18 20:58:48.545 13319-13386/ D/lvhj: 另一个协程执行完毕...
2022-09-18 20:58:49.542 13319-13319/ D/lvhj: response:response data
2022-09-18 20:58:49.545 13319-13387/ D/lvhj: 异步执行result1
2022-09-18 20:58:50.550 13319-13387/ D/lvhj: result1:1234
2022-09-18 20:58:50.553 13319-13387/ D/lvhj: 异步执行result2
2022-09-18 20:58:51.556 13319-13386/ D/lvhj: result2:123456

可以看到不管是指定协程的运行线程,还是临时切换线程,运行完毕会切换回来,都是通过 Dispatchers 来调度的。

常用的线程调度为:

  • Dispatchers.Main Android主线程
  • Dispatchers.Unconfined 当前CoroutineScope的线程策略
  • Dispatchers.Default 默认值,为JVM共享线程池
  • Dispatchers.IO IO线程池,默认为64个线程

当然除了系统自带的调度器 Dispatchers ,我们还能通过一些扩展方法实现自己的调度器,让协程运行在我们指定的线程上。

比如我们想要运行在 HandleThread 线程,或者 Executors 线程池中的线程,我们可以这样做:

 private var mHandlerThread: HandlerThread? = HandlerThread("handle_thread")

        private var mHandler: Handler? = mHandlerThread?.run 
            start()
            Handler(this.looper)
        

      

        GlobalScope.launch(mHandler.asCoroutineDispatcher("handle_thread")) 

            Log.d("lvhj","执行在协程中...")

            delay(1000L)

            Log.d("lvhj","执行完毕...")
        

这样就可以运行在指定的HandleThread线程中。

同样的我们可以创建一个线程池,比如我创建一个单线程池,那么把它转换为一个协程上下文的调度Dispatcher对象,那么这个协程就会运行在我们指定的线程池中的线程上。

   GlobalScope.launch(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) 
    
                Log.d("lvhj","执行在协程中...")
    
                delay(1000L)
    
                Log.d("lvhj","执行完毕...")
 

3.2、CoroutineName 协程命名

如果一个协程中有多个子协程,如果都张的一样,我想知道谁是谁,我们就可以通过 CoroutineName 来命名,它也是 CoroutineContext 的实现类,使用起来非常的简单,直接构造即可:


        GlobalScope.launch(CoroutineName("parent")) 

            async(CoroutineName("child1")) 
                Log.d("lvhj","切换到另一个协程1")
                Thread.sleep(2000)
                return@async "response data"
            .await()

            async(CoroutineName("child2")) 
                Log.d("lvhj","切换到另一个协程2")
                Thread.sleep(2000)
                return@async "response data"
            .await()

        

3.3、Job 协程控制

Job 不是我们协程启动返回的对象吗?用于cancel join 等操作。它虽然是协程启动的返回值,但是同样也是 CoroutineContext 的实现类,一样可以用于启动协程的构造中​​​​​​​

var job: Job? = null

        ...

        job = GlobalScope.launch(Dispatchers.Main) 

            Log.d("lvhj","执行在协程中...")

            delay(1000L)

            Log.d("lvhj","执行完毕...")
        


        override fun onDestroy() 
            job?.cancel()
            super.onDestroy()
        

 launch 的时候返回一个Job,然后通过成员变量在页面退出的时候取消掉这个协程的运行

使用构造方法,我们就可以这么操作了:


        var job: Job? = null

        ...

        GlobalScope.launch(job.run  Job() ) 

            Log.d("lvhj","执行在协程中...")

            delay(1000L)

            Log.d("lvhj","执行完毕...")
        


        override fun onDestroy() 
            job?.cancel()
            super.onDestroy()
        

3.4、CoroutineExceptionHandler 异常处理

看如下代码

GlobalScope.launch() 

            Log.d("lvhj","执行在协程中...")

            delay(1000L)

            val num = 999/0

            Log.d("lvhj","执行完毕...")
        

这样都会报错的,如果你没有处理,那么协程可不会自动帮你处理,你的App就会崩溃。我们可以try catch处理即可:

 GlobalScope.launch() 

            Log.d("lvhj","执行在协程中...")

            delay(1000L)

            try 

                val num = 999/0

             catch (e: Exception) 
                e.printStackTrace()
            

            Log.d("lvhj","执行完毕...")
        

其实我们传入一个处理异常的上下文对象,一样能实现这个效果,就无需在协程中到处try catch了。

                                                                                                            
private val exceptionHandler = CoroutineExceptionHandler  coroutineContext, throwable ->                   
    Log.d("lvhj",throwable.message ?: "Unkown Error")                                                       
                                                                                                           
                                                                                                            
GlobalScope.launch(exceptionHandler)                                                                        
                                                                                                            
    Log.d("lvhj","执行在协程中...")                                                                               
                                                                                                            
    delay(1000L)                                                                                            
                                                                                                            
    val num = 999/0                                                                                         
                                                                                                            
    Log.d("lvhj","执行完毕...")                                                                                 
                                                                                                           

这样我们就可以在外部统一的处理异常信息了。

基于这样的处理,我们还能做一个简单的扩展方法,让协程安全的启动,定义的扩展方法如下:

fun CoroutineScope.safeLaunch(
    onError: ((Throwable) -> Unit)? = null,
    onLaunchBlock: () -> Unit
) 
    val exceptionHandler = CoroutineExceptionHandler  _, throwable ->
        onError?.invoke(throwable)
    

    this.launch(exceptionHandler) 
        onLaunchBlock.invoke()
    

我们就能安全的启动协程啦

四 CoroutineScope协程作用域

4.1、协程的生命周期

当我们创建一个协程的时候,会返回一个Job对象,不管是通过返回值管理,还是通过 launch 的构造方法的形式管理,其实是一样的。

我们通过Job就可以获取当前协程的运行状态,还可以随时取消协程。

协程的状态查询

  • isActive
  • isCompleted
  • isCancelled

常用的协程操作:

  • cancel 用于Job的取消,取消协程
  • start 用于启动一个协程,让其到达Active状态
  • invokeOnCompletion 添加一个监听,当工作完成或者异常时会调用
  • join 阻塞并等候当前协程完成

协程不是默认创建就启动了吗? 怎么还有一个 start 方法 。

其实协程默认是启动的,但是我们可以创建一个懒加载的协程,手动start才开启协程。

 val job = GlobalScope.launch(start = CoroutineStart.LAZY) 
    
                Log.d("lvhj","执行在协程中...")
    
                delay(1000L)
    
                Log.d("lvhj","执行完毕...")
            
            
            job.start()

协程的取消,可以手动的调用 calcel 或者在onDestory的时候调用 calcel:


        var job: Job = Job()

        ...

        GlobalScope.launch(job) 

            Log.d("lvhj","执行在协程中...")

            delay(1000L)

            Log.d("lvhj","执行完毕...")
        


        override fun onDestroy() 
            job.cancel()
            super.onDestroy()
        

协程执行完的回调 invokeOnCompletion 也是我们常用的监听,在正常执行完毕,或者异常执行完毕都会回调这个方法。

    
       val exceptionHandler = CoroutineExceptionHandler  coroutineContext, throwable ->
           Log.d("lvhj",throwable.message ?: "Unkown Error")
        

        val job = GlobalScope.launch(Dispatchers.Main + exceptionHandler) 
            Log.d("lvhj","执行在另一个协程中...")

            delay(1000L)

            val num = 9/0

            Log.d("lvhj","另一个协程执行完毕...")
        

        job.invokeOnCompletion 
            Log.d("lvhj","完成或异常的回调")
        

4.2 常用的协程作用域

之前我们都是通过 GlobalScope 来启动一个协程的,其实这样使用在 Android 开发中并不好。因为是全局的作用域。

在 Android 开发过程中,我们需要理解一些协程代码运行的范围。而所有的Scope 如GlobalScope 都是 CoroutineScope 的子类,我们的协程创建都需要这样一个 CoroutineScope 来启动。

同时我们还有其他的一些作用范围的 CoroutineScope 对象。

  • GlobeScope:全局范围,不会自动结束执行。
  • MainScope:主线程的作用域,全局范围
  • lifecycleScope:生命周期范围,用于activity等有生命周期的组件,在DESTROYED的时候会自动结束。
  • viewModelScope:viewModel范围,用于ViewModel中,在ViewModel被回收时会自动结束

使用 coroutineScope 构建器声明自己的作用域。它会创建一个协程作用域并且在所有已启动子协程执行完毕之前不会结束。runBlocking 与 coroutineScope 的主要区别在于后者在等待所有子协程执行完毕时不会阻塞当前线程。

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R =
    suspendCoroutineUninterceptedOrReturn  uCont ->
        val coroutine = ScopeCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    

函数被 suspend 修饰,是一个挂起函数,前面我们说了挂起函数是不会阻塞线程的,它只会挂起协程,而不阻塞线程。

4.3 为什么要用 coroutineScope

是为了把一些比较耗时的多个任务拆分为不同的小任务,指定了一个作用域,在此作用域下面如果取消,就整个作用域都取消,如果异常则整个作用域内的协程都取消 .举例说明一下,比如一个 coroutineScope 下面有多个任务


            suspend fun showSomeData() = coroutineScope 
    
                val data1 = async 
                    delay(2000)
                    100
                
                val data2 = async 
                    delay(3000)
                    20
                
    
                val num = withContext(Dispatchers.IO) 
                    delay(3000)
                    val random = Random(10)
                    data1.await() + data2.await() + random.nextInt(100)
                
    
                Log.d("lvhj","num:"+num)
            

ata1 和 data2 并发,等它们完成之后获取到 num 打印出来。data1 data2 失败,那么 withContext 就不会执行了,如果 random 异常,那么整个协程作用域内的任务都会取消。

4.4 MainScope 和 GlobalScope

都是全局的作用域,但是他们有区别。如果不做处理他们都是运行在全局无法取消的,但是GlobalScope是无法取消的,MainScope是可以取消的

GlobalScope 的源码如下:

public object GlobalScope : CoroutineScope 
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext

可以看到它的上下文对象是 EmptyCoroutineContext 对象,并没有Job对象,所以我们无法通过 Job 对象去cancel 此协程。所以他是无法取消的进程级别的协程。

MianScope 的源码如下:

public fun MainScope(): CoroutineScope = 
ContextScope(SupervisorJob() + Dispatchers.Main)

可以看到它的上下文对象是 SupervisorJob + 主线程构成的。它是一个可以取消的全局主线程协程。

按照上面的代码,我们就能这么举例说明:

var mainScope= MainScope()

mainScope.launch 
            Log.d("lvhj","执行在一个协程中...")

            val result = saveSth2Local 
                async(Dispatchers.IO) 
                    "123456"
                .await()
            

           Log.d("lvhj","一个协程执行完毕... result:$result")
        

override fun onDestroy() 
    super.onDestroy()
     mainScope.cancel()

4.5 viewModelScope

iewModelScope 只能在ViewModel中使用,绑定ViewModel的生命周期

源码如下:

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"

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))
        

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope 
    override val coroutineContext: CoroutineContext = context

    override fun close() 
        coroutineContext.cancel()
    

可以看到viewModelScope使用 SupervisorJob 而不是用 Job。 为了 ViewModel 能够取消协程,需要实现 Closeable 接口 viewModelScope 默认使用 Dispatchers.Main, 方便 Activity 和 Fragment 更新 UI

private final Map<String, Object> mBagOfTags = new HashMap<>();

<T> T getTag(String key) 
    synchronized (mBagOfTags) 
        return (T) mBagOfTags.get(key);
    


<T> T setTagIfAbsent(String key, T newValue) 
    T previous;
    synchronized (mBagOfTags) 
        previous = (T) mBagOfTags.get(key);
        if (previous == null) 
            mBagOfTags.put(key, newValue);
        
    
    T result = previous == null ? newValue : previous;
    if (mCleared) 
        closeWithRuntimeException(result);
    
    return result;


@MainThread
final void clear() 
    mCleared = true;
    if (mBagOfTags != null) 
        for (Object value : mBagOfTags.values()) 
            closeWithRuntimeException(value);
        
    
    onCleared();


private static void closeWithRuntimeException(Object obj) 
    if (obj instanceof Closeable) 
        try 
            ((Closeable) obj).close();
         catch (IOException e) 
            throw new RuntimeException(e);
        
    

类中通过 HashMap 存储 CoroutineScope 对象,取消的时候, 在 clear() 方法中遍历调用 closeWithRuntimeException 取消了viewModelScope 的协程。

4.6 lifecycleScope

​​​​​​​lifecycleScope只能在Activity、Fragment中使用,会绑定Activity和Fragment的生命周期,

它的基本使用和 viewModelScope 是一样的。但是它多了生命周期的的一些感知。

fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch 
    lifecycle.whenResumed(block)

看下LifecycleController 的源码:

@MainThread
internal class LifecycleController(
    private val lifecycle: Lifecycle,
    private val minState: Lifecycle.State,
    private val dispatchQueue: DispatchQueue,
    parentJob: Job
) 
    private val observer = LifecycleEventObserver  source, _ ->
        if (source.lifecycle.currentState == Lifecycle.State.DESTROYED) 
            // cancel job before resuming remaining coroutines so that they run in cancelled
            // state
            handleDestroy(parentJob)
         else if (source.lifecycle.currentState < minState) 
            dispatchQueue.pause()
         else 
            dispatchQueue.resume()
        
    

    init 
        // If Lifecycle is already destroyed (e.g. developer leaked the lifecycle), we won't get
        // an event callback so we need to check for it before registering
        // see: b/128749497 for details.
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) 
            handleDestroy(parentJob)
         else 
            lifecycle.addObserver(observer)
        
    
    //...

在init初始化的时候,添加LifecycleEventObserver监听,对生命周期进行了判断,当大于当前状态的时候,也就是生命周期执行到当前状态的时候,会调用dispatchQueue.resume()执行队列,也就是协程开始执行。

以上是关于Kotlin协程的主要内容,如果未能解决你的问题,请参考以下文章

深入理解Kotlin协程协程的分类与线程的区别

浅析kotlin协程launch delay调用

浅析kotlin协程launch delay调用

Kotlin 协程真的比 Java 线程更高效吗?

深潜Koltin协程:Job 和等待子协程

Golang的Context介绍及其源码分析