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方法:
在这里插入图片描述

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

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

相关文章

Linux基础指令(五)

目录 前言1. 打包和压缩1.1 是什么1.2 为什么1.3 怎么办&#xff1f; 2. zip & unzip3. tar 指令结语&#xff1a; 前言 欢迎各位伙伴来到学习 Linux 指令的 第五天&#xff01;&#xff01;&#xff01; 在上一篇文章 Linux基本指令(四) 当中&#xff0c;我们学习了 fin…

git --- 基础介绍

git --- 基础介绍 git 是什么git --- 工作区, 暂存区, 资源库git --- 文件状态git --- branch 和 HEADgit --- 一次正常的git提交流程 git 是什么 Git是一款分布式源代码管理工具(版本控制工具)Git和其他传统版本控制系统比较: 传统的版本控制系统(例如 SVN)是基于差异的版本控…

回归预测 | MATLAB实现基于RF-Adaboost随机森林结合AdaBoost多输入单输出回归预测

回归预测 | MATLAB实现基于RF-Adaboost随机森林结合AdaBoost多输入单输出回归预测 目录 回归预测 | MATLAB实现基于RF-Adaboost随机森林结合AdaBoost多输入单输出回归预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 1.MATLAB实现基于RF-Adaboost随机森林结合…

mac 解决 vscode 权限不足问题,Insufficient permissions

commod 空格&#xff0c;输入终端并打开写入指令 sudo chown -R xxxxxx1 xxxxx2&#xff08;例如我的sudo chown -R admin Desktop&#xff0c;具体参数查看下方&#xff09; x1: 用户名&#xff0c;可通过左上角查看 x2: 目标文件夹。可以另起一个终端&#xff0c;用cd 和 l…

vue+element项目创建步骤

一、创建vue项目步骤 要创建一个Vue Element UI的项目&#xff0c;你可以按照以下步骤进行操作&#xff1a; 1.确保你已经安装了Node.js和npm&#xff08;Node.js的包管理器&#xff09;。你可以在命令行中运行以下命令来检查它们是否已经安装&#xff1a; node -vnpm -v2.使…

《开发实战》18 | 数据存储:NoSQL与RDBMS如何取长补短、相辅相成?

取长补短之 Redis vs MySQL 做一个简单测试&#xff0c;分别填充 10 万条数据到 Redis 和 MySQL 中。MySQL 中的 name字段做了索引&#xff0c;相当于 Redis 的 Key&#xff0c;data 字段为 100 字节的数据&#xff0c;相当于 Redis 的Value。在我的电脑上&#xff0c;使用 wr…

基于SpringBoot的企业客户管理系统的设计与实现【附PPT|万字文档(LW)和搭建文档】

主要功能 后台登录&#xff1a; 可注册员工&#xff1a;工号、密码、姓名、身份证、手机、邮箱 员工&#xff1a; ①首页、个人中心、修改密码、个人信息 ②客户信息管理、项目信息管理、项目收益管理等 后台登录&#xff1a; 管理员&#xff1a; ①首页、个人中心、修改密码、…

一文了解什么SEO

搜索引擎优化 (SEO) 是一门让页面在 Google 等搜索引擎中排名更高的艺术和科学。 一、搜索引擎优化的好处 搜索引擎优化是在线营销的关键部分&#xff0c;因为搜索是用户浏览网络的主要方式之一。 搜索结果以有序列表的形式呈现&#xff0c;网站在该列表中的排名越高&#x…

小样本规模船型优化策略的选择研究

天洑软件基于自研智能优化软件AIPOD在船舶行业的应用发表论文《小样本规模船型优化策略的选择研究》刊录于核心期刊《中国造船》。全文如下&#xff1a; 小样本规模船型优化策略的选择研究 陈骏喆&#xff0c;姜 栋&#xff0c;张 儒&#xff0c;张 明 &#xff08;南京天洑…

UniAccess Agent卸载

异常场景&#xff1a; UniAccess Agent导致系统中的好多设置打不开 例如:ipv4的协议,注册表,host等等 需要进行删除,亲测有效,及多家答案平凑的 借鉴了这位大神及他里面引用的大神的内容 https://blog.csdn.net/weixin_44476410/article/details/121605455 问题描述 这个进…

权限提升Linux篇

提权工具 https://github.com/liamg/traitor https://github.com/AlessandroZ/BeRoot https://github.com/rebootuser/LinEnum https://github.com/mzet-/linux-exploit-suggester https://github.com/sleventyeleven/linuxprivchecker https://github.com/jondonas/linux…

Git学习笔记4

GitHub是目前最火的开源项目代码托管平台。它是基于web的Git仓库&#xff0c;提供公有仓库和私有仓库&#xff0c;但私有仓库是需要付费的。 到Github上找类似的项目软件。 GitLab可以创建免费的私有仓库。 GitLab是利用 Ruby开发的一个开源的版本管理系统&#xff0c;实现一个…

如何使用Docker安装最新版本的Redis并设置远程访问(含免费可视化工具)

文章目录 安装Docker安装Redisredis.conf文件远程访问Redis免费可视化工具相关链接Docker是一种开源的应用容器引擎,使用Docker可以让我们快速部署应用环境,本文介绍如何使用Docker安装最新版本的Redis。 安装Docker 首先需要安装Docker,具体的安装方法可以参考Docker官方文…

使用 rtty 进行远程 Linux 维护和调试

rtty 是一个用于在终端上进行远程连接和数据传输的工具。它提供了一种简单的方式来与远程设备进行通信&#xff0c;使得在不同主机之间传输数据变得更加方便。 安装 rtty 是一个可执行程序&#xff0c;可以在 Linux、macOS 和 Windows 等平台上使用。 Linux/macOS 在终端中执…

开发者必备!如何将闲置iPad Pro打造为编程工具,使用VS Code编写代码

文章目录 前言1. 本地环境配置2. 内网穿透2.1 安装cpolar内网穿透(支持一键自动安装脚本)2.2 创建HTTP隧道 3. 测试远程访问4. 配置固定二级子域名4.1 保留二级子域名4.2 配置二级子域名 5. 测试使用固定二级子域名远程访问6. ipad pro通过软件远程vscode6.1 创建TCP隧道 7. ip…

Java-day17(反射)

Reflection(反射) 动态语言的关键 允许程序在执行期借助于Reflection API取得任何类的内部信息&#xff0c;并能直接操作任意对象的内部属性及方法提供的功能: 在运行时判断任意一个对象所属类 在运行时构造任意一个类的对象 在运行时判断任意一个类所具有的成员变量和方法 在…

怒刷LeetCode的第15天(Java版)

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

基于SpringBoot的在线文档管理系统

目录 前言 一、技术栈 二、系统功能介绍 管理员功能模块 员工功能模块 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势…

ArcGIS 10.3软件安装包下载及安装教程!

【软件名称】&#xff1a;ArcGIS 10.3 【安装环境】&#xff1a;Windows 【下载链接 】&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1K5ab7IHMYa23HpmuPkFa1A 提取码&#xff1a;oxbb 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 软件解压码点击原文…

java面试题-jvm基础知识

1 JVM组成 1.1 JVM由那些部分组成&#xff0c;运行流程是什么&#xff1f; 难易程度&#xff1a;☆☆☆ 出现频率&#xff1a;☆☆☆☆ JVM是什么 Java Virtual Machine Java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09; 好处&#xff1a; 一次编写&a…