这一次,让Kotlin Flow 操作符真正好用起来

news2024/11/29 12:52:15

前言

Kotlin Flow 如此受欢迎大部分归功于其丰富、简洁的操作符,巧妙使用Flow操作符可以大大简化我们的程序结构,提升可读性与可维护性。
然而,虽然好用,但有些操作符不太好理解,可惜的是网上大部分文章只是简单介绍其使用,并没有梳理各个操作符的关系以及引入的缘由,本篇将通过关键原理与使用场景串联大部分操作符,以期达到举一反三的效果。
通过本篇文章,你将了解到:

  1. 操作符全家福
  2. 单Flow操作符的原理以及使用场景
  3. 单Flow操作符里的多协程原理以及使用场景
  4. 多Flow操作符里的多协程原理以及使用场景
  5. Flow操作符该怎么学?

1. 操作符全家福

Flow操作符分类.png

红色部分为使用了多协程的操作符
上图仅包含常用官方提供的操作符,其它未包含进来的操作符原理也是类似的,当然我们也可以封装自己的操作符

由图上可知,将操作符分为了三类:

  1. 构建操作符
  2. 中间操作符
  3. 末端操作符

2. 单Flow操作符的原理以及使用场景

最简单的Flow

    fun test0() {
        runBlocking {
            //构造flow
            val flow = flow {
                //上游
                emit("hello world ${Thread.currentThread()}")
            }
            //收集flow
            flow.collect {
                //下游
                println("collect:$it ${Thread.currentThread()}")
            }
        }
    }

如上包含了两种操作符:构造操作符flow与末端操作符collect。
image.png
总结来说,flow调用流程简化为:两个操作符+两个闭包+emit函数:

  1. collect操作符触发调用,执行了flow的闭包
  2. flow闭包里调用emit函数,执行了collect闭包

Flow返回集合

collect闭包里仅仅只是打印了数据,有个需求:需要将收集到的数据放在List里。
很容易就想到:

    fun test00() {
        runBlocking {
            val result = mutableListOf<String>()
            //构造flow
            val flow = flow {
                //上游
                emit("hello world ${Thread.currentThread()}")
            }
            //收集flow
            flow.collect {
                //下游
                println("collect:$it ${Thread.currentThread()}")
                result.add(it)
            }
        }
    }

如上,定义List变量,在collect的闭包里收到数据后填充到List里。
某天,我们发现这个功能挺常用,需要将它封装起来,外界只需要传入List对象即可。

public suspend fun <T, C : MutableCollection<in T>> Flow<T>.toCollection(destination: C): C {
    collect { value ->
        destination.add(value)
    }
    return destination
}

外部使用:

    fun test01() {
        runBlocking {
            val result = mutableListOf<String>()
            flow {
                //上游
                emit("hello world ${Thread.currentThread()}")
            }.toList(result)
        }
    }

如此一看,简单了许多,这也是官方提供的Flow操作符。

原理很简单:

  1. 作为Flow的扩展函数
  2. 重写了Flow的collect闭包,也就是FlowCollector的emit函数

后续很多操作符都是这么个套路,比如取Flow的第一个数据:first操作符,比如取对Flow里相邻的两个值做操作:reduce操作符等等。

Flow变换操作符

有个需求:在Flow流到下游之前,对数据进行处理,处理完成后再发射出去。
可以使用transform 操作符。

    fun test02() {
        runBlocking {
            flow {
                //上游
                emit("hello world ${Thread.currentThread()}")
            }.transform {
                emit("$it man")
            }.collect {
                println("$it")
            }
        }
    }

再看看原理:

public inline fun <T, R> Flow<T>.transform(
    @BuilderInference crossinline transform: suspend FlowCollector<R>.(value: T) -> Unit
): Flow<R> = flow { // Note: safe flow is used here, because collector is exposed to transform on each operation
    collect { value ->
        //上游的数据先经过transform处理
        return@collect transform(value)
    }
}
  1. 依然是Flow扩展函数,返回一个新的Flow对象
  2. 新Flow对象重写了flow闭包,该闭包里调用collect收集了原始Flow的数据
  3. 当数据到来后,经过transform处理,而我们自定义的transform闭包里将数据再次发射出去
  4. 最后新返回的flow的collect闭包被调用

