Kotlin自2016年发布1.0版本发展到现在的1.3版本也已经有三年了,其中更是在2017年被Google认定支持为Android官方开发语言,相信绝大多数Android开发者都曾关注过kotlin并已使用它,我大概是在1.2版本的时候开始用的,之前也接触过一些,但是觉得刚出来的东西可能不太稳定,就没有太在意,随着Kotlin语言的完善更新和我对它的深入使用,越发觉得用了就回不去了(当然是指回到java),尤其是在接触了协程之后,所以本着对知识的总结和积累,于是就有了今天这篇关于协程的文章~
如果有对Kotlin或者协程还不熟悉的可以参考以下三部分官方文档进行基础学习,这是我自己参考后列出的学习顺序,可以更加容易理解和学习。
第一部分:异步编程技术、你的第一个协同程序
——你将知道协程是什么
第二部分:协程指南
——协程的基础和基本使用
第三部分:使用协同程序进行UI编程
——协程在Android中的使用
以下内容是结合第三部分的扩展,需要将协程运用在Android编程中的小伙伴可以看看:
利用协程实现Button倒计时
倒计时的例子经常被我们用在获取短信验证码后的场景,那么这个场景可以怎么实现呢?
1)通过Handler延时一秒发送消息,并循环60次
2)通过CountDownTimer(本质上还是Handler)
3)通过ObjectAnimator
4)通过协程
今天就讲讲如何通过协程实现这个场景。看了上面的官方教程,你应该知道了协程是依附在线程上并不会阻塞线程,所以基于这两点,我们可以在UI线程开启一个协程操作控件。
tv_countdown.setOnClickListener {
job = launch {
it.isClickable = false
for (i in 10 downTo 1) {
tv_countdown.text = "$i"
delay(1000)
}
tv_countdown.text = "reStart"
it.isClickable = true
}
}
是不是很直观,涉及倒计时的代码就在简单的for循环里,没有回调,没有绕来绕去,相信一个没有编程基础的人也能看懂这段代码是在做什么。
如果想要取消倒计时,也很简单,只需操作launch函数返回的job调用。
job.cancel()
利用通道控制并发任务数量
再看第二部分的文档时,各位应该注意到了协程中的通道,用过RxJava的可能觉得这玩意很类似其中的背压,相当于一个任务队列,先进先出,支持缓冲、条件过滤、预操作、合并等等,可以做的和Rxjava支持的差不多。
接着上面倒计时的例子,在我开启一个倒计时我禁用了Button的点击,直到倒计时结束,防止因多次点击,而在一个Button上并发多个倒计时,除了这种方法外,在官方文档中还提到了利用通道实现这一目的,并提到一个好的解决方式是使用一个 actor 来执行任务而不应该进行并发。
为View添加扩展方法
fun View.onClick(action: suspend (View) -> Unit) {
// 启动一个 actor
val eventActor = GlobalScope.actor<View>(Dispatchers.Main) {
for (event in channel) action(event)
}
// 设置一个监听器来启用 actor
setOnClickListener {
eventActor.offer(it)
}
}
应用到我们的例子中:
tv_countdown.onClick {
for (i in 10 downTo 1) {
tv_countdown.text = "$i"
delay(1000)
}
tv_countdown.text = "reStart"
}
那么这个actor又是什么?为什么使用actor就能解决执行任务而不是并发呢?
public fun <E> CoroutineScope.actor(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = 0,
start: CoroutineStart = CoroutineStart.DEFAULT,
onCompletion: CompletionHandler? = null,
block: suspend ActorScope<E>.() -> Unit
): SendChannel<E>{
....
}
actor为协程的一个扩展方法,参数有协程上下文、容量、启动选项、可选的actor协程完成处理器、协程代码,这些参数都设置了默认值,并封装了一些代码,结果返回一个SendChannel对象。其他参数不管,我们只看第二个参数capacity,这个参数用来生成一个容量为0的Channel对象的。
val channel = Channel<E>(capacity)
其实这个capacity是有三个枚举值的,并且根据不同的值返回不同的Channel。
|
当capacity 为0时 - 它会创建RendezvousChannel 。该通道根本没有任何缓冲区。只有当发送和接收调用及时相遇(集合点)时,元素才会从发送方传输到接收方,因此发送暂停直到另一个协程调用receive和receive,直到另一个协程调用send。 |
|
当capacity 是Channel.CONFLATED - 它会创建ConflatedChannel 。此通道最多缓冲一个元素并混合所有后续send 和offer 调用,以便接收器始终获取最近发送的元素。返回发送的已发送元素已合并 - 仅接收最近发送的元素,而先前发送的元素将丢失。此频道的发件人永远不会暂停并提供始终返回true 。 |
|
当capacity 被Channel.UNLIMITED -它创造LinkedListChannel 。这是一个具有无限容量的链表缓冲区的通道(仅受可用内存限制)。此频道的发件人永远不会暂停并提供始终返回true 。 |
为什么要设置一个缓冲为0的通道呢?这里就要引出send()方法和offer()方法了。这两个方法都是将元素添加到通道。
那么send()方法和offer()方法有什么区别呢?
/**
* Adds [element] into to this channel, suspending the caller while this channel [isFull],
* or throws exception if the channel [isClosedForSend] (see [close] for details).
* .....
*/
public suspend fun send(element: E)
首先通过方法的定义可以看出,send()是一个挂起方法,也就意味着它只能在挂起方法或协程中调用。其将元素添加到此通道,在此通道的缓冲区已满或者不存在时挂起调用者。
/**
* Adds [element] into this queue if it is possible to do so immediately without violating capacity restrictions
* and returns `true`. Otherwise, it returns `false` immediately
* or throws exception if the channel [isClosedForSend] (see [close] for details).
*/
public fun offer(element: E): Boolean
而offer()就是一个普通的方法,可以在任何地方调用,在不违反容量限制的情况下立即执行此操作将元素添加到此队列中并返回true,
否则返回false。
回到原来的问题,难道只要通道没有缓冲,并发任务就能变成只执行一个任务了吗?这话不完全对,要想实现这个转变还需要offer()的配合,为什么不是send(),就因为send()是一个挂起方法,并不像offer()一样会立即执行。对于上面倒计时的例子来说,这意味着当倒计时动画进行中时, 点击动作会被忽略。这会发生的原因是 actor 正忙于执行并且通道中没有缓存,这时你看offer()返回的布尔值将为false。
利用Retrofit2+协程进行网络请求
Retrofit2+RxJava大家应该用的不少了,但是现在有了协程,怎么配合Retrofit2使用呢,毕竟协程太方便了。不用担心,Retrofit2已经支持协程了,它的作者已经更新了配合Retrofit2使用的协程适配器。
Android中协程一定会是趋势,这点通过它和rxJava的收藏量可以略得一二。
我们先来看一个Retrofit2配合Rxjava的示例,请求加载一个新闻列表。
ApiManager.getApiManager().getApiService().
getNews(param1 ?: "top", "75c088a1daa9e51b558a74e2049c1aa0").
subscribeOn(Schedulers.newThread()).
observeOn(AndroidSchedulers.mainThread()).
subscribe { t ->
initRecycler(t.result.data)
}
其实看起来也并不复杂是不是,当然这是callback没有封装过的情况下。
那么,使用协程请求这个列表是怎样的?
launch{
val newsBean = ApiManager.getInstance().getApiService().getNews("top", "75c088a1daa9e51b558a74e2049c1aa0").await()
initRecycler(newsBean.result.data)
}
少了回调,是不是看起来比Rxjava顺眼了许多,这是因为协程的使用更符合我们的逻辑思维,自上而下,顺序执行,使用同步的方式编写了异步请求。
也许上面这个例子没有让你完全的体会到两者的差别,这次我们来做一个复杂的网络请求,还是上面这个例子,现在的场景是这样的(只是为了更好地说明,并非实际的场景)—— 我们需要先登录账户获取userId,然后在通过userId去获取token,最后拿着token去请求列表,一共三个网络请求。照例,Rxjava先来:
val apiService = ApiManager.getApiManager("").getApiService()
apiService.
login("15700000001", "asdf1234")
.subscribeOn(Schedulers.newThread())
.observeOn(Schedulers.newThread())
.flatMap { apiService.getToken(it) }
.observeOn(Schedulers.newThread())
.flatMap { apiService.getNews("top", it) }
.observeOn(AndroidSchedulers.mainThread())
.subscribe{ t ->
initRecycler(t.result.data)
}
有没有看晕?别的不说,就这线程的切换看的我就够眼花了,而对flatMap操作符不熟悉的人可能还需要再花时间去学习这个操作符的使用,况且这些只是精简后的少量代码,在flatMap中你可能还需要针对请求的响应做某些处理,就算这些你都觉得问题不大,一旦这个请求链中的某个环节出了异常,那日志信息可是比你刚接手的代码还要晦涩难懂,排查起来可够你喝一壶的。
so,使用协程写起来会是什么亚子呢?会比Rxjava好吗?
val apiService = ApiManager.getInstance().getApiService()
launch {
val userId = apiService.login("15700000001", "asdf1234").await()
val token = apiService.getToken(userId).await()
val newsBean = apiService.getNews("top", token).await()
initRecycler(newsBean.result.data)
}
wow,awesome~~
相比之前的版本只是多了两行代码,代码整体逻辑显得非常简洁,token会等待userId的完成,列表会等待token的完成,不管是执行流程上还是代码逻辑上给我们呈现的都是以同步的方式完成了异步请求。
协程中的异常捕获
上面的多请求例子中,使用Rxjava捕获异常并不简单,辛亏协程不会如此,想要在协程中捕获异常非常简单。
只需要在launch中加上参数
CoroutineExceptionHandler { _, throwable -> Log.e("kkk", throwable.message) }
现在我们运行上面的例子,抛出一个异常试试
throw IndexOutOfBoundsException("test exception")
可以看到,异常的确被我们捕获了
E/kkk: test exception
结尾
看完协程的官方文档和本编博文后,协程到底香不香在你心中应该有了答案。反正我是决定下个项目开始就用协程了!!!