【Kotlin实操】1.Kotlin协程原理与上手体验

目录

    • 1.异步?我们为什么使用异步
    • 2.RxJava不香吗?为什么是协程?
    • 3.协程怎么用?
    • 4.协程是个啥?
    • 5.Kotlin协程的原理
    • 6.总结
    • 参考文章

1.异步?我们为什么使用异步

同步(Sync)

所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回,这样就不能继续执行后续操作。
【Kotlin实操】1.Kotlin协程原理与上手体验_第1张图片

异步(Async)

所谓同步,就是调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,通过状态、通知和回调来通知调用者。

【Kotlin实操】1.Kotlin协程原理与上手体验_第2张图片
为什么使用异步——提升效率
异步一般都能够提升效率 这也是我们使用异步的主要原因

异步带来的问题——回调嵌套
异步调用有依赖关系时 每增加一个调用 回调的嵌套层级也相应增加

比如 3个接口调用串行,回调嵌套保证了执行顺序:
请求接口1成功–>请求接口2成功–>请求接口3成功–>更新UI

//请求 接口1
api1 (
    cllback(
        //请求接口2
        api2(
            callback(
                //请求接口3
                api3(
                    callback(
                        //更新UI 
                    )
                )
            )
        )  
    )
})

再比如3个接口并行,回调逻辑保证了执行顺序
请求结果需要汇总后更新UI

//并行 汇总
  api1(
  callback(
       if(api2已完成 && api3已完成){
           updateUI()
       }
    )
  )
  api2(callback(
      if(api1已完成 && api3已完成){
          updateUI()
      }
  	)
  )
  
  api3(callback(
      if(api1已完成 && api2已完成){
          updateUI()
      }
    )
  )
  
if(api2已完成 && api3已完成)
if(api1已完成 && api3已完成)
if(api1已完成 && api2已完成)

嗯 这~ 谁用谁知道

2.RxJava不香吗?为什么是协程?

那怎么解决异步调用的回调嵌套呢
说到异步 大名鼎鼎的RxJava是怎么做的
RxJava的链式调用

//1.串行 flatmap
api1()
	.flatMap {  
     	api2()
   	}.flatMap {  
    	api3()
   	}.subscribe {  
    	updateUI()
   	}

//2.串行 cocat
Observable.concat(api1(), api2(), api3())
		  .subscribe{
			updateUI()
		  }
		  
//3.并行 merge
Observable.merge(api1(), api2(), api3())
		  .subscribe{
			updateUI()
		  }
		  
//4.并行 zip
Observable.zip(api1(), api2(),BiFunction.apply{
		   	value1 + value2
		  })
		  .subscribe{
			updateUI()
		  }
		  

嗯 RxJava的链式调用能在调用逻辑复杂时 依然保持调用形式上的简单
那么问题来了 既然有RxJava 还要什么Kotlin协程?(要啥自行车)
Another way!

3.协程怎么用?

协程实现串行3个接口访问 launch{} 按前后顺序书写即可

//串行 
launch{
	value1 = api1()
	vale2 = api2(vlaue1) 
	value3 = api3(value2)
	
	updateUI(value3)
} 

suspend fun api1():Value1{...}
suspend fun api2():Value2{...}
suspend fun api3():Value3{...}

fun updateUI(){...}

接下来看一下并行数据汇总 async{} await()

launch{
	value1 = async{ api1() }
	value2 = async{ api2() }
	value3 = async{ api3() }
	//这里会等待value1 value2 value3都请求完成
	updateUI(value1.await(), value2.await(), value3.await())
}

suspend fun api1():Value1{...}
suspend fun api2():Value2{...}
suspend fun api3():Value3{...}

fun updateUI(){...}

4.协程是个啥?

什么是进程、线程、协程?

  • 协程是一种编程概念 就像进程、线程一样。 Go、Python等编程语言都有协程的概念 。

  • 进程是应用程序的启动实例,进程拥有代码和打开的文件资源、数据资源、独立的内存空间。进程的调度者是系统。

  • 线程从属于进程,是程序的实际执行者,一个进程至少包含一个主线程,也可以有更多的子线程,线程拥有自己的栈空间。线程的调度者是系统。

  • 协程是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。协程的调度者是用户。

