AI智能
改变未来

饿了么资深Android工程师带你领略Kotlin协程的力量

协程是什么

进程

早期的计算机运行程序还是只能一次运行一个任务,之后进程的出现实现了近似同步的执行效果,其本质上是程序的交替执行。为了保证进程中的程序能够正常执行,还会有一些存储进程状态的保存集。随着硬件的发展和多CPU的出现,能够同时执行的进程数量逐渐增多。这就带来了一个问题,即用来存储进程状态的集合所占用的资源比一个进程可以执行的资源还要多,相当于整个系统大半的进行都是用来保存进程的状态。

线程

线程的提出有效的解决了这个问题。进程不再频繁的切换,而是先执行,遇到阻塞的话暂时不管,继续执行其他的任务,当其他任务执行完之后再回过头来看阻塞任务是否执行完。多线程的缺陷在于无法自主控制调度,除开一定会执行的主线程之外,其他线程的执行顺序都无法控制,在Java上是由Java虚拟机调度,其他平台大多是由系统控制。

协程

线程执行过程中发生线程切换的时候会损耗一定的资源,这部分资源用来保存线程的状态。执行过程中如果发生了磁盘读写或网络请求这样的IO操作的时候线程的执行会被阻塞,但同时该线程还会持有CPU资源,这就造成了一定了资源浪费。理想的情况是在发送阻塞的时候,该线程主动交出CPU给其他线程使用或者给内部的其他任务。

这种方式其实就是协程的体系。通过提升CPU利用率,减少线程切换,进而提升程序运行效率。

延伸开来协程主要有三个特性。第一个是可控制,不同于线程协程能做到可被控制的发起子任务;第二个是轻量级,协程非常小、占用资源比线程还少,在JVM平台上它的本质就是一次方法的调用;第三个是语法糖,目前能够使用协程的语言都提供了很好的语法糖支持,使多任务或多线程切换不在使用回调语法。

通过Kotlin在JVM平台使用协程

示例:第三方登录

第三登录在应用开发中可以算是一个很常见的场景,具体的逻辑是这样的,首先向第三方平台请求用户token,然后将token和自身平台上的用户账号关联起来,最后获取用户信息展示到UI界面上。

对此最常见的做法是采用回调的形式。requestToken会先发出一次网络请求,请求返回后执行回调并传入token,回调内部又会用token作为参数向我们自己的服务器发起请求获得到用户信息,最终完成用户信息在UI上的改变。这种方式的问题在于嵌套层级过多,链路一旦过长看起来会非常复杂。

协程改写

要改变这种现状,自然就要用到协程,上图是用协程对前面示例的改写。在Kotlin中如果函数的函数体内只有一个语句,那么就可以将这条语句直接赋值给函数声明。另外如果方法只有一个参数且该参数为lambda表达式的时候,可以将函数后小括号省略掉。

在Kotlin中常用的启动协程的方式有三种。第一种是上图中的runBlocking,它只会用在协程和线程的交接点,也就是通常只用于启动最外层协程。第二种是launch,用于在协程内部再启动一个协程。第三种是async/await,它不仅可以启动协程,还可以得到执行的结果。

这是前面示例中细分的两个函数调用。因为前两个方式都是耗时操作,所以要放在子线程中运行。但是在安卓中子线程无法做UI改变的操作,因此改变UI的时候还是要切换到主线程。setText方法的launch中有一个UI参数,这是Kotlin的协程提供的对象,表示在UI线程中启动协程,同时协程被中断以后的恢复也是在UI线程中。

在requestToken函数内部中的return@async标识用来表示返回的是async这个闭包的内部逻辑。Async的CommonPool参数是Kotlin提供的线程池的单例对象,有了这个参数后就可以从线程池中随机的取一个子线程,然后运行闭包中的逻辑。当网络请求操作执行完之后,await函数会将请求结果直接返回给requestToken。

协程的本质

一般直接将一个耗时方法写入在代码中其实是有问题的,轻则会UI卡顿,严重的话还会造成程序无响应。因此Kotlin协程库提供了一个关键字suspend,表示挂起指出该方法是一个协程方法不是直接运行在UI线程中。Suspend修饰的函数(或lambda)只能被suspend修饰的函数(或lambda)调用。

图中被suspend修饰的requestToken函数在被编译之后会变成下方这种形式。这种语法其实在Java中经常会用到,它返回一个Object,不过多另一个Continuation类型的泛型参数,表示协程方法requestToken最终会返回一个string的值。

Continuation是协程在代码上的映射,它本质上是个接口,Kotlin中每个协程的协程体都实现了这个接口。仔细看下该接口内部的代码就会发现这就是一个回调接口。协程的本质也就是一次回调,只不过通过语法糖的形式让它看起来像是顺序执行。

协程切换

上图中每个大括号所包含的范围是协程的执行过程。发生协程切换的时候会有一个suspend Point点,通常被称作协程的挂起点,比如从协程0到协程1发生一次协程切换的时候,协程0会被挂起,协程1得以执行。

前面第三方登录的协程方法在被编译完之后会在代码中出现上图所示的方法doResume。它有一个any参数,该参数类似于Java中Object。Kotlin中所有类都会有一个直接或间接的父类指向any,这里的any其实就是协程对象。

当前类继承自CoroutineImpl,CoroutineImpl是Continuation的实现类。方法内部的this指向doResume传入的参数,第一个协程启动的时候this.label默认为0,requestToken方法被调用同时label值被改为1,requestToken执行完之后会通过传入的this参数继续调用doResume方法。这时的label值已经变为了1,所以会执行协程的第二段操作,通过这样的一系列执行就完成了整个协程的切换。

方案:SPP+PHP

Kotlin提供了一个协程扩展库,可以直接返回Call类型的对象。这个对象的扩展形式就是图中展示的,可以看到它被静态的添加了个await扩展方法,这个await就是一个协程方法和之前所提到的await并无差别。

上图的代码中当网络请求被执行完之后会得到一个Call对象,通过调用它的await方法就能够获取到请求的返回值。

这是扩展方法的具体实现,整个函数只有一个函数体,内部启动了一个协程。Enqueue表示将请求加入到请求队列中,请求成功后会通过异步回调拿到执行结果。这里回调的时候又进一步调用了协程接口continuation的resume方法和resumeWithException方法。拿到这两个回调方法之后,编译器在编译的时候会直接在对应的位置触发接下来的代码。

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » 饿了么资深Android工程师带你领略Kotlin协程的力量