上面只是使用了一个transform操作符,若是多个transform操作符,该怎么去分析呢?其实,套路是有迹可循的。
这里涉及到了一种设计模式:装饰者模式
image.png
每调用1个transform操作符就会新生成一个Flow对象,该对象装饰了它的上一个(扩展)对象,如上Flow1装饰原始Flow,Flow2装饰Flow1。

    fun test02() {
        runBlocking {
            flow {
                //上游
                emit("hello world ${Thread.currentThread()}")
            }.transform {
                emit("$it 1")
            }.transform {
                emit("$it 2")
            }.transform {
                emit("$it 3")
            }.collect {
                println("$it")
            }
        }
    }

如上,相信你很快就知道输出结果了。

你可能觉得transform还需要自己发射数据,有点麻烦,map可解君忧。

    fun test03() {
        runBlocking {
            flow {
                //上游
                emit("hello world ${Thread.currentThread()}")
            }.map {
                "$it 1"
            }.collect {
                println("$it")
            }
        }
    }

map内部封装了transform。

过滤操作符

有个需求:对上流的数据进行某种条件的筛选过滤。
有了transform的经验,我们很容易想到定义扩展函数返回新的Flow,并重写collect的闭包,在闭包里进行限制。

public inline fun <T> Flow<T>.filter(crossinline predicate: suspend (T) -> Boolean): Flow<T> = transform { value ->
    //条件满足再发射
    if (predicate(value)) return@transform emit(value)
}

internal inline fun <T, R> Flow<T>.unsafeTransform(
    @BuilderInference crossinline transform: suspend FlowCollector<R>.(value: T) -> Unit
): Flow<R> = unsafeFlow { // Note: unsafe flow is used here, because unsafeTransform is only for internal use
    collect { value ->
        return@collect transform(value)
    }
}

使用方式:

    fun test04() {
        runBlocking {
            flow {
                //上游
                emit("hello world ${Thread.currentThread()}")
                emit("fish")
            }.filter {
                //包含hello字符串才继续往下发送
                it.contains("hello")
            }.collect {
                println("$it")
            }
        }
    }

掌握了以上套路,再去理解其它类似的操作符就很简单了,都是一些简单的变种。

3. 单Flow操作符里的多协程原理以及使用场景

Flow里如何切换协程与线程

上面提到的操作符,如map、filter,相信大家也看出来了:

整个流程的过程没有涉及到其它协程,也没有涉及到其它的线程,是比较单纯也比较容易理解

有个需求:在主线程执行collect操作符,在flow闭包里执行耗时操作。
此时我们就需要flow闭包里的代码在子线程执行。
你可能一下子就说出了答案:使用flowOn操作符。

    fun test05() {
        runBlocking {
            flow {
                //上游
                println("emit ${Thread.currentThread()}")
                emit("hello world")
            }.flowOn(Dispatchers.IO)//flowOn 之前的操作符在新协程里执行
                .collect {
                    println("$it")
                    println("collect ${Thread.currentThread()}")
                }
        }
    }
