Kotlin Flow响应式编程,StateFlow和SharedFlow

news2025/1/21 11:33:01

本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。

大家好,今天是Kotlin Flow响应式编程三部曲的最后一篇。

其实回想一下我写这个Kotlin Flow三部曲的初衷,主要还是因为我自己想学这方面的知识。

虽然Kotlin我已经学了很多年了,但是对于Flow我却一直没怎么接触过。可能是因为工作当中一直用不上吧,我现在工作的主语言依然还是Java。

而我一直都是这个样子,写博客基本上不是为了谁而写的,大部分都只是因为我自己想学。但是学了不用很快又会忘记,所以经常就会通过文章的形式把它记录下来,算是助人又助己了。

而Kotlin Flow在可预见的时间里,我也上不太可能能在工作当中用得到,所以这个系列也就基本是属于我个人的学习笔记了。

今天的这一篇文章,我准备讲一讲StateFlow和SharedFlow的知识。内容和前面的两篇文章有一定的承接关系,所以如果你还没有看过前面两篇文章的话,建议先去参考 Kotlin Flow响应式编程,基础知识入门 和 Kotlin Flow响应式编程,操作符函数进阶 。


Flow的生命周期管理

首先,我们接着在 Kotlin Flow响应式编程,基础知识入门 这篇文章中编写的计时器例子来继续学习。

之前在编写这个例子的时候我有提到过,首要目的就是要让它能跑起来,以至于在一些细节方面的写法甚至都错误的。

那么今天我们就要来看一看,之前的计时器到底错在哪里了。

如果只是直观地从界面上看,好像一切都是可以正常工作的。但是,假如我们再添加一些日志来进行观察的话,问题就会浮出水面了。

那么我们在MainActivity中添加一些日志,如下所示:

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            lifecycleScope.launch {
                mainViewModel.timeFlow.collect { time ->
                    textView.text = time.toString()
                    Log.d("FlowTest", "Update time $time in UI.")
                }
            }
        }
    }
}

这里每当计时器更新一次的时候,我们同时打印一行日志来方便进行进行观察。

另外,MainViewModel中的代码这里我也贴上吧,虽然它是完全没有改动的:

class MainViewModel : ViewModel() {

    val timeFlow = flow {
        var time = 0
        while (true) {
            emit(time)
            delay(1000)
            time++
        }
    }
    
}

运行程序看一看效果:

在这里插入图片描述

一开始的时候,界面上计时器每更新一次,同时控制台也会打印一行日志,这还算是正常。

可接下来,当我们按下Home键回到桌面后,控制台的日志依然会持续打印。好家伙,这还得了?

这说明,即使我们的程序已经不在前台了,UI更新依然在持续进行当中。这是非常危险的事情,因为在非前台的情况下更新UI,某些场景下是会导致程序崩溃的。

也就是说,我们并没有很好地管理Flow的生命周期,它没有与Activity的生命周期同步,而是始终在接收着Flow上游发送过来的数据。

那这个问题要怎么解决呢?lifecycleScope除了launch函数可以用于启动一个协程之外,还有几个与Activity生命周期关联的launch函数可以使用。比如说,launchWhenStarted函数就是用于保证只有在Activity处于Started状态的情况下,协程中的代码才会执行。

那么我们用launchWhenStarted函数来改造一下上述代码:

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            lifecycleScope.launchWhenStarted {
                mainViewModel.timeFlow.collect { time ->
                    textView.text = time.toString()
                    Log.d("FlowTest", "Update time $time in UI.")
                }
            }
        }
    }
}

变动就只有这一处,我们使用launchWhenStarted函数替换了之前的launch函数,其余部分都是保持不变的。

现在重新运行一下程序,效果如下图所示:

在这里插入图片描述

可以看到,这次当我们将程序切到后台的时候,日志就会停止打印,说明刚才的改动生效了。而当我们将程序重新切回到前台时,计时器会接着刚才切出去的时间继续计时。

那么现在程序终于一切正常了吗?

很遗憾,还没有。

还有什么问题呢?上图其实已经将问题显现出来了。

现在的主要问题在于,当我们将程序从后台切回到前台时,计时器会接着之前切出去的时间继续计时。

这说明了什么?说明程序在后台的时候,Flow的管道中一直会暂存着一些的旧数据,这些数据不仅可能已经失去了时效性,而且还会造成一些内存上的问题。

