Kotlin协程-Flow

46 阅读6分钟

前言

Flow是Kotlin协程中的流。RxJava是流式编程库。Flow属于冷流对应Rxjava的Observable Flowable Single MayBe和Completable等。kotlin中热流实现MutableShared和MutableFlow等,对应RxJava中热流PublisherSubject和BehaviorSubject。

Flow是基于协程的响应式编程,处理异步数据流。与RxJava相比,Flow优势在于协程中使用,即是优点(协程中使用Flow更加简单)又是局限。Flow是冷流,只有在收集端(collect)开始监听时,生产端(emit)才开始执行。Flow冷流按需生产数据,避免不必要的计算和资源浪费。

冷流:

  1. 只有在flow{}内部才会产生输入。如emit(xxx)
  2. 必须有消费者才会产生数据,需要末端操作符collect()
  3. 生产者和消费者--对应

Flow 处理机制

fun main() {
    runBlocking {
        demo()
    }
}

/**
 * 挂起函数
 */
private suspend fun demo(){
    flow {
        (1..3).forEach{
            delay(1000)
            // 在flow{}内部;挂起函数
            println("emit value:$it")
            emit(it)
        }
    }.collect{
        // 不调用collect,上面emit不会执行
        println("collect value:$it")
    }
}
输出:
emit value:1
collect value:1
emit value:2
collect value:2
emit value:3
collect value:3

同一个协程中多个flow是同步执行的,第二个collect等待上个collect执行完毕。

fun main() {
    runBlocking {
        demo()
    }
}

/**
 * 两个flow在同一个协程中执行
 */
private suspend fun demo(){
    flowOf(1,2,3).onEach {
        delay(1000)
    }.collect{
        println("collect1 value:$it")
    }
    // 上面的flow会阻塞下面的,是同步执行的。
    flowOf(4,5).onEach {
        delay(1000)
    }.collect{
        println("collect2 value:$it")
    }
}
输出:
collect1 value:1
collect1 value:2
collect1 value:3
collect2 value:4
collect2 value:5

异步执行

fun main() {
    runBlocking {
        // 协程1
        launch {
            flowOf(1,2,3).onEach {
                delay(1000)
            }.collect{
                println("collect1 it:$it")
            }
        }
        //协程2 上面的flow不会阻塞下面的,是异步执行
        launch {
            flowOf(4,5).onEach {
                delay(1000)
            }.collect{
                println("collect2 it:$it")
            }
        }
    }
}
输出:
collect1 it:1
collect2 it:4
collect1 it:2
collect2 it:5
collect1 it:3

两个协程中互不阻塞。

切换线程

fun main() {
    runBlocking (Dispatchers.Default){
        // 发送10个元素,从0到9
        val myFlow = flow {
            repeat(10){
                // 修改原来的CoroutineContext,会异常
                withContext(Dispatchers.IO){
                    emit(it)
                }
            }
        }
        launch {
            myFlow.collect{
                println("Coroutine1:$it")
            }
        }
    }
}

Flow不能使用withContext来切协程,可以使用flowOn切协程,或者使用ChannelFlow+withContext来切

fun main() {
    // 不要用withContext切换flow的线程。用flowOn来切
    runBlocking {
        flow {
            emit(1)
        }.onEach {
            delay(1000)
        }.map {
            println("map1:${Thread.currentThread().name}")
        }.flowOn(
            Dispatchers.IO
        ).map {
            println("map2:${Thread.currentThread().name}")
        }.flowOn(Dispatchers.Default
        ).map {
            println("map3:${Thread.currentThread().name}")
        }.collect{
            println("collect:${Thread.currentThread().name}")
        }
    }
}
输出:
map1:DefaultDispatcher-worker-2 @coroutine#3
map2:DefaultDispatcher-worker-1 @coroutine#2
map3:Test worker @coroutine#1
collect:Test worker @coroutine#1

可以多次使用flowOn切换不同的逻辑代码执行线程。