//打印结果
emit Thread[DefaultDispatcher-worker-1 @coroutine#3,5,main]
hello world
collect Thread[main @coroutine#2,5,main]

可以看出,flow闭包(上游),collect闭包(下游)分别执行在不同的协程以及不同的线程里。
flowOn原理简单来说:

构造了新的协程执行flow闭包,又因为指定了协程分发器为Dispatchers.IO,因此会在子线程里执行flow闭包
原理是基于ChannelFlow

Flow处理背压

有个需求:上游发射数据速度高于下游,如何提升发射效率?
如下:

    fun test06() {
        runBlocking {
            val time = measureTimeMillis {
                flow {
                    //上游
                    println("emit ${Thread.currentThread()}")
                    emit("hello world")
                    delay(1000)
                    emit("hello world2")
                }.collect {
                        delay(2000)
                        println("$it")
                        println("collect ${Thread.currentThread()}")
                    }
            }
            println("use time:$time")
        }
    }
//打印
emit Thread[main @coroutine#2,5,main]
hello world
collect Thread[main @coroutine#2,5,main]
hello world2
collect Thread[main @coroutine#2,5,main]
use time:5024

使用buffer操作符解决背压问题:

    fun test06() {
        runBlocking {
            val time = measureTimeMillis {
                flow {
                    //上游
                    println("emit ${Thread.currentThread()}")
                    emit("hello world")
                    delay(1000)
                    emit("hello world2")
                }.buffer().collect {
                        delay(2000)
                        println("$it")
                        println("collect ${Thread.currentThread()}")
                    }
            }
            println("use time:$time")
        }
    }
//打印结果
emit Thread[main @coroutine#3,5,main]
hello world
collect Thread[main @coroutine#2,5,main]
hello world2
collect Thread[main @coroutine#2,5,main]
use time:4065

可以看出,总耗时减少了。
buffer原理简单来说:

构造了新的协程执行flow闭包,上游数据会发送到Channel 缓冲区里,发送完成继续发送下一条
collect操作符监听缓冲区是否有数据,若有则收集成功
原理是基于ChannelFlow

关于flowOn和buffer更详细的原理请移步:Kotlin Flow 背压和线程切换竟然如此相似

上游覆盖旧数据

有个需求:上游生产速度很快,下游消费速度慢,我们只关心最新数据,旧的数据没价值可以丢掉。
使用conflate操作符处理:

    fun test07() {
        runBlocking {
            flow {
                //上游
                repeat(5) {
                    emit("emit $it")
                    delay(100)
                }
            }.conflate().collect {
                delay(500)
                println("$it")
            }
        }
    }
//打印结果:
emit 0
emit 4

可以看出,中间产生的数据由于下游没有来得及消费,被上游新的数据冲刷掉了。

conflate原理简单来说:

相当于使用了buffer操作符,该buffer只能容纳一个数据,新来的数据将会覆盖旧的数据
原理是基于ChannelFlow

Flow变换取最新值

有个需求:在使用transform处理数据的时候,若是它处理比较慢,当有新的值过来后就取消未处理好的值。
使用transformLatest操作符处理:

    fun test08() {
        runBlocking {
            flow {
                //上游,协程1
                repeat(5) {
                    emit("emit $it")
                }
                println("emit ${Thread.currentThread()}")
            }.transformLatest {
                //协程2
                delay(200)
                emit("$it fish")
            }.collect {
                println("collect ${Thread.currentThread()}")
                println("$it")
            }
        }
    }
打印结果:
emit Thread[main @coroutine#3,5,main]
collect Thread[main @coroutine#2,5,main]
emit 4 fish

可以看出,由于transform处理速度比较慢,上游有新的数据过来后会取消transform里未处理的数据。
查看源码是如何处理的:

override suspend fun flowCollect(collector: FlowCollector<R>) {
    coroutineScope {
        var previousFlow: Job? = null
        //开始收集上游数据
        flow.collect { value ->
            previousFlow?.apply {
                //若是之前的协程还在,则取消
                cancel(ChildCancelledException())
                join()
            }
            //开启协程执行,此处选择不分发新线程
            previousFlow = launch(start = CoroutineStart.UNDISPATCHED) {
                collector.transform(value)
            }
        }
    }
}

transformLatest原理简单来说:

构造新的协程1执行flow闭包,收集到数据后再开启新的协程2,在协程里会调用transformLatest的闭包,最终调用collect的闭包
协程1继续发送数据,若是发现协程2还在运行,则取消协程2
原理是基于ChannelFlow

同理,map也有类似的操作符:

    fun test09() {
        runBlocking {
            flow {
                //上游
                repeat(5) {
                    emit("emit $it")
                }
                println("emit ${Thread.currentThread()}")
            }.mapLatest {
                delay(200)
                "$it fish"
            }.collect {
                println("collect ${Thread.currentThread()}")
                println("$it")
            }
        }
    }
//打印结果
emit Thread[main @coroutine#3,5,main]
collect Thread[main @coroutine#2,5,main]
emit 4 fish

收集最新的数据

有个需求:监听下载进度,UI展示最新进度。
分析:此种场景下,我们只是关注最新的进度,没必要频繁刷新UI,因此使用Flow实现时上游发射太快了可以忽略旧的数据。
使用collectLatest操作符实现:

    fun test014() {
        runBlocking {
            val time = measureTimeMillis {
                val flow1 = flow {
                    repeat(100) {
                        emit(it + 1)
                    }
                }
                flow1.collectLatest {
                    delay(20)
                    println("collect progress $it")
                }
            }
            println("use time:$time")
        }
    }
//打印结果
collect progress 100
use time:169

collectLatest原理简单来说:

开启新协程执行flow闭包
若是collect收集比较慢,下一个数据emit过来后会取消未处理的数据
原理是基于ChannelFlow

4. 多Flow操作符里的多协程原理以及使用场景

很多时候我们不止操作单个Flow,有可能需要结合多个Flow来实现特定的业务场景。

展平流

flatMapConcat

有个需求:请求某个学生的班主任信息,这里涉及到两个接口:

  1. 请求学生信息,使用Flow1表示
  2. 请求该学生的班主任信息,使用Flow2表示
  3. 我们需要先拿到学生的信息,通过信息里带的班主任id去请求班主任信息

分析需求可知:获取学生信息的请求和获取班主任信息的请求是串行的,有前后依赖关系。
使用flatMapConcat操作符实现:

    fun test010() {
        runBlocking {
            val flow1 = flow {
                emit("stuInfo")
            }
            flow1.flatMapConcat {
                //flow2
                flow {
                    emit("$it teachInfo")
                }
            }.collect {
                println("collect $it")
            }
        }
    }
//打印结果:
collect stuInfo teachInfo

从打印结果可以看出:

所谓展平,实际上就是将两个Flow的数据拍平了输出

当然,你也可以请求多个学生的班主任信息:

    fun test011() {
        runBlocking {
            val time = measureTimeMillis {
                val flow1 = flow {
                    println("emit ${Thread.currentThread()}")
                    emit("stuInfo 1")
                    emit("stuInfo 2")
                    emit("stuInfo 3")
                }
                flow1.flatMapConcat {
                    //flow2
                    flow {
                        println("flatMapConcat ${Thread.currentThread()}")
                        emit("$it teachInfo")
                        delay(1000)
                    }
                }.collect {
                    println("collect ${Thread.currentThread()}")
                    println("collect $it")
                }
            }
            println("use time:$time")
        }
    }
//打印结果:
emit Thread[main @coroutine#2,5,main]
flatMapConcat Thread[main @coroutine#2,5,main]
collect Thread[main @coroutine#2,5,main]
collect stuInfo 1 teachInfo
flatMapConcat Thread[main @coroutine#2,5,main]
collect Thread[main @coroutine#2,5,main]
collect stuInfo 2 teachInfo
flatMapConcat Thread[main @coroutine#2,5,main]
collect Thread[main @coroutine#2,5,main]
collect stuInfo 3 teachInfo
use time:3032

flatMapConcat原理简单来说:

flatMapConcat 并没有涉及到多协程,使用了装饰者模式
先将Flow2使用map进行变换,而后将Flow1、Flow2数据发射出来
Concat顾名思义,将两个Flow连接起来

flatMapMerge

有个需求:在flatMapConcat里,先查询了学生1的班主任信息后才会查询学生2的班主任信息,依照此顺序进行查询。现在需要提升效率,同时查询多个多个学生的班主任信息。
使用flatMapMerge操作符实现:

    fun test012() {
        runBlocking {
            val time = measureTimeMillis {
                val flow1 = flow {
                    println("emit ${Thread.currentThread()}")
                    emit("stuInfo 1")
                    emit("stuInfo 2")
                    emit("stuInfo 3")
                }
                flow1.flatMapMerge(4) {
                    //flow2
                    flow {
                        println("flatMapMerge ${Thread.currentThread()}")
                        emit("$it teachInfo")
                        delay(1000)
                    }
                }.collect {
                    println("collect ${Thread.currentThread()}")
                    println("collect $it")
                }
            }
            println("use time:$time")
        }
    }
//打印结果:
flatMapMerge Thread[main @coroutine#6,5,main]
collect Thread[main @coroutine#2,5,main]
collect stuInfo 1 teachInfo
collect Thread[main @coroutine#2,5,main]
collect stuInfo 2 teachInfo
collect Thread[main @coroutine#2,5,main]
collect stuInfo 3 teachInfo
use time:1086

可以看出,flatMapMerge由于是并发执行,整体速度比flatMapConcat快了很多。
flatMapMerge可以指定并发的数量,当指定flatMapMerge(0)时,flatMapMerge退化为flatMapConcat。
关键源码如下:

override suspend fun collectTo(scope: ProducerScope<T>) {
    val semaphore = Semaphore(concurrency)
    val collector = SendingCollector(scope)
    val job: Job? = coroutineContext[Job]
    flow.collect { inner ->
        job?.ensureActive()
        //并发数限制锁
        semaphore.acquire()
        scope.launch {
            //开启新的协程
            try {
                //执行flatMapMerge闭包里的flow
                inner.collect(collector)
            } finally {
                semaphore.release() // Release concurrency permit
            }
        }
    }
}

flatMapMerge原理简单来说:

flow1里的每个学生信息会触发去获取班主任信息flow2
新开了协程去执行flow2的闭包
原理是基于ChannelFlow

flatMapLatest

有个需求:flatMapConcat 是线性执行的,可以使用flatMapMerge提升效率。为了节约资源,在请求班主任信息的时候,若是某个学生的班主任信息没有返回,而下一个学生的班主任信息已经开始请求,则取消上一个没有返回的班主任Flow。
使用flatMapLatest操作符实现:

    fun test013() {
        runBlocking {
            val time = measureTimeMillis {
                val flow1 = flow {
//                    println("emit ${Thread.currentThread()}")
                    emit("stuInfo 1")
                    emit("stuInfo 2")
                    emit("stuInfo 3")
                }
                flow1.flatMapLatest {
                    //flow2
                    flow {
//                        println("flatMapLatest ${Thread.currentThread()}")
                        delay(1000)
                        emit("$it teachInfo")
                    }
                }.collect {
//                    println("collect ${Thread.currentThread()}")
                    println("collect $it")
                }
            }
            println("use time:$time")
        }
    }
//打印结果:
collect stuInfo 3 teachInfo
use time:1105

可以看出,只有学生3的班主任信息打印出来了,并且整体时间都减少了。
flatMapLatest原理简单来说:

和transformLatest很相似
原理是基于ChannelFlow

简单总结一下关于收集最新数据的操作符:

transformLatest、mapLatest、collectLatest、flatMapLatest 四者的核心实现都是ChannelFlowTransformLatest,而它最终继承自:ChannelFlow

组合流

combine

有个需求:查询学生的性别以及选修了某个课程。
分析:涉及到两个需求,查询学生性别与查询选修课程,输出结果是:性别:xx,选修了:xx课程。这俩请求可以同时发出,并没有先后顺序,因此我们没必要使用flatMapXX系列操作符。
使用combine操作符:

    fun test015() {
        runBlocking {
            val time = measureTimeMillis {
                val flow1 = flow {
                    emit("stuSex 1")
                    emit("stuSex 2")
                    emit("stuSex 3")
                }
                val flow2 = flow {
                    emit("stuSubject")
                }
                flow1.combine(flow2) {
                    sex, subject->"$sex-->$subject"
                }.collect {
                    println(it)
                }
            }
            println("use time:$time")
        }
    }
//打印结果:
stuSex 1-->stuSubject
stuSex 2-->stuSubject
stuSex 3-->stuSubject
use time:46

可以看出,flow1的每个emit和flow2的emit关联起来了。
combine操作符有个特点:

短的一方会等待长的一方结束后才结束

看个例子就比较清晰:

    fun test016() {
        runBlocking {
            val time = measureTimeMillis {
                val flow1 = flow {
                    emit("a")
                    emit("b")
                    emit("c")
                    emit("d")
                }
                val flow2 = flow {
                    emit("1")
                    emit("2")
                }
                flow1.combine(flow2) {
                        sex, subject->"$sex-->$subject"
                }.collect {
                    println(it)
                }
            }
            println("use time:$time")
        }
    }
//打印结果
a-->1
b-->2
c-->2
d-->2
use time:45

flow2早就发射到"2"了,会一直等到flow1发射结束。

combine原理简单来说:
image.png

zip

在combine需求的基础上,我们又有个优化:无论是学生性别还是学生课程,只要某个Flow获取结束了就取消Flow。
使用zip操作符:

    fun test017() {
        runBlocking {
            val time = measureTimeMillis {
                val flow1 = flow {
                    emit("a")
                    emit("b")
                    emit("c")
                    emit("d")
                }
                val flow2 = flow {
                    emit("1")
                    emit("2")
                }
                flow1.zip(flow2) {
                        sex, subject->"$sex-->$subject"
                }.collect {
                    println(it)
                }
            }
            println("use time:$time")
        }
    }
//打印结果
a-->1
b-->2
use time:71

可以看出flow2先结束了,并且flow1没发送完成。
zip原理简单来说:
image.png
可以看出,zip的特点:

短的Flow结束,另一个Flow也结束

5. Flow操作符该怎么学?

以上我们由浅入深分别分析了:

  1. 单个Flow操作符原理与使用场景
  2. 单个Flow操作符切换多个协程的原理与使用场景
  3. 多个Flow操作符切换多个协程的原理与使用场景

以上三者是递进关系,第1点比较简单,第2点难度适中。
尤其是第3点比较难以理解,因为涉及到了其它的知识:Channel、ChannelFlow、多协程、线程切换等。
在之前的文章中有提到过:ChannelFlow是Flow复杂操作符的基础,想要掌握复杂操作符的原理需要明白ChannelFlow的运行机制,有兴趣可移步:当,Kotlin Flow与Channel相逢

建议Flow操作符学习步骤:

  1. 先会使用简单的操作符filter、map等
  2. 再学会使用flowOn、buffer、callbackFlow等操作符
  3. 进而使用flatMapXXX以及combine、zip等操作符
  4. 最后可以看看其实现原理,达到举一反三应用到实际需求里

Flow操作符的闭坑指南:

  1. 涉及到多协程的操作符,需要关注其执行的线程环境
  2. 涉及到多协程的操作符,需要关注协程的生命周期

说实话,Flow操作符要掌握好挺难的,它几乎涉及了协程所有的知识点,也是协程实际应用的精华。这篇是我在协程系列里花费时间最长的文章了(也许也是最后一篇了),即使自己弄明白了,怎样把它很自然地递进引出也是个有挑战的事。
若你能够在本篇的分析中得到一点启发,那说明我的分享是有价值的。
由于篇幅关系,一些操作符debounce、sample等并没有分析,也没有再贴flatMapXXX的源码细节(这部分之前的文章都有分析过),若你有需要可以给我留言评论。

本文基于Kotlin 1.6.1,覆盖所有Flow操作符的demo

您若喜欢,请点赞、关注、收藏,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Kotlin

1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17、Android Jetpack 前置基础系列
18、Android Jetpack 易学易懂系列
19、Kotlin 轻松入门系列
20、Kotlin 协程系列全面解读

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/470455.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1970-01-01是什么?为什么计算机起始时间是1970年1月1日

我们是不是也曾见到过或者听说过1970-01-01,而且它好像常常存在于计算机行业&#xff0c;非常常见&#xff0c;从事IT行业的兄弟应该都接触过&#xff0c;那么到底为什么是1970-01-01&#xff0c;而不是其他呢&#xff1f; 其实&#xff0c;1970-01-01对于开发者来说都是不陌生…

带你了解现在的LED显示屏技术

随着LED显示屏技术的空前繁荣&#xff0c;LED显示屏产品备受关注&#xff0c;广泛应用于商业广告、实况播映、交通诱导、舞台演绎等领域&#xff0c;发展至今。你了解十大中国LED显示屏制造商吗&#xff1f; LED显示屏技术已经得到了长足的发展&#xff0c;现在的LED显示屏技术…

智融合·共未来丨智合同携手百融云创打造合同智能化应用服务平台

人工智能技术是当今社会的热议话题之一。近年来&#xff0c;众多企业在人工智能领域持续布局&#xff0c;相关技术已在社会生产各环节极大地提高了生产效率。如果把过去信息技术产业的发展比喻为“手工时代”&#xff0c;那么人工智能技术的出现则将把信息技术产业推向“自动化…

数据库迁移 | DBMotion v23.04 支持异地多活

Squids DBMotion新版本支持异地多中心双活同步了。异地多活支持业务在多个数据中心同时操作数据库&#xff0c;能极大地提升高可用性、容错性和用户体验。其中最关键的技术&#xff0c;无疑是数据同步、同步防环和数据冲突解决。Squids DBMotion通过复制数据打标和预置冲突策略…

生产凭证补传操作步骤

凭证包文件包含&#xff1a;身份证正面、身份证背面、拍照图片、身份证头像图片、联网核查文件、签名文件、签名轨迹文本、凭证json文件。 一、证件信息重复或者模糊。 1.提供业务流水号、证件信息。 2.将证件信息通过安全U盘导入生产机。 3.根据业务流水号前8位设备号及业务流…

电信及互联网行业数据安全内控审计建设实践 | 盾见

文|龚磊 伴随数据安全“五法一典”出齐&#xff0c;2021年成为我国数据安全元年。各地、各行业不断加快数据安全政策体系的完善与落地执行。政企机构不断强化数据安全建设&#xff0c;共同助力网络安全行业高景气度维系。 2021年6月&#xff0c;网络安全等保测评报告模板新版发…

计算机网络学习02

1、TCP 与 UDP 的区别&#xff1f; 是否面向连接 &#xff1a; UDP 在传送数据之前不需要先建立连接。而 TCP 提供面向连接的服务&#xff0c;在传送数据之前必须先建立连接&#xff0c;数据传送结束后要释放连接。是否是可靠传输&#xff1a; 远地主机在收到 UDP 报文后&…

《2023中国企业数智化转型升级服务全景图/产业图谱1.0版》重磅发布

‍ 数据猿出品 本次“数据猿2023年度三大媒体策划活动——《2023企业数智化转型升级服务全景图/产业图谱1.0版》”的发布&#xff0c;是数据猿在2022年3.0版本的基础上&#xff0c;迭代升级的2023开年的第一个版本。本年度下一次版本迭代将于2023年8月底发布2023年2.0版&#x…

Java学习18(Java内存区域详解)

对于 Java 程序员来说&#xff0c;在虚拟机自动内存管理机制下&#xff0c;不再需要像 C/C程序开发程序员这样为每一个 new 操作去写对应的 delete/free 操作&#xff0c;不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机&#xff0c;一…

前端web3入门脚本二:初探dex,在dex完成一笔swap

前言 现在市面上大多数去中心化交易所&#xff08;简称dex&#xff09;都是fork的uniswap的代码&#xff0c;名气比较大的如eth上的sushi 以及 bsc上的pancake。博主这里说的都是V2&#xff0c;uniswapV3在这里不做讨论。那么知道了他们的代码都是来自同一父亲之后&#xff0c;…

Python每日一练(20230428)

目录 1. 最长有效括号 &#x1f31f;&#x1f31f;&#x1f31f; 2. 矩阵中的最长递增路径 &#x1f31f;&#x1f31f;&#x1f31f; 3. 回文链表 &#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练…

考研操作系统2.3节 同步与互斥(这节有必要多复习)

2.2 实现缓冲区互斥的基本方法 软件实现方法 硬件实现方法 2.3.3 互斥锁 下面用互斥锁解决经典同步问题 2.3.4 信号量 经典同步问题&#xff08;做大题再看&#xff09; 2.3节选择题 皮特森算法&#xff1a;flag实现互斥&#xff0c;turn避免饥饿&#xff0c;选D x1 2 2 1 …

C/C++每日一练(20230428) 二叉树专场(6)

目录 1. 不同的二叉搜索树 &#x1f31f; 2. 二叉树的锯齿形层序遍历 &#x1f31f;&#x1f31f; 3. 二叉树的右视图 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每…

如何做好采购计划和库存管理?

“销售计划不专业且不稳定”“准确性低” “目前只按照过往销量和采购周期做安全库存&#xff0c;但欠货和滞销依然严重” 题主的问题其实蛮有代表性的&#xff0c; 也是传统采购和库存管理常常面临的问题&#xff1a; ① 前后方协作困难 采购/销售/财务工作相互独立&#x…

【软件测试】测试分类

文章目录 &#x1f337; 1. 按测试对像划分⭐️ &#xff08;1&#xff09;界面测试⭐️ &#xff08;2&#xff09;可靠性测试⭐️ &#xff08;3&#xff09;容错性测试⭐️ &#xff08;4&#xff09;文档测试⭐️ &#xff08;5&#xff09;兼容性测试⭐️ &#xff08;6&a…

Word论文自动化排版

论文的总体结构包含了标题、正文、子标题、图形、公式、算法、表格、参考文献等子结构。在利用Word进行论文排版时常常遇到这些困难&#xff1a;&#xff08;1&#xff09;论文的各子结构的格式非常难以做到统一&#xff0c;而且一旦某一子结构的格式需要修改&#xff0c;则需要…

量子计算的崛起:量子安全技术将成为下一代网络安全的基石?

第一章&#xff1a;引言 量子计算作为一种新型计算模式&#xff0c;与传统计算机相比具有许多优势。传统计算机使用的是二进制系统&#xff0c;即只有0和1两种状态&#xff0c;而量子计算机利用量子比特&#xff08;qubit&#xff09;的特殊性质进行计算。量子比特可以同时处于…

Mysql数据库的备份恢复

最近正在做一个异地数据的定期同步汇总工作&#xff0c;涉及到的数据库主要是Mysql数据库&#xff0c;用于存储现场的一些IOT采集的实时数据&#xff0c;所以做了以下备份恢复测试&#xff0c;现场和总部网络可定期联通&#xff0c;但速度有限&#xff0c;因此计划采用备份恢复…

11【Sass语法介绍-导入】

1.前言 在 CSS 中我们可以通过 import 来导入一个样式文件&#xff0c;Sass 扩展了 CSS 的 import 规则&#xff0c;使得可以导入 CSS 后缀的样式文件和 Scss 后缀的样式文件&#xff0c;并且提供了对 mixin 、函数和变量的访问。 与 CSS 的 import 不同的是&#xff0c; CSS…

aac音频怎么转mp3,这几个方法很简便

对于aac来说&#xff0c;其是一种高级音频编码&#xff0c;也是专门为声音数据设计的文件压缩格式。通常来说&#xff0c;aac与mp3有一些不同。aac使用了全新的算法进行编码的&#xff0c;其整体的效率较mp3更高一些。同时&#xff0c;aac格式的音质较好一些。但是&#xff0c;…