首先我们来看看目前Android开发中成熟的Retrofit + OkHttp + RxJava网络请求的格式:
fun <V> requestData(mObservable: Observable<BaseData<V>>,callBack: HttpCallBack<V>): Disposable {
return mObservable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Consumer<BaseData<V>> {
override fun accept(t: BaseData<V>) {
if (t != null && t.errorCode == 0) {
callBack.success(t.data)
} else {
callBack.failed(Throwable(t.errorMsg))
}
}
}, object : Consumer<Throwable> {
override fun accept(t: Throwable) {
Logging.log(t.localizedMessage)
if (null != callBack) {
callBack.failed(t)
} else {
throw NullPointerException("the object HttpCallback: callBack is NULL")
}
}
})
}
这是我们常规的一个异步请求,通过回调的方式来处理请求结果。
现在我们再来看看kotlin协程下的异步请求:
fun execute01() {
//切到io线程来执行同步请求
GlobalScope.launch(Dispatchers.IO) {
val result = githubApi.getUserSuspend("haoyuegongzi")
//切到主线程来执行UI操作
GlobalScope.launch(Dispatchers.Main) {
println("TAGTAG===>>>IO---result:${Thread.currentThread().name}")
tvVerifyCode.text = result.toString()
}
}
}
interface GitHubApi {
@GET("users/{login}")
suspend fun getUserSuspend(@Path("login") login: String): User
}
看到了吗?就这么简单,线程切换,请求数据,线程切换,更新UI。一行代码实现线程切换。
看到这里,或许有人犯嘀咕,我在老的技术组合“Retrofit + OkHttp + RxJava”里面写好回调,连线程切换都直接省了,还需要这么麻烦?
嗯,说得好像有理,我们继续往下看:
fun execute02() {
GlobalScope.launch(Dispatchers.Main) {
//切到子线程执行任务
var result = withContext(Dispatchers.IO) {
githubApi.getUserSuspend("haoyuegongzi")
}
//任务执行完后自动回到主线程
println("TAGTAG===>>>IO---result:${Thread.currentThread().name}")
tvVerifyCode.text = result.toString()
}
}
这个withContext函数的意义呢,就是能把耗时任务切到子线程去,任务执行完之后,又会自动切回主线程继续执行。
x线程切换,用什么切?Dispatchers.xxx,这个其实就是告诉编译器这里要执行一个异步代码,调用者需要把我切到协程里:就是这个GlobalScope.launch(Dispatchers.Main/io) 。
到了这里,或许你还会认为,协程目前展现出来的优势,还不足以让你心动到抛弃回调,投身kotlin协程的怀抱。
那我们现在来看看Java/Android开发里面让人抓狂的“地狱回调”:
fun enqueue() {
//先获取token
ApiService.enqueue("/login", object : Callback {
override fun onResponse(call: Call, response: Response) {
val token = "..."
//通过token请求用户信息
ApiService.enqueue("/getUserInfo", token, object : Callback {
override fun onResponse(call: Call, response: Response) {
val user = response.body....
runOnUiThread {
//切换到主线程更新信息
tvVerifyCode.text = user
}
}
override fun onFailure(call: Call, e: IOException) {
}
})
}
override fun onFailure(call: Call, e: IOException) {
}
})
}
这里只是两层嵌套,相比开发中遇到过不少这样的情况,有没有很抓狂?
那现在我们来看看用协程怎么实现:
fun execute() {
GlobalScope.launch(Dispatchers.Main) {
val token = login()
val user = githubApi.getUserSuspend(token)
tvVerifyCode.text = user
}
}
有没有被惊讶到?震撼到?就这么简单?利用协程的优势就这么明显?
或许你还深深的爱着回调,而且我知道你心里想的是什么,反正用这种多层回调的场景也不多,应用程序能跑起来不影响性能就好了。
这么说确实也没错,较少遇到的开发场景,没必要耗费太多精力。那么我们继续往下看。
前面这种情况,getUserInfo接口是依赖于login接口返回的token的,所以不可避免的使用了回调。
但是现在有一个场景,我们需要将接口A中的接口B中的数据进行合并展示,但是这两个接口在服务端的接口设计上是没有非常强的关联的,这时候出现了两种人。
第一种:想了想觉得没啥办法,然后还是按照先调用接口A,成功后再调用接口B,然后在接口B的回调中进行数据合并
第二种:觉得第一种方式不合理,所以去找服务端“撕逼”,告诉服务端把这两个接口的数据合并到一个接口中返回,客户端处理不了就找服务端呗。这番“撕逼”下来,你有可能成功了,也有可能失败了,但最终的结果都不是非常好。
我们想象一下,假设接口A耗时100ms,接口B耗时120ms,那么实际上在并发处理的情况下,你最快只需要120ms就可以将两个数据进行合并,但是使用嵌套回调的方案那就需要220ms(100ms+120ms)。
之前讲的都只是代码的美观层面的东西,到这里就是性能问题了,各种小的性能问题不解决,一个app怎么可能有比较好的用户体验。s是如何并发实现的:
fun executeAsync() {
GlobalScope.launch(Dispatchers.Main) {
//使用async发起两个异步请求
val one = async { one() }
val two = async { two() }
//使用await进行合并
val result = one.await().toString() + two.await().toString()
tvVerifyCode.text = result
}
}
相比上面的嵌套回调,协程的并发请求怎么样?是不是不仅在代码逻辑上简单很多,而且在逻辑上更清晰?代码量更少?
从最后一个例子我们可以知道什么是协程,为什么叫协程:
协程:就是协同多个程序之间进行合作的过程,帮助我们轻松的写出复杂的并发代码,甚至还能用非常简单的方式实现原本不可能实现的并发任务。这就是我们为什么要学习协程的理由。