进程 线程 协程的关系图:
【Kotlin实操】1.Kotlin协程原理与上手体验_第3张图片

Kotlin中的协程“

Google Android 上的 Kotlin 协程
协程是在版本 1.3 中添加到 Kotlin 的,它基于来自其他语言的既定概念。
协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码 (用同步的方式编写异步代码 真香)。
协程是我们在 Android 上进行异步编程的推荐解决方案。

  • 轻量:
    单个线程上运行多个协程,因为协程支持暂停,不会使正在运行协程的线程阻塞。暂停比阻塞节省内存,且支持多个并行操作。

  • 内存泄露更少:
    使用结构化并发机制在一个作用域内执行多个操作。

  • 内置取消支持:
    取消功能会自动通过正在运行的协程层次结构传播。

  • Jetpack 集成:
    许多 Jetpack 库都包含提供全面协程支持的扩展。

关于Kotlin的协程 这里引出几个问题

  • 问题1.协程暂停是怎么实现的?
  • 问题2.协程暂停后如何恢复?
  • 问题3.线程切换怎么实现的?
  • 问题4.协程是非阻塞式的 更轻量 更高效吗?

5.Kotlin协程的原理

1.Continuation
从方法调用方式说起
【Kotlin实操】1.Kotlin协程原理与上手体验_第4张图片
Java内存分区图 这里只关注 虚拟机栈

Java虚拟机栈是线程私有的,他的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:
每个方法在执行的同时都会创建一个栈帧(frame)用于存储局部变量、操作数、操作数栈、动态链接、方法出口(return)等信息。
每一个方法的调用过程直至执行完成的过成,就对应着一个栈帧(frame)在虚拟机栈中入栈到出栈的过程。

可见Java方法是通过Java方法栈实现调用。

Continuation是另一种函数调用方式。

上下文信息保存在continuation record中 组成树/图的结构。
调用函数就等于给当前节点生成一个子节点,然后把系统寄存器移动到这个子节点。函数的退出等于从当前节点退回到父节点。
节点的删除是由gc来管理

这样的调用方式和堆栈方式相比的好处在哪里呢?

好处就是,它可以让你从任意一个节点跳到另一个节点。而不必遵循堆栈方式的一层一层的return方式。

比如说,在当前的函数内,你只要有一个其它函数的节点信息,完全可以选择return到那个函数,而不是返回到自己的调用者。
你也可以在一个函数的任何位置储存自己的上下文信息,然后,在以后某个适当的时刻,从其它的任何一个函数里面返回到自己现在的位置。(重点)

Continuation思想的一个重要应用场景就是Continuation-passing style(CPS) 续体风格

2.CPS变换

CPS的基本思想是:当需要返回某些数据的时候,不是直接把它当作函数的返回值,而是接受一个叫做continuation的参数。这个参数就是一个call-back函数, 它接受这个数据,并做需要做的事情。

//普通函数
fun foo(x)
{
   return x;
}

//CPS风格函数
fun foo(x, continuation)
{
   continuation(x);
 
   //注意:调用continuation(x)相当于“return”,之后的语句都不会被执行了。
   assert(false, "should not be here");
}

CPS的使用特点:

  • CPS函数都有一个额外的参数 continuation(续体) ,表示控制流。函数需要返回,必须显式的调用 continuation
  • CPS所有函数都是尾调用(在函数的末尾调用了另外一个函数,这种调用称为尾调用,tail call)
  • CPS代码基本没法阅读和理解,最初主要用于编译高级语言时一种中间代码表示,有了这种中间代码,编译器的复杂度大大降低
  • CPS在异步编程里,分布式编程里大显身手(重点)

so,我知道了这些 有什么用?客官请注意 异步编程 (点题了啊),在说CPS在异步编程的应用之前 我们先来了解下 续体拦截器

我们知道

  • CPS函数调用的时候,必须传进来一个Continuation;
  • Continuation调用可以在一个函数的任何位置储存自己的上下文信息,然后,在以后某个适当的时刻,从其它的任何一个函数里面返回到自己现在的位置;

如果需要在跨线程场景下发起Continuation调用 需要指定Continuation运行的线程 这个要用到续体拦截器(ContinuationInterceptor)

  • ContinuationInterceptor(续体拦截器),续体拦截器负责拦截协程在恢复后应执行的代码(即续体)并将其在指定线程或线程池恢复。