要知道,我们使用flow构建函数构建出的Flow是属于冷流,也就是在没有任何接受端的情况下,Flow是不会工作的。但是上述例子当中,即使程序切到了后台,Flow依然没有中止,还是为它保留了过期数据,这就是一种内存上的浪费。

当然,我们这个例子非常简单,在实际项目中一个Flow可能又是由多个上游Flow合并而成的。在这种情况下,如果程序进入了后台,却仍有大量Flow依然处于活跃的状态,那么内存问题会变得更加严重。

为此,Google推荐我们使用repeatOnLifecycle函数来解决这个问题,写法如下:

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    mainViewModel.timeFlow.collect { time ->
                        textView.text = time.toString()
                        Log.d("FlowTest", "Update time $time in UI.")
                    }
                }
            }
        }
    }
}

repeatOnLifecycle函数接受一个Lifecycle.State参数,这里我们传入Lifecycle.State.STARTED,同样表示只有在Activity处于Started状态的情况下,协程中的代码才会执行。

使用repeatOnLifecycle函数改造之后,运行效果会完全不一样,我们来看一下:

在这里插入图片描述

可以看到,当我们将程序切到后台之后,日志打印就停止了。当我们将程序重新切回前台时,计时器会从零开始重新计时。

这说明什么?说明Flow在程序进入后台之后就完全停止了,不会保留任何数据。程序回到前台之后Flow又从头开始工作,所以才会从零开始计时。

正确使用repeatOnLifecycle函数,这样才能让我们的程序在使用Flow的时候更加安全。


StateFlow的基本用法

即使你从来没有使用过Flow,但是我相信你一定使用过LiveData。

而如果谈到在Flow的所有概念当中,最最接近LiveData的,那毫无疑问就是StateFlow了。

可以说,StateFlow的基本用法甚至能够做到与LiveData完全一致。对于广大Android开发者来说,我认为这是一个非常容易上手的组件。

下面我们就通过一个例子来学习一下StateFlow的基本用法。例子非常简单,就是复用了刚才计时器的例子,并稍微进行了一下改造。

首先是对MainViewModel的改造,代码如下所示:

class MainViewModel : ViewModel() {

    private val _stateFlow = MutableStateFlow(0)

    val stateFlow = _stateFlow.asStateFlow()

    fun startTimer() {
        val timer = Timer()
        timer.scheduleAtFixedRate(object : TimerTask() {
            override fun run() {
                _stateFlow.value += 1
            }
        }, 0, 1000)
    }
}

可以看到,这里我们采取了和基础知识入门篇完全不一样的计时器实现策略。

之前我们是借助Flow和协程的延迟机制来实现计时器效果的,而这里则改成了借助Java的Timer类来实现。

现在,只要调用了startTimer()函数,每隔一秒钟Java的Timer定时器都会执行一次。那么执行了要干什么呢?这就非常关键了,我们每次都给StateFlow的value值加1 。

你会发现,这个例子中展示的StateFlow的用法几乎和LiveData是完全一致。同样都是通过给value变量赋值来更新数据,甚至同样都是创建一个Mutable的private版本来进行内部操作(一个叫MutableStateFlow,一个叫MutableLiveData),再转换一个public的外部版本进行数据观察(一个叫StateFlow,一个叫LiveData)。

如此来看,在MainViewModel层面确实是非常好理解的。

接下来看一下MainActivity中的代码改造:

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            mainViewModel.startTimer()
        }
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mainViewModel.stateFlow.collect {
                    textView.text = it.toString()
                }
            }
        }
    }
}

当点击按钮时,我们会调用MainViewModel中的startTimer()函数开启定时器。

然后,这里通过lifecycleScope启动了一个协程作用域,并开始对我们刚才定义的StateFlow进行监听。上述代码中的collect函数相当于LiveData中的observe函数。

StateFlow的基本用法就是这样了,现在让我们来运行一下程序吧:

在这里插入图片描述

看上去计时器已经可以正常工作了,非常开心。

StateFlow其中一个重要的价值就是它和LiveData的用法保持了高度一致性。如果你的项目之前使用的是LiveData,那么终于可以放宽了心,零成本地迁移到Flow上了吧?


StateFlow的高级用法

虽说我们使用StateFlow改造的计时器已经可以成功运行了,但是有没有觉得刚才的写法有点太过于传统了,看着非常得不响应式(毕竟用法和LiveData完全一致)。

实际上,StateFlow也有更加响应式的用法,借助stateIn函数,可以将其他的Flow转换成StateFlow。

