狂飙吧,Lifecycle与协程、Flow的化学反应

news2024/11/26 0:36:47

前言

协程系列文章:

  • 一个小故事讲明白进程、线程、Kotlin 协程到底啥关系?
  • 少年,你可知 Kotlin 协程最初的样子?
  • 讲真,Kotlin 协程的挂起/恢复没那么神秘(故事篇)
  • 讲真,Kotlin 协程的挂起/恢复没那么神秘(原理篇)
  • Kotlin 协程调度切换线程是时候解开真相了
  • Kotlin 协程之线程池探索之旅(与Java线程池PK)
  • Kotlin 协程之取消与异常处理探索之旅(上)
  • Kotlin 协程之取消与异常处理探索之旅(下)
  • 来,跟我一起撸Kotlin runBlocking/launch/join/async/delay 原理&使用
  • 继续来,同我一起撸Kotlin Channel 深水区
  • Kotlin 协程 Select:看我如何多路复用
  • Kotlin Sequence 是时候派上用场了
  • Kotlin Flow 背压和线程切换竟然如此相似
  • Kotlin Flow啊,你将流向何方?
  • Kotlin SharedFlow&StateFlow 热流到底有多热?

原本上篇已经结束协程系列了,后面有小伙伴建议可以再讲讲实际的使用,感觉停不下来了,再用几篇收尾吧。我们知道Android开发绕不开的一个重要课题即是生命周期 ,引入了协程后两者该怎么配合呢?
通过本篇文章,你将了解到:

  1. 生命周期的前世今生
  2. Activity与协程的结合
  3. ViewModel与协程的配合
  4. Application创建全局的协程作用域
  5. Flow、协程、生命周期的三角关系

1. 生命周期的前世今生

生命周期简述

现在的系统设计更聚焦于UI和数据的分离,当前的UI展示需要哪些数据的支持,在什么时候需要展示这些数据,这些都需要开发者自己去控制。若控制不得当,可能会出现内存泄漏、资源浪费等现象。
Android提供了四大组件,其中Activity是用来展示UI的,它的创建到销毁即是它的一个完整生命周期,四大组件中我们比较关注Activity和Service的生命周期,尤其是Activity是重中之重,而Fragment的生命周期依赖于Activity,因此只要弄懂了Activity的生命周期,其它不在话下。

Activity 生命周期关注点

Activity内存泄漏

以典型的后台获取数据,Toast到UI上为例:

        binding.btnStartLifecycle.setOnClickListener {
            thread {
                //模拟网络获取数据
                Thread.sleep(5000)
                runOnUiThread {
                    //线程持有Activity实例
                    Toast.makeText(this@ThirdActivity, "hello world", Toast.LENGTH_SHORT).show()
                }
            }
        }

后台开启线程,模拟网络请求,等待5s后弹出Toast。
正常场景下没问题,若此时还未弹出Toast就退出Activity,会发生什么呢?
显而易见,当然会内存泄漏,因为Activity实例被线程持有,无法回收,Activity泄漏了。

资源浪费

以后台获取数据,展示到Activity上为例:

        binding.btnStartGetInfo.setOnClickListener {
            thread {
                //模拟获取数据
                var count = 0
                while (true) {
                    Thread.sleep(2000)
                    runOnUiThread {
                        binding.count.text = "计算值:${count++}"
                        println("${binding.count.text}")
                    }
                }
            }
        }

后台开启线程,模拟网络请求,等待5s后更新TextView。
正常场景下没问题,若此时回到桌面或是切换到其它App,我们是不需要更新UI,也就不需要获取网络数据,此种情况下就会存在资源浪费,应当避免这种写法。

存在以上两种现象是因为在实现功能的过程中没有注意Activity的生命周期,简而言之,我们关注Activity生命周期就是为了解决两类问题:
image.png

解决方法也很简单,不管是Activity退出还是回到后台都会有各个阶段生命周期的回调。因此,只要监听了Activity周期,在对应的地方进行防护就可以解决上述问题。
详情请移步:Android Activity 生命周期详解及监听