3.suspend 关键字
suspend 挂起
用于修饰函数 被suspend修饰的函数称之为挂起函数
挂起函数只能在另一个挂起函数或者协程中被调用
挂起函数不仅支持call和return 还“支持” suspend和resume

Kotlin 中被 suspend 修饰符修饰的函数在编译期间会被编译器做特殊处理。而这个特殊处理的第一道工序就是:CPS(续体传递风格)变换,它会改变挂起函数的函数签名。

举个栗子:

挂起函数 getIpAddress() 的函数签名如下所示:

suspend fun getIpAddress(ip: String): String {
    return withContext(Dispatchers.IO) {
        println("getIpAddress ${Thread.currentThread().name}")
        val json = URL(IPAddress.IP.replace("IP", ip)).readText()
        val resultType = object : TypeToken<BaseBean<IPBean>>() {}.type
        val bean = Gson().fromJson<BaseBean<IPBean>>(json, resultType)
        val city = bean.result.City
        println("city-->$city")
        city
    }
}

在编译期发生 CPS 变换之后:

  
public final Object getIpAddress(String paramString, Continuation paramContinuation) {
  CoroutineContext coroutineContext = (CoroutineContext)Dispatchers.getIO();
  ApiDataCoroutine$getIpAddress$2 apiDataCoroutine$getIpAddress$2 = new ApiDataCoroutine$getIpAddress$2();
  this(paramString, null);
  apiDataCoroutine$getIpAddress$2 = apiDataCoroutine$getIpAddress$2;
  return BuildersKt.withContext(coroutineContext, apiDataCoroutine$getIpAddress$2, paramContinuation);
}

Kotlin中的Continuation声明类型为接口

interface Continuation<in T> {
   //协程上下文
   val context: CoroutineContext
   //续体恢复时调用
   fun resumeWith(result: Result<T>)
}

续体Continuation,它包装了协程在挂起之后应该继续执行的代码
协程可以在挂起函数的地方挂起(Continuation续体暂停),挂起结束后恢复(Continuation 续体恢复)

4.状态机
举个栗子
接口串行

  • 根据Ip获取所属地址city 成功后更新UI
  • 拿到city后查询天气预报 成功后更新UI
 GlobalScope.launch(Dispatchers.Main) {
 		 println("协程Start ${Thread.currentThread().name}")
         val city = getIpAddress(ip) //IO线程获取IP所属地
         updateCityUI(cityTv, city)//主线程更新UI

         val weather = getWeather(city)//IO线程获取IP所属地的天气 需要参数city
         updateWeatherUI(weatherTv, weather)//主线程更新UI
         println("协程End ${Thread.currentThread().name}")
        }

 suspend fun getIpAddress(ip: String): String {
     return withContext(Dispatchers.IO) {
             val json = URL(IPAddress.IP.replace("IP", ip)).readText()
             ......
             val city = bean.result.City
             city
         }
     }
     
 suspend fun getWeather(city: String): String {
     return withContext(Dispatchers.IO) {
         Thread.sleep(2_000)
         val json = URL(IPAddress.WEATHER.replace("CITY", city.substring(0, 2))).readText()
         ......
         val weather = bean.result.future[0].weather
         weather
     }
 }