不过,为了能够更好地讲解stateIn函数,我们还需要对之前的例子进行一下改造。

首先将MainViewModel中的代码还原到最初版本:

class MainViewModel : ViewModel() {

    val timeFlow = flow {
        var time = 0
        while (true) {
            emit(time)
            delay(1000)
            time++
        }
    }
    
}

然后修改MainActivity中的代码:

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mainViewModel.timeFlow.collect { time ->
                    textView.text = time.toString()
                }
            }
        }
    }
}

这里我们移除了对Button点击事件的监听,而是在onCreate函数中直接让计时器就开始工作。

为什么要做这样的修改呢?

因为这会暴露出我们之前代码中隐藏的另外一个问题,观察如下效果图:

在这里插入图片描述

可以看到,原来除了程序进入后台之外,手机发生横竖屏切换也会让计时器重新开始计时。

出现这个情况的原因是,手机横竖屏切换会导致Activity重新创建,重新创建就会使得timeFlow重新被collect,而冷流每次被collect都是要重新执行的。

但这并不是我们想看到的现象,因为横竖屏切换是很迅速的事情,在这种情况下我们没必要让所有的Flow都停止工作再重新启动。

那么该怎么解决呢?现在终于可以引入stateIn函数了,先上代码,我再进行讲解。修改如下:

class MainViewModel : ViewModel() {

    private val timeFlow = flow {
        var time = 0
        while (true) {
            emit(time)
            delay(1000)
            time++
        }
    }

    val stateFlow =
        timeFlow.stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(5000), 
            0
        )
}

前面已经介绍了,stateIn函数可以将其他的Flow转换成StateFlow。那么这里,我们就是将之前的timeFlow转换成了StateFlow。

stateIn函数接收3个参数,其中第1个参数是作用域,传入viewModelScope即可。第3个参数是初始值,计时器的初始值传入0即可。

而第2个参数则是最有意思的了。刚才有说过,当手机横竖屏切换的时候,我们不希望Flow停止工作。但是再之前又提到了,当程序切到后台时,我们希望Flow停止工作。

这该怎么区分分别是哪种场景呢?

Google给出的方案是使用超时机制来区分。

因为横竖屏切换通常很快就能完成,这里我们通过stateIn函数的第2个参数指定了一个5秒的超时时长,那么只要在5秒钟内横竖屏切换完成了,Flow就不会停止工作。

反过来讲,这也使得程序切到后台之后,如果5秒钟之内再回到前台,那么Flow也不会停止工作。但是如果切到后台超过了5秒钟,Flow就会全部停止了。

这点开销还是完全可以接受的。

好的,接下来我们在MainActivity中改成对StateFlow进行collect,从而完成这个例子吧:

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mainViewModel.stateFlow.collect { time ->
                    textView.text = time.toString()
                }
            }
        }
    }
}

现在重新运行一下程序看一看效果:

在这里插入图片描述

可以看到,现在手机横竖屏切换计时器依然是可以正常计时的,说明关联的Flow也都在继续工作,符合我们的预期。

到这里,StateFlow的相关内容基本就都讲完了。接下来还有今天的最后一块主题,SharedFlow。


SharedFlow

要想轻松理解SharedFlow,首先我们得要先理解粘性这个概念。

如果你接触过EventBus,应该对粘性不会感到陌生吧?

这是一个响应式编程中专有的概念。响应式编程是一种发送者和观察者配合工作的编程模式,由发送者发出数据消息,观察者接收到了消息之后进行逻辑处理。

普通场景下,这种发送者和观察者的工作模式还是很好理解的。但是,如果在观察者还没有开始工作的情况下,发送者就已经先将消息发出来了,稍后观察者才开始工作,那么此时观察者还应该收到刚才发出的那条消息吗?

不管你觉得是应该还是不应该,这都不重要。这里我抛出这个问题是为了引出粘性的定义。如果此时观察者还能收到消息,那么这种行为就叫做粘性。而如果此时观察者收不到之前的消息,那么这种行为就叫做非粘性。

EventBus允许我们在使用的时候通过配置指定它是粘性的还是非粘性的。而LiveData则不允许我们进行指定,它的行为永远都是粘性的。

刚才我们也说过,StateFlow和LiveData具有高度一致性,因此可想而知,StateFlow也是粘性的。

怎么证明呢?通过一个非常简单的例子即可证明。