fun main() {
    runBlocking (Dispatchers.Default){
        // 发送10个元素,从0到9
        val myFlow = channelFlow {
            repeat(10){
                // 可以修改原来的CoroutineContext
                withContext(Dispatchers.IO){
                    channel.send(it)
                }
            }
        }
        launch {
            myFlow.collect{
                println("Coroutine1:$it")
            }
        }
    }
}

flow执行完成

fun main() {
    runBlocking {
        flow {
            emit(1)
            delay(1000)
            emit(2)
            throw RuntimeException("test error")
        }.onCompletion {
            println("onCompletion")
        }.catch {
            println("catch:$it")
        }.collect{
            println("collect:$it")
        }
    }
}
输出:
collect:1
collect:2
onCompletion
catch:java.lang.RuntimeException: test error

flow执行完成最终会走到onCompletion,不管是否发生异常,内部try/catch实现,可以做一些释放操作。

Flow的性能和背压

处理大规模数据时,用buffer操作符进行性能优化,使用onEach进行流的中间处理。

val flowWithBuffer: Flow<Data> = fetchData()
    .onEach { data ->
        // 中间处理逻辑
    }
    .buffer() // 使用buffer操作符进行性能优化

buffer允许流插入缓冲区,缓解生产者和消费者速度不一致问题,提高性能。背压处理可以用conflate操作符。conflate会丢弃掉生产者的旧数据,保留新数据,避免背压。

val conflatedFlow: Flow<Data> = fetchData()
    .onEach { data ->
        // 中间处理逻辑
    }
    .conflate() // 使用conflate操作符进行背压处理

数据生产大于消费速度时,保证消费者只处理最新数据,避免队列无限增长导致内存问题。

热流

  1. 不需要在flow产生数据,可以在其他任意地方
  2. 不管消费者是否订阅,生产者都会产生数据

flatMapConcat

类似于RxJava中的concatMap操作符

fun main() {
    runBlocking {
        val myFlow = flow {
            repeat(3) {
                println("emit:$it threadName:${Thread.currentThread().name}")
                emit(it)
            }
        }
        launch {
            // 将原来的流元素构建成一个新的源(按照原来的流元素输出)
            myFlow.flatMapConcat { upstreamValue ->
                flow {
                    repeat(2) {
                        emit(upstreamValue * 10 + it)
                    }
                }
            }.collect {
                println("collect:$it threadName:${Thread.currentThread().name}")
            }
        }
    }
}
输出:
emit:0 threadName:Test worker @coroutine#2
collect:0 threadName:Test worker @coroutine#2
collect:1 threadName:Test worker @coroutine#2
emit:1 threadName:Test worker @coroutine#2
collect:10 threadName:Test worker @coroutine#2
collect:11 threadName:Test worker @coroutine#2
emit:2 threadName:Test worker @coroutine#2
collect:20 threadName:Test worker @coroutine#2
collect:21 threadName:Test worker @coroutine#2

在同一个协程中emit和collect数据,通过faltMapMerge来构建成一个新的流,顺序emit和collect.

flatMapMerge

把上面代码改为flatMapMerge

fun main() {
    runBlocking {
        val myFlow = flow {
            repeat(3) {
                println("emit:$it threadName:${Thread.currentThread().name}")
                emit(it)
            }
        }
        launch {
            // 将原来的流元素构建成一个新的源(按照原来的流元素输出)
            myFlow.flatMapMerge { upstreamValue ->
                flow {
                    repeat(2) {
                        emit(upstreamValue * 10 + it)
                    }
                }
            }.collect {
                println("collect:$it threadName:${Thread.currentThread().name}")
            }
        }
    }
}
输出:
emit:0 threadName:Test worker @coroutine#3
emit:1 threadName:Test worker @coroutine#3
emit:2 threadName:Test worker @coroutine#3
collect:0 threadName:Test worker @coroutine#2
collect:1 threadName:Test worker @coroutine#2
collect:10 threadName:Test worker @coroutine#2
collect:11 threadName:Test worker @coroutine#2
collect:20 threadName:Test worker @coroutine#2
collect:21 threadName:Test worker @coroutine#2

