Android中的Coroutine协程原理详解
前言
协程是一个并发方案。也是一种思想。
传统意义上的协程是单线程的,面对io密集型任务他的内存消耗更少,进而效率高。但是面对计算密集型的任务不如多线程并行运算效率高。
不同的语言对于协程都有不同的实现,甚至同一种语言对于不同平台的操作系统都有对应的实现。
我们kotlin语言的协程是 coroutines for jvm的实现方式。底层原理也是利用java 线程。
基础知识
生态架构
相关依赖库
dependencies {
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
// 协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
// 协程Android支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
// 协程Java8支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.3"
// lifecycle对于协程的扩展封装
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
}
为什么一些人总觉得协程晦涩难懂?
1.网络上没有详细的关于协程的概念定义,每种语言、每个系统对其实现都不一样。可谓是众说纷纭,什么内核态用户态巴拉巴拉,很容易给我们带偏
2.kotlin的各种语法糖对我们造成的干扰。如:
- 高阶函数
- 源码实现类找不到
所以扎实的kotlin语法基本功是学习协程的前提。
实在看不懂得地方就反编译为java,以java最终翻译为准。
协程是什么?有什么用?
kotlin中的协程干的事就是把异步回调代码拍扁了,捋直了,让异步回调代码同步化。除此之外,没有任何特别之处。
创建一个协程,就是编译器背后偷偷生成一系列代码,比如说状态机。
通过挂起和恢复让状态机状态流转实现把层层嵌套的回调代码变成像同步代码那样直观、简洁。
它不是什么线程框架,也不是什么高深的内核态,用户态。它其实对于咱们安卓来说,就是一个关于回调函数的语法糖。。。
本文将会围绕挂起与恢复彻底剖析协程的实现原理
Kotlin函数基础知识复习
再Kotlin中函数是一等公民,有自己的类型
函数类型
fun foo(){}
//类型为 () -> Unit
fun foo(p: Int){}
//类型为 (Int) -> String
class Foo{
fun bar(p0: String,p1: Long):Any{}
}
//那么 bar 的类型为:Foo.(String,Long) -> Any
//Foo就是bar的 receiver。也可以写成 (Foo,String,Long) ->Any
函数引用
fun foo(){}
//引用是 ::foo
fun foo(p0: Int): String
//引用也是 ::foo
咋都一样?没办法,就这样规定的。使用的时候 只能靠编译器推断
val f: () -> Unit = ::foo //编译器会推断出是fun foo(){}
val g: (Int) -> String = ::foo //推断为fun foo(p0: Int): String
带Receiver的写法
class Foo{
fun bar(p0: String,p1: Long):Any{}
}
val h: (Foo,String,Long) -> Any = Foo:bar
绑定receiver的函数引用:
val foo: Foo = Foo()
val m: (String,Long) -> Any = foo:bar
额外知识点
val x: (Foo,String,Long) -> Any = Foo:bar
val y: Function3<Foo,String,Long,Any> = x
Foo.(String,Long) -> Any = (Foo,String,Long) ->Any = Function3<Foo,String,Long,Any>
函数作为参数传递
fun yy(p: (Foo,String,Long)->Any){
p(Foo(),"Hello",3L)//直接p()就能调用
//p.invoke(Foo(),"Hello",3L) 也可以用invoke形式
}
Lambda
就是匿名函数,它跟普通函数比是没有名字的,听起来好像是废话
//普通函数
fun func(){
println("hello");
}
//去掉函数名 func,就成了匿名函数
fun(){
println("hello");
}
//可以赋值给一个变量
val func = fun(){
println("hello");
}
//匿名函数的类型
val func :()->Unit = fun(){
println("hello");
}
//Lambda表达式
val func={
print("Hello");
}
//Lambda类型
val func :()->String = {
print("Hello");
"Hello" //如果是Lambda中,最后一行被当作返回值,能省掉return。普通函数则不行
}
//带参数Lambda
val f1: (Int)->Unit = {p:Int ->
print(p);
}
//可进一步简化为
val f1 = {p:Int ->
print(p);
}
//当只有一个参数的时候,还可以写成
val f1: (Int)->Unit = {
print(it);
}
关于函数的个人经验总结
函数跟匿名函数看起来没啥区别,但是反编译为java后还是能看出点差异
如果只是用普通的函数,那么他跟普通java 函数没啥区别。
比如 fun a()
就是对应java方法public void a(){}
但是如果通过函数引用(:: a)来用这个函数,那么他并不是直接调用fun a()
而是重新生成一个Function0
挂起函数
suspend 修饰。
挂起函数中能调用任何函数。
非挂起函数只能调用非挂起函数。
换句话说,suspend函数只能在suspend函数中调用。
简单的挂起函数展示:
//com.example.studycoroutine.chapter.CoroutineRun.kt
suspend fun suspendFun(): Int {
return 1;
}
挂起函数特殊在哪?
public static final Object suspendFun(Continuation completion) {
return Boxing.boxInt(1);
}
这下理解suspend为啥只能在suspend里面调用了吧?
想要让道貌岸然的suspend函数干活必须要先满足它!!!就是给它里面塞入一颗球。
然后他想调用其他的suspend函数,只需将球继续塞到其它的suspend方法里面。
普通函数里没这玩意啊,所以压根没法调用suspend函数。。。
读到这里,想必各位会有一些疑问:
question1.这不是鸡生蛋生鸡的问题么?第一颗球是哪来的?
question2.为啥编译后返回值也变了?
question3.suspendFun 如果在协程体内被调用,那么他的球(completion)是谁?
标准库给我们提供的最原始工具
public fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>) {
createCoroutineUnintercepted(completion).intercepted().resume(Unit)
}
public fun <T> (suspend () -> T).createCoroutine(completion: Continuation<T>): Continuation<Unit> =
SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)
以一个最简单的方式启动一个协程。
Demo-K1
fun main() {
val b = suspend {
val a = hello2()
a
}
b.createCoroutine(MyCompletionContinuation()).resume(Unit)
}
suspend fun hello2() = suspendCoroutine<Int> {
thread{
Thread.sleep(1000)
it.resume(10086)
}
}
class MyContinuation() : Continuation<Int> {
override val context: CoroutineContext = CoroutineName("Co-01")
override fun resumeWith(result: Result<Int>) {
log("MyContinuation resumeWith 结果 = ${result.getOrNull()}")
}
}
两个创建协程函数区别
startCoroutine 没有返回值 ,而createCoroutine返回一个Continuation,不难看出是SafeContinuation
好像看起来主要的区别就是startCoroutine直接调用resume(Unit),所以不用包装成SafeContinuation,而createCoroutine则返回一个SafeContinuation,因为不知道将会在何时何处调用resume,必须保证resume只调用一次,所以包装为safeContinuation
SafeContinuationd的作用是为了确保只有发生异步调用时才挂起
分析createCoroutineUnintercepted
//kotlin.coroutines.intrinsics.CoroutinesIntrinsicsH.kt
@SinceKotlin("1.3")
public expect fun <T> (suspend () -> T).createCoroutineUnintercepted(completion: Continuation<T>): Continuation<Unit>
先说结论
其实可以简单的理解为kotlin层面的原语,就是返回一个协程体。
开始分析
引用代码Demo-K1首先b 是一个匿名函数,他肯定要被编译为一个FunctionX,同时它还被suspend修饰 所以它肯定跟普通匿名函数编译后不一样。
编译后的源码为
public static final void main() {
Function1 var0 = (Function1)(new Function1((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
Object var10000;
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
this.label = 1;
var10000 = TestSampleKt.hello2(this);
if (var10000 == var3) {
return var3;
}
break;
case 1:
ResultKt.throwOnFailure($result);
var10000 = $result;
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
int a = ((Number)var10000).intValue();
return Boxing.boxInt(a);
}
@NotNull
public final Continuation create(@NotNull Continuation completion) {
Intrinsics.checkParameterIsNotNull(completion, "completion");
Function1 var2 = new <anonymous constructor>(completion);
return var2;
}
public final Object invoke(Object var1) {
return((<undefinedtype>)this.create((Continuation)var1)).invokeSuspend(Unit.INSTANCE);
}
});
boolean var1 = false;
Continuation var7 = ContinuationKt.createCoroutine(var0, (Continuation)(newMyContinuation()));
Unit var8 = Unit.INSTANCE;
boolean var2 = false;
Companion var3 = Result.Companion;
boolean var5 = false;
Object var6 = Result.constructor-impl(var8);
var7.resumeWith(var6);
}
我们可以看到先是 Function1 var0 = new Function1
创建了一个对象,此时跟协程没关系,这步只是编译器层面的匿名函数语法优化
如果直接
fun main() {
suspend {
val a = hello2()
a
}.createCoroutine(MyContinuation()).resume(Unit)
}
也是一样会创建Function1 var0 = new Function1
解答question1
继续调用createCoroutine
再继续createCoroutineUnintercepted ,找到在JVM平台的实现
//kotlin.coroutines.intrinsics.IntrinsicsJVM.class
@SinceKotlin("1.3")
public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
completion: Continuation<T>
): Continuation<Unit> {
//probeCompletion还是我们传入completion对象,在我们的Demo就是myCoroutine
val probeCompletion = probeCoroutineCreated(completion)//probeCoroutineCreated方法点进去看了,好像是debug用的.我的理解是这样的
//This就是这个suspend lambda。在Demo中就是myCoroutineFun
return if (this is BaseContinuationImpl)
create(probeCompletion)
else
//else分支在我们demo中不会走到
//当 [createCoroutineUnintercepted] 遇到不继承 BaseContinuationImpl 的挂起 lambda 时,将使用此函数。
createCoroutineFromSuspendFunction(probeCompletion) {
(this as Function1<Continuation<T>, Any?>).invoke(it)
}
}
@NotNull
public final Continuation create(@NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function1 var2 = new <anonymous constructor>(completion);
return var2;
}
把completion传入,并创建一个新的Function1,作为Continuation返回,这就是创建出来的协程体对象,协程的工作核心就是它内部的状态机,invokeSuspend函数
调用 create
@NotNull
public final Continuation create(@NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function1 var2 = new <anonymous constructor>(completion);
return var2;
}
把completion传入,并创建一个新的Function1,作为Continuation返回,这就是创建出来的协程体对象,协程的工作核心就是它内部的状态机,invokeSuspend函数
补充---相关类继承关系
解答question2&3
已知协程启动会调用协程体的resume,该调用最终会来到BaseContinuationImpl::resumeWith
internal abstract class BaseContinuationImpl{
fun resumeWith(result: Result<Any?>) {
// This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume
var current = this
var param = result
while (true) {
with(current) {
val completion = completion!! // fail fast when trying to resume continuation without completion
val outcome: Result<Any?> =
try {
val outcome = invokeSuspend(param)//调用状态机
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted() // this state machine instance is terminating
if (completion is BaseContinuationImpl) {
// unrolling recursion via loop
current = completion
param = outcome
} else {
//最终走到这里,这个completion就是被塞的第一颗球。
completion.resumeWith(outcome)
return
}
}
}
}
}
状态机代码截取
public final Object invokeSuspend(@NotNull Object $result) {
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
Object var10000;
switch(this.label) {
case 0://第一次进来 label = 0
ResultKt.throwOnFailure($result);
// label改成1了,意味着下一次被恢复的时候会走case 1,这就是所谓的【状态流转】
this.label = 1;
//全体目光向我看齐,我宣布个事:this is 协程体对象。
var10000 = TestSampleKt.hello2(this);
if (var10000 == var3) {
return var3;
}
break;
case 1:
ResultKt.throwOnFailure($result);
var10000 = $result;
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
int a = ((Number)var10000).intValue();
return Boxing.boxInt(a);
}
question3答案出来了,传进去的是create创建的那个continuation
最后再来聊聊question2,从上面的代码已经很清楚的告诉我们为啥挂起函数反编译后的返回值变为object了。
以hello2为例子,hello2能返回代表挂起的白板,也能返回result。如果返回白板,状态机return,协程挂起。如果返回result,那么hello2执行完毕,是一个没有挂起的挂起函数,通常编译器也会提醒 suspend 修饰词无意义。所以这就是设计需要,没有啥因为所以。
最后,除了直接返回结果的情况,挂起函数一定会以resume结尾,要么返回result,要么返回异常。代表这个挂起函数返回了。
调用resume意义在于重新回调BaseContinuationImpl的resumeWith,进而唤醒状态机,继续执行协程体的代码。
换句话说,我们自定义的suspend函数,一定要利用suspendCoroutine 获得续体,即状态机对象,否则无法实现真正的挂起与resume。
suspendCoroutine
我们可以不用suspendCoroutine,用更直接的suspendCoroutineUninterceptedOrReturn也能实现,不过这种方式要手动返回白板。不过一定要小心,要在合理的情况下返回或者不返回,不然会产生很多意想不到的结果
suspend fun mySuspendOne() = suspendCoroutineUninterceptedOrReturn<String> { continuation ->
thread {
TimeUnit.SECONDS.sleep(1)
continuation.resume("hello world")
}
//因为我们这个函数没有返回正确结果,所以必须返回一个挂起标识,否则BaseContinuationImpl会认为完成了任务。
// 并且我们的线程又在运行没有取消,这将很多意想不到的结果
kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
}
而suspendCoroutine则没有这个隐患
suspend fun mySafeSuspendOne() = suspendCoroutine<String> { continuation ->
thread {
TimeUnit.SECONDS.sleep(1)
continuation.resume("hello world")
}
//suspendCoroutine函数很聪明的帮我们判断返回结果如果不是想要的对象,自动返
kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
}
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T =
suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
//封装一个代理Continuation对象
val safe = SafeContinuation(c.intercepted())
block(safe)
//根据block返回结果判断要不要返回COROUTINE_SUSPENDED
safe.getOrThrow()
}
SafeContinuation的奥秘
//调用单参数的这个构造方法
internal actual constructor(delegate: Continuation<T>) : this(delegate, UNDECIDED)
@Volatile
private var result: Any? = initialResult //UNDECIDED赋值给 result
//java原子属性更新器那一套东西
private companion object {
@Suppress("UNCHECKED_CAST")
@JvmStatic
private val RESULT = AtomicReferenceFieldUpdater.newUpdater<SafeContinuation<*>, Any?>(
SafeContinuation::class.java, Any::class.java as Class<Any?>, "result"
)
}
internal actual fun getOrThrow(): Any? {
var result = this.result // atomic read
if (result === UNDECIDED) { //如果UNDECIDED,那么就把result设置为COROUTINE_SUSPENDED
if (RESULT.compareAndSet(this, UNDECIDED, COROUTINE_SUSPENDED)) returnCOROUTINE_SUSPENDED
result = this.result // reread volatile var
}
return when {
result === RESUMED -> COROUTINE_SUSPENDED // already called continuation, indicate COROUTINE_SUSPENDED upstream
result is Result.Failure -> throw result.exception
else -> result // either COROUTINE_SUSPENDED or data <-这里返回白板
}
}
public actual override fun resumeWith(result: Result<T>) {
while (true) { // lock-free loop
val cur = this.result // atomic read。不理解这里的官方注释为啥叫做原子读。我觉得 Volatile只能保证可见性。
when {
//这里如果是UNDECIDED 就把 结果附上去。
cur === UNDECIDED -> if (RESULT.compareAndSet(this, UNDECIDED, result.value)) return
//如果是挂起状态,就通过resumeWith回调状态机
cur === COROUTINE_SUSPENDED -> if (RESULT.compareAndSet(this, COROUTINE_SUSPENDED, RESUMED)){
delegate.resumeWith(result)
return
}
else -> throw IllegalStateException("Already resumed")
}
}
}
val safe = SafeContinuation(c.intercepted())
block(safe)
safe.getOrThrow()
先回顾一下什么叫真正的挂起,就是getOrThrow返回了“白板”,那么什么时候getOrThrow能返回白板?答案就是result被初始化后值没被修改过。那么也就是说resumeWith没有被执行过,即:block(safe)这句代码,block这个被传进来的函数,执行过程中没有调用safe的resumeWith。原理就是这么简单,cas代码保证关键逻辑的原子性与并发安全
继续以Demo-K1为例子,这里假设hello2运行在一条新的子线程,否则仍然是没有挂起。
{
thread{
Thread.sleep(1000)
it.resume(10086)
}
}
总结
最后,可以说开启一个协程,就是利用编译器生成一个状态机对象,帮我们把回调代码拍扁,成为同步代码。
Android中的Coroutine协程原理详解的更多相关文章
- Android 中各种权限深入体验及详解
Android 中各种权限深入体验及详解 分类: Android2012-07-15 19:27 2822人阅读 评论(0) 收藏 举报 androidpermissionsinstallersyst ...
- Python中Paramiko协程方式详解
什么是协程 协程我们可以看做是一种用户空间的线程. 操作系统对齐存在一无所知,需要用户自己去调度. 比如说进程,线程操作系统都是知道它们存在的.协程的话是用户空间的线程,操作系统是不知道的. 为什么要 ...
- Lua的协程和协程库详解
我们首先介绍一下什么是协程.然后详细介绍一下coroutine库,然后介绍一下协程的简单用法,最后介绍一下协程的复杂用法. 一.协程是什么? (1)线程 首先复习一下多线程.我们都知道线程——Thre ...
- PHP协程入门详解
概念 咱们知道多进程和多线程是实现并发的有效方式.但多进程的上下文切换资源开销太大:多线程开销相比要小很多,也是现在主流的做法,但其的控制权在内核,从而使用户(程序员)失去了对代码的控制,而且线程的上 ...
- Android中Serializable和Parcelable序列化对象详解
学习内容: 1.序列化的目的 2.Android中序列化的两种方式 3.Parcelable与Serializable的性能比较 4.Android中如何使用Parcelable进行序列化操作 5.P ...
- Android中xml设置Animation动画效果详解
在 Android 中, Animation 动画效果的实现可以通过两种方式进行实现,一种是 tweened animation 渐变动画,另一种是 frame by frame animation ...
- Android中attrs.xml文件的使用详解
$*********************************************************************************************$ 博主推荐 ...
- android中常见的命名及其特点详解
Paseal命名法 Paseal命名法特点:String MyName-DelphiInt MyAge每个单词首字母大写 Camel命名法 Camel(驼峰的意思)命名法特点:String myNam ...
- Android中的ImageView的scaleType属性详解
ImageView的Scaletype决定了图片在View上显示时的样子,如进行何种比例的缩放,及显示图片的整体还是部分,等等. 设置的方式包括: 1. 在layout xml中定义android:s ...
随机推荐
- elasticsearch查询之三种fetch id方式性能测试
一.使用场景介绍 elasticsearch除了普通的全文检索之外,在很多的业务场景中都有使用,各个业务模块根据自己业务特色设置查询条件,通过elasticsearch执行并返回所有命中的记录的id: ...
- CVE-2021-1732 LPE漏洞分析
概述 CVE-2021-1732是一个发生在windows内核win32kfull模块的LPE漏洞,并且由于创建窗口时调用win32kfull!xxxCreateWindowEx过程中会进行用户模式回 ...
- 深入MySQL(二):MySQL的数据类型
前言 对于MySQL中的数据类型的选择,不同的数据类型看起来可能是相同的效果,但是其实很多时候天差地别. 本章从MySQL中的常用类型出发,结合类型选择的常见错误,贯彻MySQL的常用类型选择. 常用 ...
- XSS Challenge靶场练习
实验目的 学习xss的基础知识及利用方式. 实验原理 XSS 跨站脚本攻击(Cross Site Scripting),为不和层叠样式表(Cascading Style Sheets, CSS)的缩写 ...
- (一)scrapy 安装及新建爬虫项目并运行
> 参考:https://www.cnblogs.com/hy123456/p/9847570.html 在 pycharm 中并没有创建 scrapy 工程的选项,需要手动创建. 这里就有两种 ...
- MySQL常用查询命令(连接查询&子查询)
多张表联合起来查询即为连接查询,可分为: 内连接:等值连接.非等值连接.自连接 外连接:右外连接.左外连接 也就是先把多张表通过某种指定条件用join...on...语法连接起来,然后再进行where ...
- Python 小数据池和代码块缓存机制
前言 本文除"总结"外,其余均为认识过程:3.7.5: 总结: 如果在同一代码块下,则采用同一代码块下的缓存机制: 如果是不同代码块,则采用小数据池的驻留机制: 需要注意的是,交互 ...
- 如何通过xstart远程连接桌面
转至:https://www.cnblogs.com/LiuChang-blog/p/12324193.html 1.1.安装依赖包: (1)安装语言包: [root@slave-node2 ~]# ...
- 控制台console不打印信息的解决办法
一直用控制台调试,突然不知道怎么回事看不到控制台输出的信息了: 需要检查下面几方面的问题: 1:我的就属于第一个问题,不知道怎么搜索的时候就改变了Filter; 2:确保以上选项是勾选的 3:点击设置 ...
- think php 验证码
1.下载 composer require topthink/think-captcha 1.* // composer 下载 //过程 D:\PHP\phpstudy_pro\WWW\1906A\p ...