修改MainViewModel中的代码,如下所示:

class MainViewModel : ViewModel() {

    private val _clickCountFlow = MutableStateFlow(0)

    val clickCountFlow = _clickCountFlow.asStateFlow()

    fun increaseClickCount() {
        _clickCountFlow.value += 1
    }
}

这里我们使用了一个名为clickCountFlow的StateFlow来进行简单的计数功能。然后定义了一个increaseClickCount()函数,用于将计数值加1。

接下来修改MainActivity中的代码:

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text_view)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            mainViewModel.increaseClickCount()
        }
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mainViewModel.clickCountFlow.collect { time ->
                    textView.text = time.toString()
                }
            }
        }
    }
}

可以看到,每当点击一次按钮时,我们都调用increaseClickCount()函数来让计数值加1。

另外就是使用前面学习过的写法,对clickCountFlow进行collect。

现在运行一下程序,效果如下图所示:

在这里插入图片描述

这里需要关注的重点是,当手机发生横竖屏切换时,计数器的数字仍然会保留在屏幕上。

你觉得这很正常?其实则不然。因为当手机发生横竖屏切换时,整个Activity都重新创建了,则此调用clickCountFlow的collect函数之后,并没有什么新的数据发送过来,但我们仍然能在界面上显示之前计数器的数字。

由此说明,StateFlow确实是粘性的。

粘性特性在绝大多数场景下都非常好使,这也是为什么LiveData和StateFlow都设计成粘性的原因。

但确实在一些场景下,粘性又会导致出现某些问题。而LiveData并没有提供非粘性的版本,所以网上甚至还出现了一些用Hook技术来让LiveData变成非粘性的方案。

相比之下,Flow则人性化了很多。想要使用非粘性的StateFlow版本?那么用SharedFlow就可以了。

在开始介绍SharedFlow的用法之前,我们先来看一下到底是什么样的场景不适用于粘性特性。

假设我们现在正在开发一个登录功能,点击按钮开始执行登录操作,登录成功之后弹出一个Toast告知用户。

首先修改MainViewModel中的代码,如下所示:

class MainViewModel : ViewModel() {

    private val _loginFlow = MutableStateFlow("")

    val loginFlow = _loginFlow.asStateFlow()

    fun startLogin() {
        // Handle login logic here.
        _loginFlow.value = "Login Success"
    }
}

这里我们定义了一个startLogin函数,当调用这个函数时开始执行登录逻辑操作,登录成功之后向loginFlow进行赋值来告知用户登录成功了。

接着修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            mainViewModel.startLogin()
        }
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mainViewModel.loginFlow.collect {
                    if (it.isNotBlank()) {
                        Toast.makeText(this@MainActivity, it, Toast.LENGTH_SHORT).show()
                    }
                }
            }
        }
    }
}

这里当点击按钮时,我们调用MainViewModel中的startLogin函数开始执行登录。

然后在对loginFlow进行collect的地方,通过弹出一个Toast来告知用户登录已经成功了。

现在运行一下程序,效果如下图所示:

在这里插入图片描述

可以看到,当点击按钮开始执行登录时,弹出了一个Login Success的Toast,说明登录成功了。到这里都还挺正常的。

接下来当我们尝试去旋转一下屏幕,此时又会弹出一个Login Success的Toast,这就不对劲了。

而这,就是粘性所导致的问题。

现在我们明白了在某些场景下粘性特性是不太适用的,接下来我们就学习一下如何使用SharedFlow这个非粘性的版本来解决这个问题。

修改MainViewModel中的代码,如下所示:

class MainViewModel : ViewModel() {

    private val _loginFlow = MutableSharedFlow<String>()

    val loginFlow = _loginFlow.asSharedFlow()

    fun startLogin() {
        // Handle login logic here.
        viewModelScope.launch {
            _loginFlow.emit("Login Success")
        }
    }
}

SharedFlow和StateFlow的用法还是略有不同的。

首先,MutableSharedFlow是不需要传入初始值参数的。因为非粘性的特性,它本身就不要求观察者在观察的那一刻就能收到消息,所以也没有传入初始值的必要。

另外就是,SharedFlow无法像StateFlow那样通过给value变量赋值来发送消息,而是只能像传统Flow那样调用emit函数。而emit函数又是一个挂起函数,所以这里需要调用viewModelScope的launch函数启动一个协程,然后再发送消息。

总体改动就是这么多,MainActivity中的代码是不需要做修改的,现在让我们重新运行一下程序吧:

在这里插入图片描述

可以看到,这次当我们再旋转一下屏幕,不会再像刚才那样又弹出一次Toast了,说明SharedFlow的改动已经生效了。

当然,其实SharedFlow的用法还远不止这些,我们可以通过一些参数的配置来让SharedFlow在有观察者开始工作之前缓存一定数量的消息,甚至还可以让SharedFlow模拟出StateFlow的效果。

但是我觉得这些配置会让SharedFlow更难理解,就不打算讲了。还是让它们之间的区别更纯粹一些,通过粘性和非粘性的需求来选择你所需要的那个版本即可。

好了,到这里,Kotlin Flow三部曲全剧终。

虽不敢说通过这三篇文章你就能成为Flow大神了,但是相信这些知识已经足够你解决工作中遇到了绝大多数问题了。


如果想要学习Kotlin和最新的Android知识,可以参考我的新书 《第一行代码 第3版》,点击此处查看详情。

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

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

相关文章

基于瑞芯微平台cif接口dvp相机的视频接入(ov2640、rv1126为例)

基于瑞芯微平台cif接口dvp相机的视频接入&#xff08;ov2640、rv1126为例&#xff09;名词定义视频格式sensor与ispI2CXCLK行场同步信号DATA抓图名词定义 CIF&#xff0c;指RK芯片中的VIP模块&#xff0c;用以接收Sensor数据并保存到Memory中&#xff0c;仅转存数据&#xff0c…

Komo 综合资产收集和漏洞扫描工具

前言 因工作中的需要&#xff0c;开发了这款综合资产收集和漏洞扫描工具&#xff0c;方便在工作中各方面的收集资产和漏洞扫描&#xff0c;同时也可用于挖洞。 Komo已经在工作中辅助我挖到过一些漏洞&#xff0c;同时轻便了我资产收集的过程。 Komo is a comprehensive asset c…

【图像处理OpenCV(C++版)】——3.3 几何变换之极坐标变换

前言&#xff1a; &#x1f60a;&#x1f60a;&#x1f60a;欢迎来到本博客&#x1f60a;&#x1f60a;&#x1f60a; &#x1f31f;&#x1f31f;&#x1f31f; 本专栏主要结合OpenCV和C来实现一些基本的图像处理算法并详细解释各参数含义&#xff0c;适用于平时学习、工作快…

MATLAB | 如何从热图中提取数据

这期做了个可能有用的小工具&#xff0c;一般论文中热图很少给出数据&#xff0c;于是就想写个小工具通过热图上的颜色估计出数据值来&#xff0c;目前写了个初版的工具分享给大家&#xff01; 工具函数 由于只是初版&#xff0c;要手动改的地方还是不少的&#xff0c;要设置…

PHP多商户AI智能在线客服系统源码 机器人自动回复 即时通讯聊天系统源码

一套智能在线客服系统源码 多商户网页客服系统源码 支持二十种国际语言 带机器人自动回复。 框架&#xff1a;Thinkphp5workerman&#xff0c; 环境&#xff1a;nginxphp7.3mysql5.6 支持H5公众号APP小程序 了解更多可私信我&#xff01; 系统功能&#xff1a; 1、支持国际…

编写程序时调用第三方程序时使用的是相对路径而不是绝对路径会造成什么严重后果(Windows Linux)

简介 在编写程序时&#xff0c;有很多人调用第三方程序使用的是相对路径&#xff0c;而不是绝对路径&#xff0c;如下&#xff1a; #!/bin/python3import osos.system("whoami") #调用whoami程序&#xff0c;查看当前用户名#!/bin/bashfind / -name "hellowor…

day10|239. 滑动窗口最大值、347.前 K 个高频元素

239. 滑动窗口最大值 给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值 。 示例 1&#xff1a; 输入&#xff1a;nums [1,3…

如何修改视频MD5的格式?这些方法值得你收藏

MD5实际上是计算机安全领域中广泛使用的一种散列函数&#xff0c;可以用来保护消息的完整性&#xff0c;简单来说就是类似于我们的指纹&#xff0c;可以说MD5是每个文件的“数字指纹”。比如&#xff1a;我们在平台上传一些热门视频&#xff0c;平台会自动识别视频的MD5值&…

嵌入式 LINUX 驱动开发 day01 第一个内核模块程序 多文件编译为一个程序, 内核模块参数, 内核模块依赖

