当前位置: 首页 > news >正文

Android Jetpack组件架构 :LiveData的使用和原理

Android Jetpack组件架构: LiveDate的使用和原理

在这里插入图片描述

导言

继Lifecycle组件之后我们接下来要介绍的就是LiveDate组件,所谓LiveDate字面意思上就是有声明的数据,当数据有改动时该组件可以感知到这个操作并将该事件通知到其观察者,这样我们就可以在观察者中做出一些处理,一般都是用来更新UI的操作。这样就实现了当数据改变时U界面自动更新的效果。

LiveDate的使用

LiveDate + ViewModel 实现UI感知数据变化

首先我们来介绍使用LiveDate的基本姿势,也就是LiveDate配合ViewModel使用,这符合MVVM架构的设计,至于ViewModel我们将在之后的篇章中介绍到,这里先简单使用一下它。

首先我们创建一个ViewModel类:

class SimpViewModel: ViewModel() {//LiveDate的实例--在ViewModel中创建val mLiveDate = MutableLiveData<String>()
}

里面存储了一个LiveDate的实例,MutableLiveDataLiveDate的实现类,后面的String泛型代表该LiveDate中存储的数据是String类型的。紧接着我们在Activity中使用,这里我们启用了ViewBinding进行布局的自动绑定:

private const val TAG = "MainActivity"
class MainActivity : AppCompatActivity() {//懒加载private val mBinding:ActivityMainBinding by lazy {ActivityMainBinding.inflate(layoutInflater)}private lateinit var mViewModel:SimpViewModeloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(mBinding.root)//创建出ViewModel对象mViewModel = ViewModelProvider(this).get(SimpViewModel::class.java)//获得LiveDate对象val mLiveDate = mViewModel.mLiveDate//观察LiveDate,一旦有数据改变就触发观察方法更新UImLiveDate.observe(this,{mBinding.simpleLiveDate.text = itLog.d(TAG, "在线程:${Thread.currentThread().name}中运行")})GlobalScope.launch{//延迟5sdelay(5000)mLiveDate.postValue("LifeDate已更新")}}
}

可以看到我们首先是创建出了一个ViewModel对象然后获取到了它的LiveDate实例,接着我们就调用到了LiveDate的observe方法,该方法是用于为LiveDate对象添加一个观察者的,第一个参数为LifecycleOwner对象,这个我们在Lifecycle的介绍中提到过了,是一个具有生命周期的对象,传入的意义是什么呢?这主要是为了在观察的Activity进入休眠状态时不再通知其观察者,这样可以实现正确通知观察者的效果。传入的第二个对象就是一个观察者了,我们需要实现其Oberserve方法,这个方法就是在LiveDate的数据变化时触发的。

最后我们创建了一个协程,先延时五秒钟然后通过postValue方法改变了LiveDate中的值,具体我们还有一个setValue方法也可以改变LiveDate的值,不同之处在于postValue方法可以在非主线程中调用而setValue方法必须在主线程中调用,在这个observe方法中我们还额外打印出来这个回调方法是在哪个线程中执行的,通过打印我们也可以发现是在主线程中被调用的,这也在意料之中,因为只能在主线程中更新UI:
在这里插入图片描述

更改LiveDate的数据

接下来我们将在LiveDate更新完的数据传递到观察者对象之前对该数据进行更改,相当于是对setValue或者postValue设置的值进行额外的处理再传递给Observe方法执行。这里我们以一个根据当前温度来判断天气的例子,先在ViewModel中新建一个MapLiveDate:

class SimpViewModel: ViewModel() {//LiveDate的实例--在ViewModel中创建val mLiveDate = MutableLiveData<Double>()val mMapLiveDate = Transformations.map(mLiveDate,{val s1 = when(it) {in -20.0 .. 0.0 -> "很冷"in 0.0 .. 10.0 -> "冷"in 10.0 .. 15.0 -> "较冷"in 15.0 .. 28.0 -> "温暖"in 28.0..Double.MAX_VALUE -> "热"else -> { "您是人吗" }}"当前天气:${s1}"})
}

这个LiveDate是通过Transformations.map方法创建出来的,第一个参数就是它跟踪的LiveDate,一旦它跟踪的LiveDate发生了数据更新事件,该LiveDate就会将变化后的数据捕获并可以在其方法中对捕获的数据进行处理,比如说在这里我们就根据传入的Double类型的参数判断天气并将其转化成String类型的参数传递给该LiveDate的Observe方法。