以上代码反编译后

 
    public final Object invokeSuspend(Object paramObject) {
        Object object1 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
        int i = this.label;
        Object object2 = "Thread.currentThread()";
        byte b1 = 4;
        byte b2 = 3;
        byte b3 = 2;
        byte b4 = 1;
        switch (this.label) {
            case 0: {
                ResultKt.throwOnFailure(paramObject);
                CoroutineScope coroutineScope = this.p$;
                StringBuilder stringBuilder2 = new StringBuilder();
                this();
                stringBuilder2.append("");
                Thread thread = Thread.currentThread();
                Intrinsics.checkExpressionValueIsNotNull(thread, (String) object2);
                String str3 = thread.getName();
                stringBuilder2.append(str3);
                String str2 = stringBuilder2.toString();
                System.out.println(str2);
                ApiDataCoroutine apiDataCoroutine2 = ApiDataCoroutine.INSTANCE;
                str3 = this.$ip;
                this.L$0 = coroutineScope;
                this.label = b4;
                Object object4 = apiDataCoroutine2.getIpAddress(str3, (Continuation) this);
                if (object4 == object1)
                    return object1;
                object4 = object4;
                StringBuilder stringBuilder1 = new StringBuilder();
                this();
                stringBuilder1.append("city-->");
                stringBuilder1.append((String) object4);
                String str1 = stringBuilder1.toString();
                System.out.println(str1);
                ApiDataCoroutine apiDataCoroutine1 = ApiDataCoroutine.INSTANCE;
                TextView textView = this.$cityTv;
                this.L$0 = coroutineScope;
                this.L$1 = object4;
                this.label = b3;
                Object object3 = apiDataCoroutine1.updateCityUI(textView, (String) object4, (Continuation) this);
                break;
            }
            case 1: {
                i = 0;
                Object object5 = this.L$0;
                Object object3 = object5;
                object3 = object5;
                ResultKt.throwOnFailure(paramObject);
                object5 = paramObject;
                object5 = object5;
                StringBuilder stringBuilder = new StringBuilder();
                this();
                stringBuilder.append("city-->");
                stringBuilder.append((String) object5);
                String str = stringBuilder.toString();
                System.out.println(str);
                ApiDataCoroutine apiDataCoroutine = ApiDataCoroutine.INSTANCE;
                TextView textView = this.$cityTv;
                this.L$0 = object3;
                this.L$1 = object5;
                this.label = b3;
                Object object4 = apiDataCoroutine.updateCityUI(textView, (String) object5, (Continuation) this);
                break;
            }
            case 2: {
                i = 0;
                b3 = 0;
                Object object6 = this.L$1;
                Object object4 = object6;
                object4 = object6;
                object6 = this.L$0;
                Object object3 = object6;
                object3 = object6;
                ResultKt.throwOnFailure(paramObject);
                object6 = ApiDataCoroutine.INSTANCE;
                this.L$0 = object3;
                this.L$1 = object4;
                this.label = b2;
                Object object5 = object6.getWeather((String) object4, (Continuation) this);
                break;
            }
            case 3: {
                i = 0;
                b2 = 0;
                Object object4 = this.L$1;
                Object object5 = object4;
                object5 = object4;
                object4 = this.L$0;
                Object object3 = object4;
                object3 = object4;
                ResultKt.throwOnFailure(paramObject);
                object4 = object5;
                object5 = paramObject;
                object5 = object5;
                StringBuilder stringBuilder = new StringBuilder();
                this();
                stringBuilder.append("");
                thread = Thread.currentThread();
                Intrinsics.checkExpressionValueIsNotNull(thread, (String) object2);
                object2 = thread.getName();
                stringBuilder.append((String) object2);
                object2 = stringBuilder.toString();
                System.out.println(object2);
                object2 = ApiDataCoroutine.INSTANCE;
                TextView textView = this.$weatherTv;
                this.L$0 = object3;
                this.L$1 = object4;
                this.L$2 = object5;
                this.label = b1;
                object2 = object2.updateWeatherUI(textView, (String) object5, (Continuation) this);
                break;
            }
            case 4:
                i = 0;
                Object object3 = this.L$2;
                Object object4 = object3;
                object4 = object3;
                object3 = this.L$1;
                object2 = object3;
                object2 = object3;
                object3 = this.L$0;
                object1 = object3;
                object1 = object3;
                ResultKt.throwOnFailure(paramObject);
                break;
            default:
                object1 = new IllegalStateException();
                super("call to 'resume' before 'invoke' with coroutine");
                throw object1;
                break;
        }
        return Unit.INSTANCE;
    }

6.总结

Kotlin的协程就是Kotlin提供的一套线程框架 ,本质上和Handler,AsyncTask,RxJava 是一致的。只不过协程封装的更友好,使用起来更方便。
协程可以用同步的方式写异步代码。
协程并没有比线程更高效、更轻量级。

参考文章

怎样理解同步异步和阻塞非阻塞
同步(Synchronous)和异步(Asynchronous)
忘了RxJava吧——你需要的是拥抱Kotlin协程
Kotlin 协程真的比 Java 线程更高效吗
Kotlin Coroutines Offical Doc

你可能感兴趣的:(Android,kotlin)