2. Activity与协程的结合

没有关联生命周期的协程的使用

先看Demo:

        val scope = CoroutineScope(Job())
        binding.btnStartUnlifecyleCoroutine.setOnClickListener {
            scope.launch {
                delay(5000)
                scope.launch(Dispatchers.Main) {
                    Toast.makeText(this@ThirdActivity, "协程还在运行中", Toast.LENGTH_SHORT).show()
                }
            }
        }

如上,构造了协程作用域,通过它启动协程,5s后在后台打印。
当点击该按钮后,我们退出Activity,最后发现Toast还会出现,说明发生了泄漏。

关联生命周期的协程的使用

解决泄漏

协程的出现简化了我们的编程结构,然而只要和Activity产生瓜葛都避免不了要关注它的生命周期。
还好,协程内部主动关联了生命周期,不用开发者去手动处理,来看看怎么使用的。

        binding.btnStartWithlifecyleCoroutine.setOnClickListener {
            lifecycleScope.launch {
                delay(5000)
                lifecycleScope.launch(Dispatchers.Main) {
                    Toast.makeText(this@ThirdActivity, "协程还在运行中", Toast.LENGTH_SHORT).show()
                }
                //假设有网络请求
                println("协程还在运行中")
            }
        }

与上个demo不同的是协程作用域的选择,这次用的是lifecycleScope,它是LifecycleOwner的扩展属性。
点击按钮后,退出Activity,此时看不到Toast,也看不到打印,说明协程作用域检测到Activity退出后将自己销毁了,也就不会引用Activity实例,当然就解决了内存泄漏问题。

避免资源浪费

细心的你可能发现了:若此时点击按钮后回到桌面,发现打印还在继续,实际上为了节约资源我们不想让它们继续运行,怎么办呢?
当然,协程也考虑了这种场景,提供了几个便利的函数。

        binding.btnStartPauseLifecyleCoroutine.setOnClickListener {
            lifecycleScope.launchWhenResumed {
                delay(5000)
                lifecycleScope.launch(Dispatchers.Main) {
                    Toast.makeText(this@ThirdActivity, "协程还在运行中", Toast.LENGTH_SHORT).show()
                }
                println("协程还在运行中")
            }
        }

点击按钮后,退回到桌面,等待几秒后也没发现打印,从桌面回到App后,发现Toast和打印都出现了。
这也符合了我们的要求:App在前台时协程工作,App在后台时协程停止工作,避免不必要的资源浪费。
launchWhenResumed()函数顾名思义是当Activity处在Resume状态时激活协程,非Resume状态时挂起协程,类似的还有launchWhenCreated、launchWhenStarted。

关联生命周期的协程的原理

解决内存泄漏的原理

知道了怎么使用,又到了探索原理的时刻,重点在协程作用域。

#LifecycleOwner.kt
//扩展属性
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

#Lifecycle.kt
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            //构造新的协程作用域,默认在主线程执行协程
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
                //协程作用域关联生命周期
                newScope.register()
                return newScope
            }
        }
    }

fun register() {
    launch(Dispatchers.Main.immediate) {
        if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
            //监听生命周期变化
            lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
        } else {
            //如果已经处在destroy状态,直接取消协程
            coroutineContext.cancel()
        }
    }
}

由上可知:

  1. LifecycleOwner有个扩展属性lifecycleScope,而LifecycleOwner又持有了Lifecycle,因此LifecycleOwner的lifecycleScope来自于Lifecycle的扩展属性coroutineScope
  2. 既然是Lifecycle的扩展属性,理所当然可以监听Lifecycle的状态变化

lifecycleScope 监听了Lifecycle的状态变化,直接看其回调的处理即可:

#Lifecycle.kt
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
    if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
        //如果处于Destroy状态,也就是Activity被销毁了,那么移除监听者
        lifecycle.removeObserver(this)
        //取消协程
        coroutineContext.cancel()
    }
}

