关键词:Kotlin 协程 入门java
假定你对协程(Coroutine)一点儿都不了解,经过阅读本文看看是否能让你明白协程是怎么一回事。git
我以前写过一些协程的文章,好久之前了。那会儿仍是很痛苦的,毕竟 kotlinx.coroutines 这样强大的框架还在襁褓当中,因而乎我写的几篇协程的文章几乎就是在告诉你们如何写这样一个框架——那种感受简直糟糕透了,由于没有几我的会有这样的需求。程序员
此次准备从协程用户(也就是程序员你我他啦)的角度来写一下,但愿对你们能有帮助。github
在开始讲解协程以前,咱们须要先确认几件事儿:api
看下你的答案:网络
咱们经过 Retrofit 发送一个网络请求,其中接口以下:并发
interface GitHubServiceApi {
@GET("users/{login}")
fun getUser(@Path("login") login: String): Call<User>
}
data class User(val id: String, val name: String, val url: String)
复制代码
Retrofit 初始化以下:框架
val gitHubServiceApi by lazy {
val retrofit = retrofit2.Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofit.create(GitHubServiceApi::class.java)
}
复制代码
那么咱们请求网络时:异步
gitHubServiceApi.getUser("bennyhuo").enqueue(object : Callback<User> {
override fun onFailure(call: Call<User>, t: Throwable) {
handler.post { showError(t) }
}
override fun onResponse(call: Call<User>, response: Response<User>) {
handler.post { response.body()?.let(::showUser) ?: showError(NullPointerException()) }
}
})
复制代码
请求结果回来以后,咱们切换线程到 UI 线程来展现结果。这类代码大量存在于咱们的逻辑当中,它有什么问题呢?ide
showError
,在数据读取失败时咱们又调用了一次,真实的开发环境中可能会有更多的重复Kotlin 自己的语法已经让这段代码看上去好不少了,若是用 Java 写的话,你的直觉都会告诉你:你在写 Bug。
若是你不是 Android 开发者,那么你可能不知道 handler 是什么东西,不要紧,你能够替换为
SwingUtilities.invokeLater{ ... }
(Java Swing),或者setTimeout({ ... }, 0)
(Js) 等等。
你固然能够改形成 RxJava 的风格,但 RxJava 比协程抽象多了,由于除非你熟练使用那些 operator,否则你根本不知道它在干吗(试想一下 retryWhen
)。协程就不同了,毕竟编译器加持,它能够很简洁的表达出代码的逻辑,不要想它背后的实现逻辑,它的运行结果就是你直觉告诉你的那样。
对于 Retrofit,改形成协程的写法,有两种,分别是经过 CallAdapter 和 suspend 函数。
咱们先来看看 CallAdapter 的方式,这个方式的本质是让接口的方法返回一个协程的 Job:
interface GitHubServiceApi {
@GET("users/{login}")
fun getUser(@Path("login") login: String): Deferred<User>
}
复制代码
注意 Deferred 是 Job 的子接口。
那么咱们须要为 Retrofit 添加对 Deferred
的支持,这须要用到开源库:
implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
复制代码
构造 Retrofit 实例时添加:
val gitHubServiceApi by lazy {
val retrofit = retrofit2.Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
//添加对 Deferred 的支持
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
retrofit.create(GitHubServiceApi::class.java)
}
复制代码
那么这时候咱们发起请求就能够这么写了:
GlobalScope.launch(Dispatchers.Main) {
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 线程吗?
答案是:不会。固然不会,否则那 Deferred
与 Future
又有什么区别呢?这里 await
就很可疑了,由于它其实是一个 suspend 函数,这个函数只能在协程体或者其余 suspend 函数内部被调用,它就像是回调的语法糖同样,它经过一个叫 Continuation
的接口的实例来返回结果:
@SinceKotlin("1.3")
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
复制代码
1.3 的源码其实并非很直接,尽管咱们能够再看下 Result
的源码,但我不想这么作。更容易理解的是以前版本的源码:
@SinceKotlin("1.1")
public interface Continuation<in T> {
public val context: CoroutineContext
public 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) }
}
}
复制代码
这样的回调你们一看就能明白。讲了这么多,请你们记住一点:从执行机制上来说,协程跟回调没有什么本质的区别。
suspend 函数是 Kotlin 编译器对协程支持的惟一的黑魔法(表面上的,还有其余的咱们后面讲原理的时候再说)了,咱们前面已经经过 Deferred
的 await
方法对它有了个大概的了解,咱们再来看看 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
相似,咱们就再也不详细分析了。
好,坚持读到这里的朋友们,大家必定是异步代码的“受害者”,大家确定遇到过“回调地狱”,它让你的代码可读性急剧下降;也写过大量复杂的异步逻辑处理、异常处理,这让你的代码重复逻辑增长;由于回调的存在,还得常常处理线程切换,这彷佛并非一件难事,但随着代码体量的增长,它会让你抓狂,线上上报的异常因线程使用不当致使的可不在少数。
而协程能够帮你优雅的处理掉这些。
协程自己是一个脱离语言实现的概念,咱们“很严谨”(哈哈)的给出维基百科的定义:
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, 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: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean
public fun start(): Boolean
public fun cancel(cause: CancellationException? = null)
public suspend fun join()
...
}
复制代码
咱们再来看看 Thread 的定义:
public class Thread implements Runnable {
...
public final native boolean isAlive();
public synchronized void start() { ... }
@Deprecated
public final void stop() { ... }
public final void join() throws InterruptedException { ... }
...
}
复制代码
这里咱们很是贴心的省略了一些注释和不太相关的接口。咱们发现,Thread 与 Job 基本上功能一致,它们都承载了一段代码逻辑(前者经过 run 方法,后者经过构造协程用到的 Lambda 或者函数),也都包含了这段代码的运行状态。
而真正调度时两者才有了本质的差别,具体怎么调度,咱们只须要知道调度结果就能很好的使用它们了。
咱们先经过例子来引入,从你们最熟悉的代码到协程的例子开始,演化到协程的写法,让你们首先能从感性上对协程有个认识,最后咱们给出了协程的定义,也告诉你们协程究竟能作什么。
这篇文章没有追求什么内部原理,只是企图让你们对协程怎么用有个第一印象。若是你们仍然感受到迷惑,不怕,后面我将再用几篇文章从例子入手来带着你们分析协程的运行,而原理的分析,会放到你们可以熟练掌握协程以后再来探讨。
欢迎关注 Kotlin 中文社区!
中文官网:www.kotlincn.net/
中文官方博客:www.kotliner.cn/
公众号:Kotlin
知乎专栏:Kotlin
CSDN:Kotlin中文社区
掘金:Kotlin中文社区
简书:Kotlin中文社区