这篇文章是我个人对协程的理解,在探索的过程当中写下的体会,这篇博客会一直有改动(对协程的理解更加深入,排版比较丑,后期博客更新完会调整排版),有不对的地方或者不清晰的地方希望大佬们指出,我会进行解释和优化本篇文章。
协程
一,协程作用域Scope
属性:
协程作用域里面就一个变量context协程的上下文,协程上下文是一组附加到协程中的对象,它是各种不同元素的"集合",可以包括协程执行的主元素Job、负责协程线程策略的元素CoroutineDispatcher、协程标识和名称CoroutineName、处理未被捕捉的异常CoroutineExceptionHandler等等。如果把协程看作一个轻量线程,协程上下文就像是线程的一组局部变量。用GlobeScope创建出来的协程上下文其实是EmptyCoruntingContext,里面什么也没有,所以在创建的时候编译器会自动给我们加些默认的东西。
1.当创建子协程的时候会传递父协程的上下文给子协程,所以子协程的上下文继承了父协程的上下文,那么子协程的上下文就是协程对象中的上下文 = 初始上下文(作用域的上下文or父协程上下文) + 构建器参数上下文 + 续体拦截器(调度器) + Job(协程对象本身) ,协程体的this就是Scope
根据图里面可以看到协程里面的代码其实是Scope的扩展函数,所以协程体里面调用this就是Scope代表的被创建的协程对象也就是方便为了在协程体重调用lauch或者async创建携程体的时候通过this获取父协程的上下文从而形成父子关系。但是如果是调用挂起函数启动新的协程比如withContext并不是通过this获取的父协程上下文,而是通过调用挂起函数必须传一个Continuation参数,所以他是通过获取这个参数然后拿到父协程上下文的
方法:
除了这个变量Scope,他还提供了几个方法,这个Scope作用域代表的我可以取消可以获取你的协程执行状态等等,所以他其实是通过变量里面的context获取Job进行判断的。
二,协程的挂起和恢复
当我们用基础设施层创建协程(createContinuntion)的时候我们是通过一个挂起函数和传入的一个Continuntion回调来实现的,然后调用createContinuntion返回值的resume执行即可执行挂起函数,那么他是怎么实现的呢?
编译器会给挂起函数创建一个匿名内部类,这个匿名类继承自SuspendCorunting重写了他的invokeSuspend方法这个里面就是函数体,通过api返回的continution的resume函数会调用resumeWith,而这个会执行invokeSuspend函数,接着拿到返回值看是不是调用的是挂起函数如果是那么开启一个线程执行函数体接着返回,等到线程执行完函数体时候调用resumewith回复执行里面又会调用invokeSuspend,就这么往复直到协程执行完成,返回异常或者值;如果里面没有挂起函数那么invokeSuspend就会一直走直到整个协程体执行完然后返回,后者情况就是同步。
我们在调用lauch启动一个协程的时候,编译器只会给我们创建一个匿名内部类,并不会为每一个挂起函数都创建一个匿名内部类耗性能,但是他是怎么做到一个匿名内部类管理所有的挂起函数的呢,答案就是状态机。协程会检测挂起函数有几个然后设置几个状态,在调用resumeWith的时候会调用invokeSuspend执行协程体,这个时候会检查协程体里面接下来的函数的返回值:如果调用到挂起函数的时候,会开线程(不一定需要根据协程的上下文获取调度器决定)执行并且状态+1接着返回一个挂起标志位,invoke中检测到这个标志的时候就会返回,然后就回到了你的resumewith中(因为是在这个方法里面调用的invokeSuspend),而resume中又会检查这个invokeSuspend的返回值如果是挂起标志位那么就会返回,这个时候lauch就被return了自然就会执行协程体外面的代码,当线程里面挂起函数执行完后又会回调协程的resumeWith来继续执行invokeSuspend,然后检查invoke的标志位,invoke里面这个时候就会根据上次设置的状态决定该执行哪个协程之后的代码,然后如果在碰到挂起在返回,resume中检测到在返回,之后线程里面执行完了在调用resume,哈哈就这样一直该状态一直检测返回值看是不是挂起,直到协程体执行完。
大致流程:
(resumewith-》 invokeSuspend)—》【检查状态机该执行哪个挂起函数之后的代码】调用完函数后检查返回值【修改状态机,挂起函数返回标志(线程执行挂起函数之后调用resume回到开头起始点)】—》返回到resume中-----》检查返回值如果是挂起状态return-------》主线程
线程执行完了----》获取corunting执行resume—》resumeWith回到上面的代码
三,我是谁?【协程】
我怎么知道我的上下文信息,我该运行在哪个线程或者我该怎么知道我的名字和我的启动模式【协程说】
答案:通过上下文!!!(之前理解是拦截器)
拦截器也属于上下文的一种,上下文看源码会发现他其实是一个集合存储的一系列的Element元素就是他的元素,而不同Element用来区分的时候是通过Key进行区分的,比如协程的返回Job对象,调度器Dispatcher,拦截器等等,协程在执行的时候会获取当前上下文进行通过他内定的Key来取这些值。默认顶层协程构建的时候是不会进行设置参数的,编译器会为我们设置默认的上下文信息。
拦截器在链表中会始终保持在头部,以保证拦截器的resumewith最先执行。使用头插法,left表示为下一个Element
拦截器会在协程刚开启的时候回调一次,接着在调用resumeWith的时候也会回调一次(挂起函数回复),所以为n+1次【n是挂起函数有几个在协程里面】
开启协程的时候会进行调用intercepted(),
查看是否需要切换线程,本质是通过获取当前looper和调度器中的looper是否一样来判断的,如果需要切换线程,使用当前调度器的handler发送一个post完成线程切换
当挂起函数执行完后,Job会调用之前保存的continuatinon的afterCompletion(),恢复之前的resume执行,ucont为挂起函数参数中传来的Continuation,因为是调用之前的拦截所以上下文也是之前的,也就完成了第二次线程切换 恢复。
前面说过协程执行的时候会有一个协程作用域,作用域里面包含的你的上下文信息,如果是顶层协程那么编译器会默认给你加默认的上下文【调用器为Defult或者我的启动模式】,通过拦截器我可以拿到当前我的上下文信息,从而知道协程他的信息。
上下文也可以保存数据,通过指定的key父协程设置数据,子协程可以拿到(两种方式上面讲过),子协程在启动的时候会拿到父协程的Job和父协程的上下文信息,接着进行plus操作,因为子协程可以拿到我父协程的job也就可以检测到我父协程的状态(比如取消)这个时候我子协程的协程体执行的时候就会检测,如果父协程取消那么我抛异常,也就是父子协程具有传递性!!