写点什么

破解 Kotlin 协程 (1) - 入门篇,35 岁以后的 Android 程序员出路在哪里

用户头像
Android架构
关注
发布于: 3 小时前

try {showUser(gitHubServiceApi.getUser("bennyhuo").await())} catch (e: Exception) {showError(e)}}


说明: Dispatchers.Main 在不同的平台上的实现不同,如果在 Android 上为 HandlerDispatcher,在 Java Swing 上为 SwingDispatcher 等等。


首先我们通过 launch 启动了一个协程,这类似于我们启动一个线程,launch 的参数有三个,依次为协程上下文、协程启动模式、协程体:


public fun CoroutineScope.launch(context: CoroutineContext = EmptyCoroutineContext, // 上下文 start: CoroutineStart = CoroutineStart.DEFAULT, // 启动模式 block: suspend CoroutineScope.() -> Unit // 协程体): Job


启动模式不是一个很复杂的概念,不过我们暂且不管,默认直接允许调度执行。


上下文可以有很多作用,包括携带参数,拦截协程执行等等,多数情况下我们不需要自己去实现上下文,只需要使用现成的就好。上下文有一个重要的作用就是线程切换,Dispatchers.Main 就是一个官方提供的上下文,它可以确保 launch 启动的协程体运行在 UI 线程当中(除非你自己在 launch 的协程体内部进行线程切换、或者启动运行在其他有线程切换能力的上下文的协程)。


换句话说,在例子当中整个 launch 内部你看到的代码都是运行在 UI 线程的,尽管 getUser 在执行的时候确实切换了线程,但返回结果的时候会再次切回来。这看上去有些费解,因为直觉告诉我们,getUser 返回了一个 Deferred 类型,它的 await 方法会返回一个 User 对象,意味着 await 需要等待请求结果返回才可以继续执行,那么 await 不会阻塞 UI 线程吗?


答案是:不会。当然不会,不然那 DeferredFuture 又有什么区别呢?这里 await 就很可疑了,因为它实际上是一个 suspend 函数,这个函数只能在协程体或者其他 suspend 函数内部被调用,它就像是回调的语法糖一样,它通过一个叫 Continuation 的接口的实例来返回结果:


@SinceKotlin("1.3")public interface Continuation<in T> {public val context: CoroutineContextpublic fun resumeWith(result: Result<T>)}


1.3 的源码其实并不是很直接,尽管我们可以再看下 Result 的源码,但我不想这么做。更容易理解的是之前版本的源码:


@SinceKotlin("1.1")public interface Continuation<in T> {public val context: CoroutineContextpublic fun resume(value: T)public fun resumeWithException(exception: Throwable)}


相信大家一下就能明白,这其实就是个回调嘛。如果还不明白,那就对比下 Retrofit 的 Callback


public interface Callback<T> {void onResponse(Call<T> call, Response<T> response);void onFailure(Call<T> call, Throwable t);}


有结果正常返回的时候,Continuation 调用 resume 返回结果,否则调用 resumeWithException 来抛出异常,简直与 Callback 一模一样。


所以这时候你应该明白,这段代码的执行流程本质上是一个异步回调:


GlobalScope.launch(Dispatchers.Main) {try {//showUser 在 await 的 Continuation 的回调函数调用后执行 showUser(gitHubServiceApi.getUser("bennyhuo").await())} catch (e: Exception) {showError(e)}}


而代码之所以可以看起来是同步的,那就是编译器的黑魔法了,你当然也可以叫它“语法糖”。


这时候也许大家还是有问题:我并没有看到 Continuation 啊,没错,这正是我们前面说的编译器黑魔法了,在 Java 虚拟机上,await 这个方法的签名其实并不像我们看到的那样:


public suspend fun await(): T


它真实的签名其实是:


kotlinx/coroutines/Deferred.await (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;


即接收一个 Continuation 实例,返回 Object 的这么个函数,所以前面的代码我们可以大致理解为:


//注意以下不是正确的代码,仅供大家理解协程使用 GlobalScope.launch(Dispatchers.Main) {gitHubServiceApi.getUser("bennyhuo").await(object: Continuation<User>{override fun resume(value: User) {showUser(value)}override fun resumeWithException(exception: Throwable){showError(exception)}})}


而在 await 当中,大致就是:


//注意以下并不是真实的实现,仅供大家理解协程使用 fun await(continuation: Continuation<User>): Any {... // 切到非 UI 线程中执行,等待结果返回 try {val user = ...handler.post{ continuation.resume(user) }} catch(e: Exception) {handler.post{ continuation.resumeWithException(e) }}}


这样的回调大家一看就能明白。讲了这么多,请大家记住一点:从执行机制上来讲,协程跟回调没有什么本质的区别。

4.2 suspend 函数的方式

suspend 函数是 Kotlin 编译器对协程支持的唯一的黑魔法(表面上的,还有其他的我们后面讲原理的时候再说)了,我们前面已经通过 Deferredawait 方法对它有了个大概的了解,我们再来看看 Retrofit 当中它还可以怎么用。


Retrofit 当前的 release 版本是 2.5.0,还不支持 suspend 函数。因此想要尝试下面的代码,需要最新的 Retrofit 源码的支持;当然,也许你看到这篇文章的时候,Retrofit 的新版本已经支持这一项特性了呢。


首先我们修改接口方法:


@GET("users/{login}")suspend fun getUser(@Path("login") login: String): User


这种情况 Retrofit 会根据接口方法的声明来构造 Continuation,并且在内部封装了 Call 的异步请求(使用 enqueue),进而得到 User 实例,具体原理后面我们有机会再介绍。使用方法如下:


GlobalScope.launch {try {showUser(gitHubServiceApi.getUser("bennyhuo"))} catch (e: Exception) {showError(e)}}


它的执行流程与 Deferred.await 类似,我们就不再详细分析了。

5. 协程到底是什么

好,坚持读到这里的朋友们,你们一定是异步代码的“受害者”,你们肯定遇到过“回调地狱”,它让你的代码可读性急剧降低;也写过大量复杂的异步逻辑处理、异常处理,这让你的代码重复逻辑增加;因为回调的存在,还得经常处理线程切换,这似乎并不是一件难事,但随着代码体量的增加,它会让你抓狂,线上上报的异常因线程使用不当导致的可不在少数。


协程可以帮你优雅的处理掉这些。


协程本身是一个脱离语言实现的概念,我们“很严谨”(哈哈)的给出维基百科的定义:


Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed. Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions,


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


event loops, iterators, infinite lists and pipes.


简单来说就是,协程是一种非抢占式或者说协作式的计算机程序并发调度的实现,程序可以主动挂起或者恢复执行。这里还是需要有点儿操作系统的知识的,我们在 Java 虚拟机上所认识到的线程大多数的实现是映射到内核的线程的,也就是说线程当中的代码逻辑在线程抢到 CPU 的时间片的时候才可以执行,否则就得歇着,当然这对于我们开发者来说是透明的;而经常听到所谓的协程更轻量的意思是,协程并不会映射成内核线程或者其他这么重的资源,它的调度在用户态就可以搞定,任务之间的调度并非抢占式,而是协作式的。


关于并发和并行:正因为 CPU 时间片足够小,因此即便一个单核的 CPU,也可以给我们营造多任务同时运行的假象,这就是所谓的“并发”。并行才是真正的同时运行。并发的话,更像是 Magic。


如果大家熟悉 Java 虚拟机的话,就想象一下 Thread 这个类到底是什么吧,为什么它的 run 方法会运行在另一个线程当中呢?谁负责执行这段代码的呢?显然,咋一看,Thread 其实是一个对象而已,run 方法里面包含了要执行的代码——仅此而已。协程也是如此,如果你只是看标准库的 API,那么就太抽象了,但我们开篇交代了,学习协程不要上来去接触标准库,[kotlinx.coroutines](


) 框架才是我们用户应该关心的,而这个框架里面对应于 Thread 的概念就是 Job 了,大家可以看下它的定义:


public interface Job : CoroutineContext.Element {...public val isActive: Booleanpublic val isCompleted: Booleanpublic val isCancelled: Boolean


public fun start(): Booleanpublic fun cancel(cause: CancellationException? = null)public suspend fun join()...}


我们再来看看 Thread 的定义:


public class Thread implements Runnable {...


public final native boolean isAlive();public synchronized void start() { ... }@Deprecatedpublic final void stop() { ... }public final void join() throws InterruptedException { ... }...}


这里我们非常贴心的省略了一些注释和不太相关的接口。我们发现,Thread 与 Job 基本上功能一致,它们都承载了一段代码逻辑(前者通过 run 方法,后者通过构造协程用到的 Lambda 或者函数),也都包含了这段代码的运行状态。


而真正调度时二者才有了本质的差异,具体怎么调度,我们只需要知道调度结果就能很好的使用它们了。

6. 小结

我们先通过例子来引入,从大家最熟悉的代码到协程的例子开始,演化到协程的写法,让大家首先能从感性上对协程有个认识,最后我们给出了协程的定义,也告诉大家协程究竟能做什么。


这篇文章没有追求什么内部原理,只是企图让大家对协程怎么用有个第一印象。如果大家仍然感觉到迷惑,不怕,后面我将再用几篇文章从例子入手来带着大家分析协程的运行,而原理的分析,会放到大家能够熟练掌握协程之后再来探讨。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
破解 Kotlin 协程(1) - 入门篇,35岁以后的Android程序员出路在哪里