然后我们再更改Activity中的代码:

private const val TAG = "MainActivity"
class MainActivity : AppCompatActivity() {private val mBinding:ActivityMainBinding by lazy {ActivityMainBinding.inflate(layoutInflater)}private lateinit var mViewModel:SimpViewModeloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(mBinding.root)//创建出ViewModel对象mViewModel = ViewModelProvider(this).get(SimpViewModel::class.java)//获得LiveDate对象val mLiveDate = mViewModel.mLiveDateval mMapDate = mViewModel.mMapLiveDate//观察LiveDate,一旦有数据改变就触发观察方法更新UImLiveDate.observe(this,{
//            mBinding.simpleLiveDate.text = it.toString()Log.d(TAG, "更新之后的数据为:${it.toString()}")Log.d(TAG, "在线程:${Thread.currentThread().name}中运行")})mMapDate.observe(this,{mBinding.simpleLiveDate.text = it})GlobalScope.launch{//延迟5sdelay(3000)mLiveDate.postValue(10.0)}}
}

不同之处在于这里我们是在我们新创建出来的MapLiveDate中更新UI的,按照道理来说这个mMapLiveDate中收到的参数就是我们之前根据Double将其转化成String之后的结果,我们来看运行结果:

在这里插入图片描述
可以看到这里更新的UI就是我们转化之后的数据了,那前一个Observer接收到的是什么参数呢:
在这里插入图片描述
可以看到前一个Observer接收到的参数还是一样的。

合并多个LiveDate的数据源

这个所谓合并多个LiveDate实际上就是用一个大的LiveDate来监听多个其他小的LiveDate,来达到任何一个小LiveDate发生改变时大的LiveDate都能监听到的效果。话不多说,直接上代码,viewModel:

private const val TAG = "SimpViewModel"
class SimpViewModel: ViewModel() {//LiveDate的实例--在ViewModel中创建val mLiveDate = MutableLiveData<Double>()val mLiveDate2 = MutableLiveData<Double>()val mMapLiveDate = Transformations.map(mLiveDate,{val s1 = when(it) {in -20.0 .. 0.0 -> "很冷"in 0.0 .. 10.0 -> "冷"in 10.0 .. 15.0 -> "较冷"in 15.0 .. 28.0 -> "温暖"in 28.0..Double.MAX_VALUE -> "热"else -> { "您是人吗" }}"当前天气:${s1}"})val mMergyLiveDate = MediatorLiveData<Double>()init {mMergyLiveDate.apply {addSource(mLiveDate, {Log.d(TAG, ":接收到了mLiveDate的数据:${it}")})addSource(mLiveDate2, {Log.d(TAG, ":接收到了mLiveDate2的数据:${it}")})}}}

Activity:

private const val TAG = "MainActivity"
class MainActivity : AppCompatActivity() {private val mBinding:ActivityMainBinding by lazy {ActivityMainBinding.inflate(layoutInflater)}private lateinit var mViewModel:SimpViewModeloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(mBinding.root)//创建出ViewModel对象mViewModel = ViewModelProvider(this).get(SimpViewModel::class.java)//获得LiveDate对象val mLiveDate = mViewModel.mLiveDateval mLiveData2 = mViewModel.mLiveDate2val mMapDate = mViewModel.mMapLiveDateval mMergyLiveDate = mViewModel.mMergyLiveDate//观察LiveDate,一旦有数据改变就触发观察方法更新UImLiveDate.observe(this,{Log.d(TAG, "LiveDate1   更新之后的数据为:${it.toString()}")Log.d(TAG, "在线程:${Thread.currentThread().name}中运行")})mLiveData2.observe(this,{Log.d(TAG, "LiveDate2   更新之后的数据为:${it.toString()}")})mMapDate.observe(this,{mBinding.simpleLiveDate.text = it})mMergyLiveDate.observe(this,{Log.d(TAG, "接收到的数据为:${it}")})GlobalScope.launch{//延迟5smLiveDate.postValue(10.0)delay(3000)mLiveData2.postValue(15.0)}}
}

可以在打印日志中看到,这两个LiveDate发生改变时MergyLiveDate都感受到了:
在这里插入图片描述
以上就是所有有关LiveDate的基础使用了,接下来我们来分析LiveDate的原理。

LiveDate的原理

首先我们直接点开源文件,它的开头也有一大段注释:
在这里插入图片描述
这大段的注释中有几个比较重要的信息,首先是Observer观察者只有在处于Active状态时才能感受到LiveDate中数据的变化,至于这个Active状态,是在Lifecycle组件的状态处于Lifecycle.State.STARTED或者Lifecycle.State.RESUMED时Observer才处于
Active状态,这也就是我们一开始说的LiveDate是依赖于Lifecycle组件框架的原因。当宿主Lifecycle的状态处于Lifecycle.State.DESTROYED 时,Observer将会被自动移除,这个功能显然可以避免内存泄漏的问题。

Observe开始观察

首先我们来分析Observe方法,该方法的作用就是为LiveDate设置一个观察者:

    @MainThreadpublic void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {assertMainThread("observe");//判断是否运行在主线程中if (owner.getLifecycle().getCurrentState() == DESTROYED) {//如果宿主Lifecycle当前的状态为DESTROYED// ignorereturn;//说明当前LiveDate不是处于Active状态,不进行任何处理,直接返回}LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); //将宿主Owner和观察者Observer包装成一个装饰类ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper); //判断LiveDate中是否已经有Observerif (existing != null && !existing.isAttachedTo(owner)) { //如果LiveDate中的Observer列表中已经存在了throw new IllegalArgumentException("Cannot add the same observer"+ " with different lifecycles"); //报错,一个Observer不能多次观察一个LiveDate}if (existing != null) { return;}//如果原有的列表中不存在的话,说明之前没有注册过,将其添加进Observers列表中owner.getLifecycle().addObserver(wrapper);}

在上面的方法中已经对大概的逻辑进行了一些注释了,首先这个方法是需要在主线程执行的,不然就会抛出异常。之后会首先判断当前LiveDate是否处于Active状态,如果不是Active状态那么就直接返回,此时不需要进行数据的观察。接下来会将LifecycleOwner和传入的Observer包装成一个LifecycleBoundObserver对象,关于这个类我们等等再来细看。之后调用到mObservers.putIfAbsent方法,mObservers是一个可以安全迭代(即可以在迭代时安全修改)的Map:

    private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers =new SafeIterableMap<>();

至于putIfAbsent方法和一般的put方法有一些差别,如果传入的key对应的value已经存在,那么将不会进行替换,并且返回已经存在的value,如果传入的key对应的value不存在的话则会将key和value添加进入Map中并且返回null.

所以这一个方法完成的是两个目的,首先是在Map中添加元素,其次是判断传入的key在之前是否已经被添加过了,如果LiveDate中的Observer列表中已经存在了就直接抛出异常,因为一个Observer不允许被多次添加进入一个LiveDate的观察者列表中。如果之前不存在的话,则会在最后调用addObserver方法将其添加进入Observer列表中,不过这里我们可以发现这里将其添加进入的是Lifecycle的列表,而不是LiveDate的列表(LiveDate的列表在之前的putIfAbsent方法中已经添加了。)

LifecycleBoundObserver类 和 ObserverWrapper类

我们先来看LifecycleBoundObserver这个类的整个结构:

    class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {@NonNullfinal LifecycleOwner mOwner;LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {super(observer);mOwner = owner;}@Overrideboolean shouldBeActive() {return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);}@Overridepublic void onStateChanged(@NonNull LifecycleOwner source,@NonNull Lifecycle.Event event) {Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();if (currentState == DESTROYED) {removeObserver(mObserver);return;}Lifecycle.State prevState = null;while (prevState != currentState) {prevState = currentState;activeStateChanged(shouldBeActive());currentState = mOwner.getLifecycle().getCurrentState();}}@Overrideboolean isAttachedTo(LifecycleOwner owner) {return mOwner == owner;}@Overridevoid detachObserver() {mOwner.getLifecycle().removeObserver(this);}}

首先可以看到它实现的是LifecycleEventObserver 接口,还记得这个接口吗?在上一篇关于Lifecycle的文章中提到过:
在这里插入图片描述
既然已经知道了是LifecycleEventObserver接口的话,那么其实我们就可以很清楚的知道这个Observe事件并不是由LiveDate自身分发的,它的分发是和Lifecycle中的默认观察者一样的,也就是说是通过Lifecycle的Observer列表进行分发的。所以我们接下来直接看它的onStateChanged方法就好了,这个方法是在Lifecycle进行事件分发时触发的。