至此就比较明了了:

每个Activity实例就是一个LifecycleOwner,进而每个Activity都关联了一个lifecycleScope对象,该对象可以监听Activity的生命周期,在Activity销毁时取消协程。

避免资源浪费原理

相较于解决内存泄漏原理,避免资源浪费原理比较绕,我们简单捋一下。
以launchWhenResumed函数为例,它是LifecycleCoroutineScope里的函数:

#Lifecycle.kt
public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
    //启动了协程
    lifecycle.whenResumed(block)
}

#PausingDispatcher.kt
public suspend fun <T> Lifecycle.whenResumed(block: suspend CoroutineScope.() -> T): T {
    return whenStateAtLeast(Lifecycle.State.RESUMED, block)
}

public suspend fun <T> Lifecycle.whenStateAtLeast(
    minState: Lifecycle.State,
    block: suspend CoroutineScope.() -> T
): T = withContext(Dispatchers.Main.immediate) {
    //切换协程,在主线程执行
    val job = coroutineContext[Job] ?: error("when[State] methods should have a parent job")
    //协程分发器
    val dispatcher = PausingDispatcher()
    //关联了生命周期
    val controller =
        LifecycleController(this@whenStateAtLeast, minState, dispatcher.dispatchQueue, job)
    try {
        //在新的协程里执行block
        withContext(dispatcher, block)
    } finally {
        controller.finish()
    }
}

以上透露了三个信息:

  1. launchWhenResumed 不是挂起函数,它内部启动了新的协程
  2. launchWhenResumed的闭包要通过PausingDispatcher 调度
  3. LifecycleController 关联了生命周期

重点看第3点:

#LifecycleController.kt
private val observer = LifecycleEventObserver { source, _ ->
    if (source.lifecycle.currentState == Lifecycle.State.DESTROYED) {
        //取消协程
        handleDestroy(parentJob)
    } else if (source.lifecycle.currentState < minState) {
        //小于目标状态,比如非Resume,则挂起协程
        dispatchQueue.pause()
    } else {
        //继续分发协程
        dispatchQueue.resume()
    }
}

init {
    if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
        handleDestroy(parentJob)
    } else {
        //LifecycleController 初始化时监听生命周期
        lifecycle.addObserver(observer)
    }
}

还是通过了lifecycle关联了生命周期。

以上代码结合着看估计还是有点懵,也有点绕,没关系老规矩,用图一看便知:
image.png
重点在于是否可以分发的判断,该判断是基于DispatchQueue里的状态:

    fun canRun() = finished || !paused

当非Resume状态时,paused=true,不能分发;
当处在Resume状态时,paused=false,能分发。
当Activity退出,finished=true。

3. ViewModel与协程的配合

没有关联生命周期的协程的使用

在MVVM的架构里,推荐的做法是在ViewModel里进行数据的请求,如:

    val liveData = MutableLiveData<String>()
    fun getStuInfo() {
        thread {
            //模拟网络请求
            Thread.sleep(2000)
            liveData.postValue("hello world")
        }
    }

而后在Activity里监听数据的变化:

        //监听数据变化
        val vm  by viewModels<MyVM>()
        vm.liveData.observe(this) {
            Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
        }
        vm.getStuInfo()

当然直接开线程的请求数据的方式并不优雅,既然有了协程,那么用协程切换到子线程请求即可。

    val scope = CoroutineScope(Job())
    fun getStuInfoV2() {
        scope.launch {
            //模拟网络请求
            delay(4000)
            liveData.postValue("hello world")
            println("hello world")
        }
    }

和上面一样的测试步骤:
当退出Activity后,ViewModel里的协程打印还在持续,虽然此时Activity并没有泄漏,但我们也知道ViewModel是为Activity服务的,Activity都销毁了,ViewModel没存在的必要了,因此其关联的协程也该取消达到节约资源的目的。

