一、概述
挂起函数、async,一次都只能返回一个结果,但在某些业务场景下,我们往往需要协程返回多次结果,比如 IM 通道接收的消息,或者是手机定位返回的经纬度坐标需要实时更新。那么,在这些场景下,之前学习的协程知识就无法直接解决了。Kotlin 协程中的 Channel,就是专门用来做这种事情的。
二、Channel管道概念
理解Channel可以结合Rxjava流的概念,Channel是一个管道,管道首尾端可以收发数据,如下图所示:
结合下面的代码来看看:
fun main() {
runBlocking {
printMsg("start")
//创建管道
val channel = Channel<Int>()
//主线程协程
launch{
(1..5).forEach {
printMsg("channel send $it")
channel.send(it)
}
}
//子线程协程
launch(Dispatchers.IO) {
for (i in channel) {
printMsg("channel receive $i")
}
}
printMsg("end")
}
}
fun printMsg(msg: String) {
println("${Thread.currentThread().name} $msg")
}
在runBlocking的开始和结束打印了日志,同时通过Channel发送了5条数据,需要注意的是发送数据和接收数据的协程并不相同,并且没有写Thread.sleep的代码防止线程退出,看看日志输出:
main @coroutine#1 start
main @coroutine#1 end
main @coroutine#2 channel send 1
main @coroutine#2 channel send 2
DefaultDispatcher-worker-1 @coroutine#3 channel receive 1
DefaultDispatcher-worker-1 @coroutine#3 channel receive 2
main @coroutine#2 channel send 3
main @coroutine#2 channel send 4
DefaultDispatcher-worker-1 @coroutine#3 channel receive 3
DefaultDispatcher-worker-1 @coroutine#3 channel receive 4
main @coroutine#2 channel send 5
DefaultDispatcher-worker-1 @coroutine#3 channel receive 5
可以看到:
runBlocking即使打印了end,整个协程也没有退出,channel还在运行,说明channel不会主动退出。channel可以跨线程和协程传输数据。channel可以在for循环中迭代,这是因为channel实现了ReceiveChannel接口,而ReceiveChannel有iterator()迭代的方法,源码如下:
//channel实现了SendChannel、ReceiveChannel接口
public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> {...}
//ReceiveChannel有iterator()方法
public interface ReceiveChannel<out E> {
...
//返回ChannelIterator
public operator fun iterator(): ChannelIterator<E>
...
}
//ChannelIterator源码
public interface ChannelIterator<out E> {
public suspend operator fun hasNext(): Boolean
public suspend fun next0(): E {...}
public operator fun next(): E
}
如果想要关闭Channnel以免浪费协程资源只需要调用close方法即可:
channel.close()
三、Channel源码解析
Channel的源代码如下:
public fun <E> Channel(
capacity: Int = RENDEZVOUS,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E>{...}
当我们调用Channel()的时候,感觉像是在调用一个构造函数,但实际上它却只是一个普通的顶层函数。这个函数带有一个泛型参数 E,另外还有三个参数。
1、capacity
第一个参数capacity,代表了管道的容量。在默认情况下是RENDEZVOUS,代表了默认Channel 的管道容量为 0。
capacity 有以下几种情况:
RENDEZVOUS,默认值,代表了容量为 0;UNLIMITED,代表了无限容量;CONFLATED,代表了容量为 1,新的数据会替代旧的数据;BUFFERED,代表了具备一定的缓存容量,默认情况下是 64,具体容量由这个 VM 参数决定kotlinx.coroutines.channels.defaultBuffer。Int除了上面的几种模式,也可以给任意的Int值来定义管道容量
接下来只改变创建Channel的参数并调用了channel.close()方法,其他代码不变,看下代码和日志:
① RENDEZVOUS
//-------------------------------RENDEZVOUS--------------------------------
val channel = Channel<Int>(capacity = Channel.Factory.RENDEZVOUS)
//日志
main @coroutine#1 start
main @coroutine#1 end
main @coroutine#2 channel send 1
main @coroutine#2 channel send 2
DefaultDispatcher-worker-1 @coroutine#3 channel receive 1
DefaultDispatcher-worker-1 @coroutine#3 channel receive 2
main @coroutine#2 channel send 3
main @coroutine#2 channel send 4
DefaultDispatcher-worker-1 @coroutine#3 channel receive 3
DefaultDispatcher-worker-1 @coroutine#3 channel receive 4
main @coroutine#2 channel send 5
DefaultDispatcher-worker-1 @coroutine#3 channel receive 5
本来以为发送二个接收二个是一种偶然现象,但当把发送的个数改为1000,发现还是这样,可以理解为这种模式确实有这个规律。
② UNLIMITED
//-------------------------------UNLIMITED--------------------------------
val channel = Channel<Int>(capacity = Channel.Factory.UNLIMITED)
//日志
main @coroutine#1 start
main @coroutine#1 end
main @coroutine#2 channel send 1
main @coroutine#2 channel send 2
main @coroutine#2 channel send 3
main @coroutine#2 channel send 4
main @coroutine#2 channel send 5
DefaultDispatcher-worker-1 @coroutine#3 channel receive 1
DefaultDispatcher-worker-1 @coroutine#3 channel receive 2
DefaultDispatcher-worker-1 @coroutine#3 channel receive 3
DefaultDispatcher-worker-1 @coroutine#3 channel receive 4
DefaultDispatcher-worker-1 @coroutine#3 channel receive 5
由于 Channel 的容量是UNLIMITED无限大的,所以发送方可以一直往管道当中塞入数据,等数据都塞完以后,接收方才开始接收。这跟之前的交替执行是不一样的。要注意。
③ CONFLATED
//-------------------------------CONFLATED--------------------------------
val channel = Channel<Int>(capacity = Channel.Factory.CONFLATED)
//日志
main @coroutine#1 start
main @coroutine#1 end
main @coroutine#2 channel send 1
main @coroutine#2 channel send 2
main @coroutine#2 channel send 3
main @coroutine#2 channel send 4
main @coroutine#2 channel send 5
DefaultDispatcher-worker-1 @coroutine#3 channel receive 1
DefaultDispatcher-worker-1 @coroutine#3 channel receive 5
等上一次处理完才去接收最新的数据,这种情况会丢失一些发送的数据。
④ BUFFERED
//-------------------------------BUFFERED--------------------------------
val channel = Channel<Int>(capacity = Channel.Factory.BUFFERED)
//日志
main @coroutine#1 start
main @coroutine#1 end
main @coroutine#2 channel send 1
main @coroutine#2 channel send 2
main @coroutine#2 channel send 3
main @coroutine#2 channel send 4
main @coroutine#2 channel send 5
DefaultDispatcher-worker-1 @coroutine#3 channel receive 1
DefaultDispatcher-worker-1 @coroutine#3 channel receive 2
DefaultDispatcher-worker-1 @coroutine#3 channel receive 3
DefaultDispatcher-worker-1 @coroutine#3 channel receive 4
DefaultDispatcher-worker-1 @coroutine#3 channel receive 5
因为BUFFERED默认的缓冲区是64,所以日志的输出肯定是这样的。如果发送的数据大于64,是否等发送到64个后才开始回调接收端呢?以发送1000个数据为例,截取其中的一小段日志如下:
......
main @coroutine#2 channel send 5
main @coroutine#2 channel send 6
DefaultDispatcher-worker-1 @coroutine#3 channel receive 1
main @coroutine#2 channel send 7
DefaultDispatcher-worker-1 @coroutine#3 channel receive 2
main @coroutine#2 channel send 8
......
通过上面的日志片段可以看出,虽然缓冲区为64,但是接收数据并不一定要把缓冲区填满才开始。
2、onBufferOverflow
第二个参数onBufferOverflow是指: 指定第一个参数capacity容量的情况下管道的容量满了,Channel用什么样的策略来应对。
这里,它主要有三种做法:
SUSPEND,默认,当管道的容量满了以后,如果发送方还要继续发送,我们就会挂起当前的send()方法。由于它是一个挂起函数,所以我们可以以非阻塞的方式,将发送方的执行流 程挂起,等管道中有了空闲位置以后再恢复。DROP_OLDEST,顾名思义,就是丢弃最旧的那条数据,然后发送新的数据;DROP_LATEST,丢弃最新的那条数据。这里要注意,这个动作的含义是丢弃当前正准备 发送的那条数据,而管道中的内容将维持不变。
我们可以定义第一个参数capacity为Int值1,第二个参数onBufferOverflow为DROP_OLDEST来实现第一个参数capacity为CONFLATED的模式,代码如下:
fun main() {
runBlocking {
printMsg("start")
//创建管道
val channel = Channel<Int>(
capacity = 1, <-------------------变化在这里
onBufferOverflow = BufferOverflow.DROP_OLDEST <-------------------变化在这里
)
//主线程协程
launch{
(1..5).forEach {
printMsg("channel send $it")
channel.send(it)
}
channel.close()
}
//子线程协程
launch(Dispatchers.IO) {
for (i in channel) {
printMsg("channel receive $i")
}
}
printMsg("end")
}
}
fun printMsg(msg: String) {
println("${Thread.currentThread().name} $msg")
}
//日志
main @coroutine#1 start
main @coroutine#1 end
main @coroutine#2 channel send 1
main @coroutine#2 channel send 2
main @coroutine#2 channel send 3
main @coroutine#2 channel send 4
main @coroutine#2 channel send 5
DefaultDispatcher-worker-1 @coroutine#3 channel receive 1
DefaultDispatcher-worker-1 @coroutine#3 channel receive 5
3、onUndeliveredElement
先看一段代码:
fun main() {
runBlocking {
printMsg("start")
//创建管道
val channel = Channel<Int>(
capacity = Channel.Factory.UNLIMITED,
onUndeliveredElement = { <--------------第三个参数onUndeliveredElement为高阶函数
printMsg("onUndeliveredElement $it") <--------------打印第三个参数回调的值
}
)
//发送3个数据
(1..3).forEach {
printMsg("channel send $it")
channel.send(it)
}
//取出一个,剩下二个
channel.receive()
//取消当前的channel
channel.cancel()
printMsg("end")
}
}
fun printMsg(msg: String) {
println("${Thread.currentThread().name} $msg")
}
//日志
main @coroutine#1 start
main @coroutine#1 channel send 1
main @coroutine#1 channel send 2
main @coroutine#1 channel send 3
main @coroutine#1 onUndeliveredElement 2 //onUndeliveredElement回调
main @coroutine#1 onUndeliveredElement 3 //onUndeliveredElement回调
main @coroutine#1 end
可以看到,onUndeliveredElement 的作用,就是一个回调,当我们发送出去的 Channel 数据无法被接收方处理的时候,就可以通过 onUndeliveredElement 这个回调,来进行监听。
四、Channel 关闭引发的问题
如果我们忘记调用Channel的 close(),所以会导致程序一直运行无法终止。这个问题其实是很严重的。有没有办法避免这个问题呢?Kotlin 官方其实还为我们提供了另一种创建 Channel 的方式,也就是 produce{}高阶函数。produce{}会在数据收发完成后关闭通道:
fun main() {
runBlocking {
printMsg("start")
//创建管道
val channel = produce {
//发送1个数据
send(1)
}
//取出1个数据
channel.receiveCatching().also {
printMsg("channel receive ${it.getOrNull()} ,isClosed=${it.isClosed}")
}
//尝试再取出一个数据
channel.receiveCatching().also {
printMsg("channel receive ${it.getOrNull()} ,isClosed=${it.isClosed}")
}
printMsg("end")
}
}
fun printMsg(msg: String) {
println("${Thread.currentThread().name} $msg")
}
//日志
main @coroutine#1 start
main @coroutine#1 channel receive 1 ,isClosed=false
main @coroutine#1 channel receive null ,isClosed=true
main @coroutine#1 end
五、Channel接收数据的方式
1、channel.consumeEach {}
kotlin为我们提供了一个高阶函数以方便我们接收数据,如下:
fun main() {
runBlocking {
printMsg("start")
//创建管道
val channel = produce {
(1..5).forEach {
printMsg("channel send $it")
//发送数据
send(it)
}
}
//接收数据
channel.consumeEach {
printMsg("channel receive $it,isChannelClose=${channel.isClosedForReceive}")
}
printMsg("end")
}
}
fun printMsg(msg: String) {
println("${LocalDateTime.now()} ${Thread.currentThread().name} $msg")
}
//日志
main @coroutine#1 start
main @coroutine#2 channel send 1
main @coroutine#2 channel send 2
main @coroutine#1 channel receive 1,isChannelClose=false
main @coroutine#1 channel receive 2,isChannelClose=false
main @coroutine#2 channel send 3
main @coroutine#2 channel send 4
main @coroutine#1 channel receive 3,isChannelClose=false
main @coroutine#1 channel receive 4,isChannelClose=false
main @coroutine#2 channel send 5
main @coroutine#1 channel receive 5,isChannelClose=true //channel已经关闭
main @coroutine#1 end
2、for循环
fun main() {
runBlocking {
printMsg("start")
//创建管道
val channel = produce {
(1..5).forEach {
printMsg("channel send $it")
//发送数据
send(it)
}
}
//接收数据
for(i in channel){
printMsg("channel receive $i,isChannelClose=${channel.isClosedForReceive}")
}
printMsg("end")
}
}
fun printMsg(msg: String) {
println("${Thread.currentThread().name} $msg")
}
//日志
main @coroutine#1 start
main @coroutine#2 channel send 1
main @coroutine#2 channel send 2
main @coroutine#1 channel receive 1,isChannelClose=false
main @coroutine#1 channel receive 2,isChannelClose=false
main @coroutine#2 channel send 3
main @coroutine#2 channel send 4
main @coroutine#1 channel receive 3,isChannelClose=false
main @coroutine#1 channel receive 4,isChannelClose=false
main @coroutine#2 channel send 5
main @coroutine#1 channel receive 5,isChannelClose=true //channel已经关闭
main @coroutine#1 end
3、receiveCatching
如果channel已经被关闭,那么调用channel.receive()会报错kotlinx.coroutines.channels.ClosedReceiveChannelException: Channel was closed,可以使用receiveCatching,如下:
fun main() {
runBlocking {
printMsg("start")
//创建管道
val channel = produce {
printMsg("channel send 1")
//发送数据
send(1)
}
//接收数据
channel.receiveCatching().also {
if (it.isSuccess) {
val result = it.getOrNull()
printMsg("channel receive $result,isChannelClose=${it.isClosed}")
}
}
//尝试再次接收数据
channel.receiveCatching().also {
printMsg("channel receive again, isSuccess=${it.isSuccess}, isChannelClose=${it.isClosed}")
}
printMsg("end")
}
}
fun printMsg(msg: String) {
println("${Thread.currentThread().name} $msg")
}
//日志
main @coroutine#1 start
main @coroutine#2 channel send 1
main @coroutine#1 channel receive 1,isChannelClose=false
main @coroutine#1 channel isSuccess=false,isChannelClose=true
main @coroutine#1 end
小结:
当我们想要读取 Channel 当中的数据时,我们一定要使用 for 循环,或者是channel.consumeEach {},千万不要直接调用 channel.receive()。如果在某些特殊场景下,我们必须要自己来调用 channel.receive(),那么可以考虑使用 receiveCatching(),它可以防止异常发生。注意 receiveCatching()只能接收一个数据并且会立刻结束,不能用它接收多组数据。
六、为什么说Channel是热的?
先看下面一段代码
fun main() {
runBlocking {
printMsg("start")
//创建管道
val channel = produce<Int>(capacity = 10) {
//发送数据
(1..3).forEach {
send(it)
printMsg("channel send $it")
}
}
//没有接收者
printMsg("end")
}
}
fun printMsg(msg: String) {
println("${Thread.currentThread().name} $msg")
}
//日志
main @coroutine#1 start
main @coroutine#1 end
main @coroutine#2 channel send 1
main @coroutine#2 channel send 2
main @coroutine#2 channel send 3
即使没有接收者,Channel也会发送数据,所以我们说Channel是热的。
七、Channel的缺点
1、可能接收到旧数据,也有可能数据丢失
2、因为是“热”的只管发,会造成资源浪费
3、如果不及时的close,在页面退出、注销监听器等场景下可能会导致内存泄漏。
八、Channel的应用场景
Channel的应用场景一般偏底层,所以在实际开发中直接使用的场景比较少,下面几个例子抛砖引玉。
1、协程间传递数据
val channel = Channel<String>()
//协程一
launch {
channel.send("Hello World")
}
//协程二
launch {
channel.receiveCatching().also {
printMsg(it.getOrElse { "" })
}
}
2、基于Channel的生产者-消费者模型
fun main() {
runBlocking {
val channel = Channel<String>()
//生产者
launch {
channel.send("Hello")
delay(500)
channel.send("World")
delay(500)
channel.send("Hello")
delay(500)
channel.send("Kotlin")
}
//消费者
launch {
//用循环接收
while (true){
channel.receiveCatching().also {
printMsg(it.getOrElse { "" })
}
}
}
}
}
3、做一个倒计时功能
/**
* Activity倒计时的扩展方法
*/
fun AppCompatActivity.countDownFlow(
time: Int = 60,
start: ((scope: CoroutineScope) -> Unit)? = null,
next: ((describe: String) -> Unit)? = null,
end: () -> Unit,
catch: () -> Unit
): Job {
return lifecycleScope.launch {
flow {
(time downTo 0).forEach {
delay(1000)
emit(it)
}
}.onStart {
//倒计时开始,这里可以让Button禁止点击状态
start?.let { it(this@launch) }
}.onCompletion {
//倒计时结束,这里可以让Button恢复点击状态
end()
}.catch {
//捕获协程的异常
catch()
}.collect {
//在这里显示每一秒倒计时的UI
next?.let { n -> n(it.toString()) }
}
}
}
不推荐直接使用Channel,推荐使用Flow。
参考了以下内容:
其他资料