emit和collect在两个不同的协程。协程#3顺序emit。协程#2顺序collect。从协程号上可以看出是先创建collect协程再创建emit协程(这也简洁说明flow是冷流,由collect开始的)

去掉launch

fun main() {
    runBlocking {
        val myFlow = flow {
            repeat(3) {
                println("emit:$it threadName:${Thread.currentThread().name}")
                emit(it)
            }
        }
        // 这个会使emit和collect不在同一个协程中
        myFlow.flatMapMerge { upstreamValue ->
            flow {
                repeat(2) {
                    emit(upstreamValue * 10 + it)
                }
            }
        }.collect {
            println("collect:$it threadName:${Thread.currentThread().name}")
        }
    }
}
输出:
emit:0 threadName:Test worker @coroutine#2
emit:1 threadName:Test worker @coroutine#2
emit:2 threadName:Test worker @coroutine#2
collect:0 threadName:Test worker @coroutine#1
collect:1 threadName:Test worker @coroutine#1
collect:10 threadName:Test worker @coroutine#1
collect:11 threadName:Test worker @coroutine#1
collect:20 threadName:Test worker @coroutine#1
collect:21 threadName:Test worker @coroutine#1

可以看到我们写到同一个协程里,但是用了flatMapMerge会使emit和collect不在同一个协程里输出。

fun main() {
    runBlocking {
        val myFlow = flow {
            repeat(3) {
                println("emit:$it threadName:${Thread.currentThread().name}")
                emit(it)
            }
        }
        // 并发数是1
        myFlow.flatMapMerge(1) { upstreamValue ->
            flow {
                repeat(2) {
                    emit(upstreamValue * 10 + it)
                }
            }
        }.collect {
            println("collect:$it threadName:${Thread.currentThread().name}")
        }
    }
}
输出:
emit:0 threadName:Test worker @coroutine#1
collect:0 threadName:Test worker @coroutine#1
collect:1 threadName:Test worker @coroutine#1
emit:1 threadName:Test worker @coroutine#1
collect:10 threadName:Test worker @coroutine#1
collect:11 threadName:Test worker @coroutine#1
emit:2 threadName:Test worker @coroutine#1
collect:20 threadName:Test worker @coroutine#1
collect:21 threadName:Test worker @coroutine#1

当flatMapMerge的concurrency(默认16)是1(并发数是1),emit和collect在同一个协程中。则emit一次colect消费后才发送下一个emit。

flatMapLatest

fun main() {
    runBlocking {
        val myFlow = flow {
            repeat(3) {
                println("emit:$it threadName:${Thread.currentThread().name}")
                emit(it)
            }
        }
        // 会使emit和collect不在同一个协程中。并发数是1
        myFlow.flatMapLatest { upstreamValue ->
            flow {
                repeat(2) {
                    emit(upstreamValue * 10 + it)
                }
            }
        }.collect {
            println("collect:$it threadName:${Thread.currentThread().name}")
        }
    }
    
}
输出:
emit:0 threadName:Test worker @coroutine#3
emit:1 threadName:Test worker @coroutine#3
emit:2 threadName:Test worker @coroutine#3
collect:20 threadName:Test worker @coroutine#2
collect:21 threadName:Test worker @coroutine#2

在两个协程中,flatMapLatest中延迟100ms进行flatMapLatest操作,只会拿到最后一次的flatMapLatest(保证消费者拿到最新,丢弃中间数据)。如果去掉flatMapLatest中的delay延迟,则还是两个协程中执行,但是可以看到emit和collect全部打印出来了。也就是说flatMapLatest中如果是耗时的则会丢弃中间数据保证最后的数据得到。

flatMapConcat默认emit和collect在同一个协程中(串式)。flatMapMerge(并发)和flatMapLatest(并发中间数据可能丢失,保证最新数据)默认emit和collect不在同一个协程中。

总结:

Flow错误处理机制、数据转换合并、性能优化、背压等