关联生命周期的协程的使用

    fun getInfo() {
        viewModelScope.launch {
            //模拟网络请求
            delay(4000)
            liveData.postValue("hello world")
            println("hello world")
        }
    }

此种写法比上面的更简洁。
当退出Activity后,协程被取消了,当然打印也不会出现了。

关联生命周期的协程的原理

重点在viewModelScope对象,它是ViewModel的一个扩展属性:

#ViewModel.kt
public val ViewModel.viewModelScope: CoroutineScope
    get() {
        //查缓存
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        //加入到缓存里
        return setTagIfAbsent(
            JOB_KEY,
            //构造协程作用域
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

ViewModel构造了一个扩展属性:viewModelScope,用以表示当前ViewModel的协程作用域,将作用域对象存储到Map里。
后续在ViewModel里想要使用协程的地方调用viewModelScope即可,极大增强了便利性。
接下来看看它如何在Activity销毁后取消协程。

    final void clear() {
        mCleared = true;
        if (mBagOfTags != null) {
            synchronized (mBagOfTags) {
                //从缓存取出协程作用域
                for (Object value : mBagOfTags.values()) {
                //取消协程
                closeWithRuntimeException(value);
            }
            }
        }
    }

整个流程用图表示:
image.png

上面的流程涉及到ViewModel的原理,有兴趣可以移步:Jetpack ViewModel 抽丝剥茧

4. Application创建全局的协程作用域

无论是Activity里的lifecycleScope亦或是ViewModel里的viewModelScope,都和页面有关系,页面销毁了它们都没有存在的必要了。而有时候我们需要在页面之外的其它地方使用协程,它们不受页面创建与销毁的影响,通常我们会想到使用全局的协程。
image.png

自定义Application扩展属性

val Application.scope: CoroutineScope
get() {
    return CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
//使用
application.scope.launch {
    delay(5000)
    println("协程在全局状态运行1")
}

构造了全局的协程作用域,当在其它模块拿到Application实例时就可以访问该扩展属性。
此种方式的好处:可以方便地自定义协程上下文。

GlobalScope

一般在测试的时候使用,不推荐使用在正式的项目里。

GlobalScope.launch {
    delay(5000)
    println("协程在全局状态运行2")
}

ProcessLifecycleOwner

官方出品,它更多的时候被用来监测App在前后台的状态,原理是通过监听Lifecycle,既然有Lifecycle,当然有协程作用域了:

ProcessLifecycleOwner.get().lifecycleScope.launch {
    delay(5000)
    println("协程在全局状态运行3")
}

5. Flow、协程、生命周期的三角关系

概念明晰

从Android开发的角度来看,三者有如下区别:

  1. 生命周期主要说的是UI的生命周期
  2. Flow和协程是Kotlin语言范畴的,Kotlin是跨平台的
  3. Flow必须要在协程里使用
  4. 结合1.2两点,我们发现关联了生命周期的协程作用域都是以扩展属性的形式存在的,毕竟其它平台可能不需要关联生命周期

Flow 与生命周期

LiveData关联生命周期

Flow号称是LiveData的增强实现,我们知道LiveData是可以检测生命周期的,如:

        binding.btnStartLifecycleLivedata.setOnClickListener { 
            vm.liveData.observe(this) {
                //接收数据
                println("hello world")
            }
            vm.getInfo()
        }

当App退回到桌面,此时即使ViewModel里继续往LiveData里赋值,也不会触发LiveData回调。当App恢复到前台后,LiveData回调将被触发。
此种设计是为了避免不必要的资源浪费。

Flow结合launchWhenXX

此时你可能会想到:不用LiveData传递数据,改用Flow替代它,该怎么关联生命周期呢?
按照前面的经验,很容易有如下写法:

        binding.btnStartLifecycleFlowWhen.setOnClickListener {
            lifecycleScope.launchWhenResumed {
                MyFlow().flow.collect {
                    println("collect when $it")
                }
            }
        }

    val flow = flow {
        var count = 0
        while (true) {
            kotlinx.coroutines.delay(1000)
            println("emit hello world $count")
            emit(count++)
        }
    }

构造一个冷流Flow,在Activity里通过launchWhenResumed启动协程,并在协程里调用collect末端操作符。collect触发flow闭包里的代码执行,源源不断地发射数据,collect闭包里的打印也将持续。
此时将App退回到桌面,发现打印没有出现,而后将App返回前台,打印继续。如此一来就可以达成和LiveData一样的效果。
从打印结果我们还发现有趣的现象:

在打印到数字5的时候,我们退回桌面,等待若干秒后再回到前台,此时从6开始打印
说明launchWhenXX函数在Activity不活跃时并没有终止flow上游的工作,仅仅只是将协程挂起了

Flow结合repeatOnLifecycle

而更多的时候,当Activity不活跃时,我们不想要flow继续工作,此时引入了另一个API:repeatOnLifecycle

        binding.btnStartLifecycleFlowRepeat.setOnClickListener {
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.RESUMED) {
                    MyFlow().flow.collect {
                        println("collect repeat $it")
                    }
                }
                println("repeatOnLifecycle over")
            }
        }

通过打印发现:

在打印到数字5的时候,我们退回桌面,等待若干秒后再回到前台,此时从0开始打印
说明repeatOnLifecycle函数在Activity不活跃时终止了flow上游的工作,因为协程被取消了。当Activity活跃后,协程又重新启动,flow工作重来一次

你也许还有疑惑:上面的Demo没有直接证明两者的区别,因为在Activity退到桌面后flow闭包里的打印都没出现。
对Demo稍加修改,结果就会显而易见:

    val flow = flow {
        var count = 0
        while (true) {
            kotlinx.coroutines.delay(1000)
            println("emit hello world $count")
            emit(count++)
        }
    }.flowOn(Dispatchers.IO)

使用repeatOnLifecycle时,在Activity退到桌面后,打印消失,说明flow停止工作
使用launchWhenXX是,在Activity退到桌面后,打印继续,说明flow在工作

repeatOnLifecycle 原理

repeatOnLifecycle 是LifecycleOwner的扩展函数,进而是lifecycle的扩展函数,因此它就拥有了生命周期。
repeatOnLifecycle 函数里开启了新的协程,并监听生命周期的变化:

//监听生命周期
observer = LifecycleEventObserver { _, event ->
    if (event == startWorkEvent) {
        //大于目标生命状态,则启动协程
        launchedJob = this@coroutineScope.launch {
            // Mutex makes invocations run serially,
            // coroutineScope ensures all child coroutines finish
            mutex.withLock {
                coroutineScope {
                    block()
                }
            }
        }
        return@LifecycleEventObserver
    }
    if (event == cancelWorkEvent) {
        //小于目标生命状态,则取消协程
        launchedJob?.cancel()
        launchedJob = null
    }
    if (event == Lifecycle.Event.ON_DESTROY) {
        //Activity退出,则唤醒挂起的协程
        cont.resume(Unit)
    }
}
this@repeatOnLifecycle.addObserver(observer as LifecycleEventObserver)

repeatOnLifecycle 还有另一种使用方式:

                MyFlow().flow.flowWithLifecycle(this@ThirdActivity.lifecycle, Lifecycle.State.RESUMED)
                    .collectLatest {
                        println("collect repeat $it")
                    }

和repeatOnLifecycle一样的效果,只是此种方式产生的Flow是线程安全的。

launchWhenXX与repeatOnLifecycle区别与应用场景

image.png

最后,总结三者之间的关系。
image.png

Flow很强大也很好用,关键是怎么用,如何从众多的Flow操作符选择合适进行业务开发,如何一眼就分辨它们的作用,下篇将揭开Flow常见操作符神秘的面纱,敬请关注。
本文基于Kotlin 1.5.3,文中完整实验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/365166.html

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

相关文章

Elasticsearch:使用 pipelines 路由文档到想要的 Elasticsearch 索引中去

路由文件 当应用程序需要向 Elasticsearch 添加文档时&#xff0c;它们首先要知道目标索引是什么。在很多的应用案例中&#xff0c;特别是针对时序数据&#xff0c;我们想把每个月的数据写入到一个特定的索引中。一方面便于管理索引&#xff0c;另外一方面在将来搜索的时候可以…

从0开始学python -37

Python3 错误和异常 作为 Python 初学者&#xff0c;在刚学习 Python 编程时&#xff0c;经常会看到一些报错信息&#xff0c;在前面我们没有提及&#xff0c;这章节我们会专门介绍。 Python 有两种错误很容易辨认&#xff1a;语法错误和异常。 Python assert&#xff08;断…

C语言实现用堆解决 TOP-K 问题

目录 TopK函数实现 如何测试 完整源码 生活中我们经常能见到TopK问题&#xff0c;例如&#xff1a;专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。 所以&#xff0c;TopK问题即求出一组数据中前K个最大或最小的元素&#xff0c;一般情况下&#xff0c;数据量都…

[ Java ] 时间API在更新,传奇已经谢幕,但技术永远不死

&#xff08;Bill Joy(左一)&#xff0c;Vinod Khosla(左二)&#xff0c;Andy Bechtolsheim(右二)&#xff0c;Scott McNealy(右一) &#xff09; CSDN 博文征集活动&#xff08;和日期相关的代码和bug&#xff09;&#xff1a;点击这里 各位 “big guys”&#xff0c;这篇博文…

【数据结构】顺序表的深度剖析

&#x1f307;个人主页&#xff1a;平凡的小苏 &#x1f4da;学习格言&#xff1a;别人可以拷贝我的模式&#xff0c;但不能拷贝我不断往前的激情 &#x1f6f8;C语言专栏&#xff1a;https://blog.csdn.net/vhhhbb/category_12174730.html &#x1f680;数据结构专栏&#xff…

Dart的安装及环境变量配置

本文介绍dart的安装步骤及环境变量配置&#xff0c;以及如何在vscode中进行开发环境配置。一、dart的安装访问dart官网https://dart.cn/&#xff0c;点击网站右上角的获取DART SDK进行下载页面。如下图&#xff0c;选择下载SDK的zip压缩文件。根据自己的操作系统情况选择合适版…

DOM 文档对象模型

目录 一、简介 二、节点Node 三、document 1、简介 2、document对象的原型链 3、部分属性 四、元素节点 1、如何获取元素节点对象 通过document对象来获取已存在的元素节点 通过document对象来创建元素节点 2、原型链 3、通过元素节点对象获取其他节点的方法 五、…

如何备份网站到本地电脑(适用虚拟主机)

一、mysql数据库备份 登陆主机控制面板&#xff0c;点击左侧的数据库。 在数据库管理页面最下方有备份数据库的操作项目。点击【通过SQL文件导入导出】&#xff0c;进入到导出和导入的页面。 选择【导出/备份】这个选项导出。会在在wwwroot目录生成以时间命名的sql文件。 导出…

ADC模数转换器(基于STM32F407)

简介 Analog-to-digital converters&#xff08;模拟数字转换器&#xff09;&#xff0c;我的STM32F407中内置3个ADC&#xff0c;每个 ADC 有 12 位、10 位、8 位和 6 位可选&#xff0c;ADC 具有独立模式、双重模式和三重模式&#xff0c;对于不同 AD 转换要求几乎都有合适的…

list链表,结点

目录 1.链表 2.list构造函数 3.list的赋值和交换&#xff0c;&#xff0c;assign,swap 4.list大小的操作,size,empty,resize 5.list插入和删除&#xff0c;push_back,pop_back,push_front,pop_front,insert,clear,erase,remove 6.list容器数据存取,front,back 7.list反转…

数字孪生加持,水利水电工程或将实现全生命周期管理

水利水电工程在数字孪生技术的加持&#xff0c;使得建设和运营更加高效和智能化&#xff0c;将工程中各种元素、过程和系统数字化&#xff0c;并建立数字孪生模型&#xff0c;以实现工程建设和运营的智能化管理。数字孪生对水利水电实现对工程建设的全生命周期管理&#xff0c;…

Bean的生命周期和作用域

Bean的生命周期Bean的执行流程&#xff1a;Bean 执行流程&#xff1a;启动Spring 容器 -> 实例化 Bean&#xff08;分配内存空间&#xff0c;从无到有&#xff09;-> Bean 注册到 Spring 中&#xff08;存操作&#xff09; -> 将 Bean 装配到需要的类中&#xff08;取…

《计算机网络:自顶向下方法》实验2:常用网络命令的使用

使用Ping实用程序来测试计算机的网络连通性 登录到Windows中。单击开始,然后将鼠标指针移到程序上,再移到Windows系统,然后单击命令提示符。在命令提示窗口键入ping 127.0.0.1。问题1:发送了多少数据包?接受了多少数据包?丢失了多少数据包? 发送了4个数据包;接受了4个数…

JavaScript Web API实战:7个小众技巧让你的网站瞬间提升用户体验

随着技术的日新月异&#xff0c;为开发人员提供了令人难以置信的新工具和API。但据了解&#xff0c;在100 多个 API中&#xff0c;只有5%被开发人员积极使用。 让我们来看看一些有用的Web API&#xff0c;它们可以帮助您将网站推向月球&#xff01; 1、 截屏接口 Screen Capt…

Blockchain gold经测试完美兼容EVM虚拟机

尽管对于行业人士来说&#xff0c;有关寻找更快更便宜的基础层区块链的对话并不是什么新鲜事。 但随着 Defi Summer 持续一年有余的繁荣增长&#xff0c;更实际的需求——以太坊上高昂的 gas 费用使得开发者时间尤为昂贵。 可以看到的是&#xff0c;作为有着以太坊 CPU 之称的 …

Halcon 拟合直线

本文用 Halcon 的矩阵操作实现最小二乘拟合直线 *首先随机生成一组数据 Mx : [100:10:500] tuple_length(Mx, len) tuple_gen_const(len, 5, r) Ma : 2 Mb : 40 tuple_rand(len, noise) My : Ma * Mx Mb * noise gen_circle(ContCircle, My, Mx, r)接下来用矩阵进行最小二乘求…

一次Linux系统密码修改失败事件

一、事件描述 某业务系统采用移动云主机&#xff0c;某次因误操作导致移动云内嵌密码管理相关Pga进程导致页面无法修改密码&#xff0c;东移动云主机web终端登录也无法修改&#xff0c;密码错误次数最大已无法登录&#xff0c;无奈只能重启主机&#xff0c;修改密码&#xff1b…

如何保证线程的原子性

线程的原子性是指&#xff1a;一个或者一系列指令&#xff0c;它的操作是不可中断的&#xff0c;一旦开始是不允许被其他CPU或线程来中断。 我们来看下述代码&#xff1a;ThreadAtomicityDemo类中有个初始值为0的Integer类型的静态变量count&#xff0c;还有一个每次sleep一毫…

Vue3快速上手

Vue3快速上手 1.Vue3简介 2020年9月18日&#xff0c;Vue.js发布3.0版本&#xff0c;代号&#xff1a;One Piece&#xff08;海贼王&#xff09;耗时2年多、2600次提交、30个RFC、600次PR、99位贡献者github上的tags地址&#xff1a;https://github.com/vuejs/vue-next/release…

微信上制作投票链接在线制作投票链接如果制作投票链接

现在来说&#xff0c;公司、企业、学校更多的想借助短视频推广自己。通过微信投票小程序&#xff0c;网友们就可以通过手机拍视频上传视频参加活动&#xff0c;而短视频微信投票评选活动既可以给用户发挥的空间激发参与的热情&#xff0c;又可以让商家和企业实现推广的目的&…