1.第一个内核模块程序 ( 记得配置自己的交叉编译的工具,) 首先两个文件 vser.c Makefile (记得大写的M) vser.c #include <linux/init.h> //内核初始化头文件 #include <linux/module.h> //内核模块文件 #include <linux/kernel.h> //&…

Java基础算法每日5道详解(6)

112. Path Sum 路径总和 Given the root of a binary tree and an integer targetSum, return true if the tree has a root-to-leaf path such that adding up all the values along the path equals targetSum. A leaf is a node with no children. Example 1: Input: ro…

html+css实现一个响应式管理平台架构模板

文本将会带你使用htmlcss实现一个响应式的管理平台架构模板&#xff0c;目前来说市面上的管理平台架构模板大同小异&#xff0c;文本的知识点都会符合场景所需。 目录 1、管理平台的架构内容 2、顶部的布局 3、下半部分布局 4、左侧菜单区域实现 5、右侧主体区域实现 …

前端重新部署如何通知用户刷新网页?

我把我掘金的文章同步一份给CSDN 1.目标场景 有时候上完线&#xff0c;用户还停留在老的页面&#xff0c;用户不知道网页重新部署了&#xff0c;跳转页面的时候有时候js连接hash变了导致报错跳不过去&#xff0c;并且用户体验不到新功能。 2.思考解决方案 如何去解决这个问…

顶象助力绿球金科打造App低碳出行场景

“低碳出行”、“碳中和”、“碳惠普”正在成为近几年的科技热词之一。 自2020年9月&#xff0c;中国向世界许下“力争2030年前实现碳达峰&#xff0c;2060年前实现碳中和”的承诺以来&#xff0c;一场围绕绿色节能、低碳减排的变革正在席卷各行各业。 “碳中和”已经成为时代…

如何让SCI期刊审稿人,理解你的文章? - 易智编译EaseEditing

首先需要对论文进行全文润色 对于发表论文来说&#xff0c;进行润色是必须的&#xff0c;正因为SCI论文翻译要求高难度大&#xff0c;无论笔译还是口译都一定要有过硬的基本功&#xff0c;知识面要足够宽广&#xff0c;专业综合能力要求高。 所以当一篇论文的整体结构不到位&…

凯恩帝机床联网

一、设备信息确认 1、确认型号 数控面板拍照确认&#xff1a; 此系统为&#xff1a;K1TCi 注&#xff1a;凡是系统中带i的&#xff0c;基本上都含网口。 2、确认通讯接口 网口常见位置&#xff0c;XS92&#xff08;丝印标号&#xff09;&#xff0c;可通过这个确认&#x…

PnetLab模拟器安装锐捷镜像

安装准备&#xff1a; 1.安装完成pnetlab&#xff0c;这里不过多叙述&#xff1b; 2.在锐捷的网站下载好模拟器镜像&#xff08;目前只支持Switch和Router&#xff09;&#xff0c;下载地址&#xff1a;https://www.ruijie.com.cn/fw/wd/88899/ 官网下载后的内容包括下面几个…

Linux umount报错:device is busy

执行nfs卸载命令umount /mnt&#xff0c;报错target is busy. 或device is busy可以按以下步骤检查&#xff1a;退出要卸载挂载的目录&#xff0c;再执行卸载挂载cd ../umount /mnt找出占用目录的端口&#xff0c;kill端口fuser -m /mnt/kill -9 端口umount /mnt停止nfs服务&am…

计算机基础——操作系统

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.操作系统 1.操作系统简介 2.操作系统的主要功能 &#xff08;1&#xff…

【Kotlin】字符串操作 ② ( 字符串替换函数 replace | 字符串比较操作符 == 和 === | 字符串遍历 forEach )

文章目录一、字符串替换函数 replace二、字符串比较操作符 和 三、字符串遍历 forEach一、字符串替换函数 replace 字符串替换函数 replace 函数原型如下 : /*** 返回一个新字符串&#xff0c;通过替换此字符序列中匹配给定正则表达式的每个子字符串获得* 用给定的[替换]。**…

一个芯片工程师的ADC学习笔记 (二)

众所周知&#xff0c;ADC主要用于对模拟信号进行数字采集&#xff0c;以进行数据处理。我们周围的信号一般都是不断变化的模拟量&#xff0c;如光、温度、速度、压力、声音等。然而&#xff0c;我们大多数人都使用数字设备。如果我们想方便地使用和处理信息&#xff0c;就需要将…