        @Overridepublic void onStateChanged(@NonNull LifecycleOwner source,@NonNull Lifecycle.Event event) {Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();//获得当前宿主的生命周期状态if (currentState == DESTROYED) { //如果宿主已经消失,那么将其从Lifecycle的观察列表中移除removeObserver(mObserver);return;}Lifecycle.State prevState = null; //之前的状态while (prevState != currentState) { //当之前的状态 不等于 当前状态prevState = currentState; //将状态迭代//这里调用`activeStateChanged方法`activeStateChanged(shouldBeActive());//`shouldBeActive`方法获得的是当前状态是否应该是Active状态//更新当前状态currentState = mOwner.getLifecycle().getCurrentState();}}

具体的逻辑也已经在上面标注出来了,唯一的问题就是activeStateChanged方法,我们来看这个方法,这个方法是在LiveDate的内部抽象类ObserverWrapper中定义的,他也是上面的LifecycleBoundObserver的父类,所以接下来又要看ObserverWrapper类了:

    private abstract class ObserverWrapper {final Observer<? super T> mObserver;boolean mActive;int mLastVersion = START_VERSION;ObserverWrapper(Observer<? super T> observer) {mObserver = observer;}abstract boolean shouldBeActive();boolean isAttachedTo(LifecycleOwner owner) {return false;}void detachObserver() {}void activeStateChanged(boolean newActive) {if (newActive == mActive) {return;}// immediately set active state, so we'd never dispatch anything to inactive// ownermActive = newActive;changeActiveCounter(mActive ? 1 : -1);if (mActive) {dispatchingValue(this);}}}

这个类也是和它的名字相符合,整个一个就是对Observer进行了装饰,所以整个装饰下来就是这样的:

在这里插入图片描述
我们的Observer经过重重装饰就已经实现了LifecycleEventObserver接口,从而其方法也可以由整个Lifecycle框架进行分发了。回到正题,来看它的activeStateChanged方法,如果Active状态并没有发生改变的话就直接返回,不作任何处理。否则他会调用到changeActiveCounter方法,并且如果当前新状态为Active的话还会进行值的分发。

LiveDate更改观察者状态

前面提到了会调用changeActiveCounter方法,这个方法是在LiveDate中定义的,具体如下:

    void changeActiveCounter(int change) {int previousActiveCount = mActiveCount; //获取之前的ActiveCount的值mActiveCount += change; //更新ActiveCount的值if (mChangingActiveState) { //如果当前有线程正在处理的话就直接返回return;}mChangingActiveState = true;try {while (previousActiveCount != mActiveCount) { //当之前的ActiveCount的值与现在的ActiveCount的值不等的话boolean needToCallActive = previousActiveCount == 0 && mActiveCount > 0;//如果之前的ActiveCount为0且现在的ActiveCount值大于0boolean needToCallInactive = previousActiveCount > 0 && mActiveCount == 0;//如果之前的ActiveCount大于0且当前的ActiveCount值等于0previousActiveCount = mActiveCount;//更新之前的ActiveCount的值if (needToCallActive) {//如果需要调用onActive方法的话onActive();} else if (needToCallInactive) {//如果需要调用onInActive方法的话onInactive();}}} finally {mChangingActiveState = false;}}

该方法做的事情主要就是维护mActiveCount的值,这个mActiveCount就是有多少个之前处于Active状态下的Observer的数量,如果观察该LiveDate的观察者全都不是处于Active状态的话就调用onInactive()回调,并且此时逻辑上LiveDate是处于非Active状态的,反之就会回调onActive方法,这个方法默认是没有实现的。

LiveDate分发Value

接下来我们再来看dispatchingValue方法:

    void dispatchingValue(@Nullable ObserverWrapper initiator) {if (mDispatchingValue) {mDispatchInvalidated = true;return;}mDispatchingValue = true;do {mDispatchInvalidated = false;if (initiator != null) {considerNotify(initiator);initiator = null;} else {for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {considerNotify(iterator.next().getValue());if (mDispatchInvalidated) {break;}}}} while (mDispatchInvalidated);mDispatchingValue = false;}

可以看到这个方法一开始的处理也是和之前的changeActiveCounter方法类似,主要是为了防止并发修改。接下来的核心逻辑其实就是considerNotify方法:

    private void considerNotify(ObserverWrapper observer) {if (!observer.mActive) { //如果当前传入的Observer不是Active状态就直接返回return;}if (!observer.shouldBeActive()) {//如果传入的observer不应该处于Active状态,也就是说当前是Active状态但是之后不应该处于Active状态observer.activeStateChanged(false);//更新Observer的active状态return;}if (observer.mLastVersion >= mVersion) {//如果observer的上一个Version值大于当前的Version值return; //直接返回}observer.mLastVersion = mVersion; //更新Version值observer.mObserver.onChanged((T) mData); //触发回调方法}

整个逻辑也很清楚首先会更新当前的Active状态,之后根据当前的状态和接下来的状态(此处的状态特指Active状态)来进行Observer标志位的设置,这里还涉及到一个Version值的概念,我们之后再提。最后一行就是触发我们写入的onChanged方法了,也就是我们传入的回调方法。不过这里只是触发我们回调的一种情况,就是宿主的生命周期变化时会将触发回调,我们先来简单总结一下:
在这里插入图片描述

通过set/postValue方法触发回调

接下来是我们最重要的一个部分,那就是通过setValue或者postValue方法触发我们的回调,这次我们先来看postValue方法

    protected void postValue(T value) {boolean postTask;//同步方法,进行上锁synchronized (mDataLock) { //pendingData,意思就是尚未被发送的数据postTask = mPendingData == NOT_SET; //此处判断本次投递是不是挂起的任务,第一次总是truemPendingData = value;//设置挂起数据为当前数据}//将锁释放if (!postTask) { //如果不是挂起任务的话,也就是当上次任务还没有投递完成时return; //直接返回}ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);}

这里涉及到了mPendingDate这个成员变量,我们会在后面比较setValue方法中再次提到,这里会首先通过线程池的postToMainThread方法投递mPostValueRunnable的任务,这个任务是在LiveData创建的:

    private final Runnable mPostValueRunnable = new Runnable() {@SuppressWarnings("unchecked")@Overridepublic void run() {Object newValue;synchronized (mDataLock) {//上锁newValue = mPendingData;//将新值设置为mPendingData,这个值在之前的postValue方法中设置mPendingData = NOT_SET;//重新将Pending位设置为NOT_SET}setValue((T) newValue);//调用setValue方法}};

至于postToMainThread方法,就是将runnable通过Handler机制投递到主线程中执行,然后调用其setValue方法修改Value的值。我们可以设想一下当只有一条线程执行一次postValue的情况,首先会将mDataLock进行上锁,然后通过Handler将任务发送到主线程执行,这个runnable运行时也会上锁,然后将新值取出,将mPendingData转为NOT_SET状态。最后执行setValue方法更改新值。

在多线程的情况下,我们再来分析,当有两条线程同时执行postValue方法时首先先进入的那条线程会将设置postTask 时的代码块先进行上锁,这样第二条线程只能阻塞在代码块之前;然后第一条线程执行完值的设置时将锁给释放,然后第二条线程就进入了值的设置的阶段,这个时候由于锁被第二条线程给持有了,所以第一条线程在执行runnable时就会被阻塞,第二条线程设置完值之后再将锁给释放,这样第一条线程就执行了runnable方法。不过这个时候第一条线程设置的值实际上是第二条线程中传入的值,因为这个mPendingDatavolatile状态的:

volatile Object mPendingData = NOT_SET;

总之,postValue方法保证了在多线程的情况下总是只有一个最新的值被执行具体的setValue操作,这样做的好处就是可以提高性能,且不会在主线程的消息队列中堆积过多任务,导致主线程的阻塞。好了看完了postValue方法之后我们再来看setValue方法:

    protected void setValue(T value) {assertMainThread("setValue");mVersion++;mData = value;dispatchingValue(null);}

首先这个方法也是一个只能在主线程调用的方法,其次它维护了一个记录值变化的记录值mVersion,也就是说每次成功对LiveData中的数据进行修改后mVersion的值都会发生变化,以此来判断是否需要分发回调事件,最后触发dispatchingValue方法,这个方法我们在之前生命周期发生改变时的情况下提到过,不同的在于此处传入的是null:

    void dispatchingValue(@Nullable ObserverWrapper initiator) {if (mDispatchingValue) {mDispatchInvalidated = true;return;}mDispatchingValue = true;do {mDispatchInvalidated = false;if (initiator != null) {considerNotify(initiator);initiator = null;} else {for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {considerNotify(iterator.next().getValue());if (mDispatchInvalidated) {break;}}}} while (mDispatchInvalidated);mDispatchingValue = false;}

具体来说会进入到后面那段for循环中:

for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {considerNotify(iterator.next().getValue());if (mDispatchInvalidated) {break;}
}

很显然是对每个观察者都调用了considerNotify方法,这个方法我们之前也提到过,这次我们来完全解析一下:

    private void considerNotify(ObserverWrapper observer) {if (!observer.mActive) {return;}if (!observer.shouldBeActive()) {observer.activeStateChanged(false);return;}if (observer.mLastVersion >= mVersion) {return;}observer.mLastVersion = mVersion;observer.mObserver.onChanged((T) mData);}

这里判断观察者是否需要执行回调就是通过开始的三个if条件来判断,当Observer满足以下三个条件之一时将不会执行回调:

  • Observer不处于Active状态
  • Observer不应该处于Activie状态,也就是说当前是Active状态但马上就不是Active状态了
  • 上一次的Observer的Version值大于等于当前的Version值(正常修改之后Version值应该会变大)

当不满足上面三个条件时才会执行回调方法,至于Observer状态的设置是由Lifecycle进行管理的。不过这里思考一个问题,当一个LiveData先执行改变Value,然后一个Observer再观察时也是满足执行回调方法的情况的,也就是说LiveData默认是实现了粘性事件的,如果我们不想要粘性事件的话,思考一下该如何做。我们可以在每次observer进入到Active状态时的mLastVersion为绑定的LiveData的mVersion值。

事件分发的总结

老规矩还是以一张图总结setValue或者postValue方法:
在这里插入图片描述

相关文章:

Android Jetpack组件架构 :LiveData的使用和原理

Android Jetpack组件架构&#xff1a; LiveDate的使用和原理 导言 继Lifecycle组件之后我们接下来要介绍的就是LiveDate组件&#xff0c;所谓LiveDate字面意思上就是有声明的数据&#xff0c;当数据有改动时该组件可以感知到这个操作并将该事件通知到其观察者&#xff0c;这样…...

【学习笔记】Prufer序列

Prufer序列 起源于对 C a y l e y Cayley Cayley定理的证明&#xff0c;但是其功能远不止于此 现在考虑将一棵n个节点的树与一个长度为n-2的prufer序列构造对应关系 T r e e − > P r u f e r : Tree->Prufer: Tree−>Prufer: ①从树上选择编号最小的叶子节点&#x…...

由于找不到msvcr110.dll的5种解决方法

在使用电脑的过程中&#xff0c;我们可能会遇到一些问题&#xff0c;比如打开软件时提示找不到 msvcr110.dll 文件丢失。这通常意味着该文件已被删除或损坏&#xff0c;导致程序无法正常运行。本文将介绍几种解决方案&#xff0c;帮助您解决这个问题。 首先&#xff0c;我们需…...

最长连续递增子序列

给定一个顺序存储的线性表&#xff0c;请设计一个算法查找该线性表中最长的连续递增子序列。例如&#xff0c;(1,9,2,5,7,3,4,6,8,0)中最长的递增子序列为(3,4,6,8)。 输入格式: 输入第1行给出正整数n&#xff08;≤105&#xff09;&#xff1b;第2行给出n个整数&#xff0c;…...

Java学习星球,十月集训,五大赛道(文末送书)

目录 什么是知识星球&#xff1f;我的知识星球能为你提供什么&#xff1f;专属专栏《Java基础教程系列》内容概览&#xff1a;《Java高并发编程实战》、《MySQL 基础教程系列》内容概览&#xff1a;《微服务》、《Redis中间件》、《Dubbo高手之路》、《华为OD机试》内容概览&am…...

前端VUE---JS实现数据的模糊搜索

实现背景 因为后端实现人员列表返回&#xff0c;每次返回的数据量在100以内&#xff0c;要求前端自己进行模糊搜索 页面实现 因为是实时更新数据的&#xff0c;就不需要搜索和重置按钮了 代码 HTML <el-dialogtitle"团队人员详情":visible.sync"centerDi…...

Android Studio 的android.jar文件在哪儿

一般在&#xff1a;C:\Users\admin\AppData\Local\Android\Sdk\platforms\android-33下&#xff08;不一定是33&#xff0c;这个得看你Android Studio->app->builde.gradle的targetSdk是多少&#xff09; 怎么找&#xff1a; 1.打开Android Studio 粘贴地址后&#xff0…...

Elasticsearch 部署学习

文章目录 Elasticsearch 部署学习1. 单节点部署 elasticsearch1.1 部署 jdk1.2 下载 elasticsearch1.3 上传文件并修改配置文件1.4 启动1.5 问题总结1.6 浏览器验证 2. 集群部署 elasticsearch3. 常用命令4. Elasticsearch kibana安装:one: 参考部署文档:two: 下载对应版本的安…...

nodejs 如何在npm发布自己的包 <记录>

一、包结构 必要结构&#xff1a; 一个包对应一个文件夹&#xff08;文件夹名不是包名&#xff0c;但最好与包名保持一致&#xff0c;包名以package.json中的name为主&#xff09;包的入口文件index.js包的配置文件package.json包的说明文档README.md 二、需要说明的文件 1.配…...

移植RTOS的大体思路

最首先当然是去官网看看是不是已经支持目标芯片啦&#xff0c;没有的话&#xff0c;就需要自己手动移植了 获取源码 一般可以从rtos官网或者GitHub上获取源码 确认源码结构 这种有官方文档说明&#xff0c;需要修改的一般都是BSP和libcpu相关文件夹中的内容 CPU架构移植 …...

FPGA到底是什么?

首先只是凭自己浅略的了解&#xff0c;FPGA好像也是涉及到了开发板&#xff0c;单片机之类的东西&#xff0c;和嵌入式十分相似&#xff0c;但是比嵌入式更高级的东西。 肯定有很多小伙伴如我一样&#xff0c;只是听说过FPGA&#xff0c;听别人说的传呼其神&#xff0c;那么它到…...

算法-单词搜索 II

算法-单词搜索 II 1 题目概述 1.1 题目出处 https://leetcode.cn/problems/word-search-ii/description/?envTypestudy-plan-v2&envIdtop-interview-150 1.2 题目描述 2 DFS 2.1 解题思路 每个格子往上下左右四个方向DFS&#xff0c;拼接后的单词如果在答案集中&…...

怒刷LeetCode的第15天(Java版)

目录 第一题 题目来源 题目内容 解决方法 方法一&#xff1a;哈希表双向链表 方法二&#xff1a;TreeMap 方法三&#xff1a;双哈希表 第二题 题目来源 题目内容 解决方法 方法一&#xff1a;二分查找 方法二&#xff1a;线性搜索 方法三&#xff1a;Arrays类的b…...

Android开发MVP架构记录

Android开发MVP架构记录 安卓的MVP&#xff08;Model-View-Presenter&#xff09;架构是一种常见的软件设计模式&#xff0c;用于帮助开发者组织和分离应用程序的不同组成部分。MVP架构的目标是将应用程序的业务逻辑&#xff08;Presenter&#xff09;、用户界面&#xff08;V…...

day2作业

1&#xff0c;输入两个数&#xff0c;完成两个数的加减乘除 #输入两个数&#xff0c;完成两个数的加减乘除 num1int(input("请输入第一个数:")) num2int(input("请输入第二个数:")) print(str(num1)str(num2)str(num1num2)) print(str(num1)-str(num2)str…...

Python办公自动化之Word

Python操作Word 1、Python操作Word概述2、写入Word2.1、标题2.2、章节与段落2.3、字体与引用2.4、项目列表2.5、分页2.6、表格2.7、图片3、读取Word3.1、读取文档3.2、读取表格4、将Word表格保存到Excel5、格式转换5.1、Doc转Docx5.2、Word转PDF1、Python操作Word概述 python-d…...

力扣26:删除有序数组中的重复项

26. 删除有序数组中的重复项 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 给你一个 非严格递增排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 …...

基于C#的AE二次开发之IQueryFilter接口、ISpatialFilter接口、IQueryDef 接口的查询接口的介绍

一、开发环境 开发环境为ArcGIS Engine 10.2与Visual studio2010。在使用ArcEngine查询进行查询的时候主要使用三种查询接口IQueryFilter&#xff08;属性查询&#xff09; 、ISpatialFilter&#xff08;空间查询&#xff09; 、IQueryDef &#xff08;多表查询&#xff09; 那…...

Oracle 11g RAC部署笔记

搭了三次才搭好&#xff0c;要记录一下。 1. Oracle 11g RAC部署的相关步骤以及需要的包&#xff0c;可以参考这里。 Oracle 11g RAC部署_12006142的技术博客_51CTO博客Oracle 11g RAC部署&#xff0c;Oracle11gRAC部署操作环境&#xff1a;CentOS7.4Oracle11.2.0.4一、主机网…...

Redis 字符串操作实战(全)

目录 SET 存入键值对 SETNX SETEX SETBIT SETRANGE MSET 批量存入键值对 MSETNX PSETEX BITCOUNT 计算值中1的数量 BITOP 与或非异或操作 DECR 减1 DECRBY APPEND 追加 INCR 自增 INCRBY INCRBYFLOAT GET 取值 GETBIT GETRANGE GETSET 取旧值赋新值 MGET …...

python LeetCode 88 刷题记录

题目 给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2&#xff0c;另有两个整数 m 和 n &#xff0c;分别表示 nums1 和 nums2 中的元素数目。 请你 合并 nums2 到 nums1 中&#xff0c;使合并后的数组同样按 非递减顺序 排列。 注意&#xff1a;最终&#xff0c;合并…...

基于 Socket 网络编程

基于 Socket 网络编程 前言一、基于Socket的网络通信传输&#xff08;传输层&#xff09;二、UDP 的数据报套接字编程1、UDP 套接字编程 API2、使用 UDP Socket 实现简单通信 三、TCP 流套接字编程1、TCP 流套接字编程 API2、使用 TCP Socket 实现简单通信3、使用 Tcp 协议进行…...

关于C#.Net网页跳转的7种方法

一、目前在ASP.NET中页面传值共有这么几种方式&#xff1a;1.Response.Redirect("http://www.hao123.com",false); 目标页面和原页面可以在2个服务器上&#xff0c;可输入网址或相对路径。后面的bool值为是否停止执行当前页。 跳转向新的页面&#xff0c;原窗口被代…...

使用acme.sh申请免费ssl证书(Cloudflare方式API自动验证增加DNS Record到期证书到期自动重新申请)

下载acme.sh curl https://get.acme.sh | sh -s emailmyexample.comcd ~/.acme.sh/获取Cloudflare密钥 Preferences | Cloudflare 登录选择账户详情选择API Token选择创建令牌选择区域DNS模板&#xff0c;并设置编辑写入权限生成并复制令牌备用回到首页概览界面下部获取账号…...

【C语言】进阶——结构体+枚举+联合

①前言&#xff1a; 在之前【C语言】初阶——结构体 &#xff0c;简单介绍了结构体。而C语言中结构体的内容还有更深层次的内容。 一.结构体 结构体(struct)是由一系列具有相同类型或不同类型的数据项构成的数据集合&#xff0c;这些数据项称为结构体的成员。 1.结构体的声明 …...

Socket编程基础(1)

目录 预备知识 socket通信的本质 认识TCP协议和UDP协议 网络字节序 socket编程流程 socket编程时常见的函数 服务端绑定 整数IP和字符串IP 客户端套接字的创建和绑定 预备知识 理解源IP和目的IP 源IP指的是发送数据包的主机的IP地址&#xff0c;目的IP指的是接收数据包…...

无线通信——Mesh自组网的由来

阴差阳错找到了一个工作&#xff0c;是做无线通信的&#xff0c;因为无线设备采用Mesh&#xff0c;还没怎么接触过&#xff0c;网上搜索下发现Mesh的使用场景不多&#xff0c;大部分都是用在家里路由器上面。所以写了片关于Mesh网的文档。Mesh网可应用在无网络区域的地方&#…...

LRU、LFU 内存淘汰算法的设计与实现

1、背景介绍 LRU、LFU都是内存管理淘汰算法&#xff0c;内存管理是计算机技术中重要的一环&#xff0c;也是多数操作系统中必备的模块。应用场景&#xff1a;假设 给定你一定内存空间&#xff0c;需要你维护一些缓存数据&#xff0c;LRU、LFU就是在内存已经满了的情况下&#…...

常用工具使用

ubuntu 1.1 ubuntu与windows 互相复制问题 方法一、 打开虚拟机 &#xff0c;点击上方导航栏 ‘虚拟机’ 查看VMware Tools是否安装&#xff0c;安装即可 方法二、 apt-get autoremove open-vm-tools apt-get install open-vm-tools apt-get install open-vm-tools-desktop…...

HashMap源码解析_jdk1.8(一)

HashMap解析 HashMap源码解析_jdk1.8&#xff08;一&#xff09;哈希常用数据结构查找/插入/删除性能比较。哈希冲突 HashMap的数据结构HashMap相关变量size和capacity HashMap源码解析_jdk1.8&#xff08;一&#xff09; 哈希 Hash&#xff0c;一般翻译做“散列”&#xff0…...