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.rhkb.cn/news/141374.html

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

相关文章

屏幕分辨率dpi解析(adb 调试查看)

authordaisy.skye的博客_CSDN博客-嵌入式,Qt,Linux领域博主 ro.sf.lcd_density属性指定了这个机型使用的dpi是多少&#xff0c;dpi全称是dots per inch&#xff0c;对角线每英寸的像素点的个数。 密度 ldpi mdpi hdpi xhdpi xxhdpi 分辨率 240x320 320x480 480x800 7…

2023-9-25 耍杂技的牛

题目链接&#xff1a;耍杂技的牛 #include <iostream> #include <algorithm>using namespace std;typedef pair<int, int> PII;const int N 50010;int n; PII cow[N];int main() {cin >> n;for(int i 0; i < n; i ){int w, s;cin >> w >…

Android 10.0 系统开启和关闭黑白模式主题功能实现

1. 概述 在10.0的rom系统开发定制化中,在系统SystemUI的下拉状态栏中,产品开发功能需求要求添加黑白模式功能开关的功能,就是打开黑白模式,系统颜色就会变成黑白颜色, 关闭黑白模式开关系统就会变成彩色模式,所以就需要了解下系统是怎么设置黑白模式和彩色模式的,然后添…

接口自动化测试之Mock

【软件测试面试突击班】如何逼自己一周刷完软件测试八股文教程&#xff0c;刷完面试就稳了&#xff0c;你也可以当高薪软件测试工程师&#xff08;自动化测试&#xff09; 1.Mock实现原理和实现机制 在某些时候&#xff0c;后端在开发接口的时候&#xff0c;处理逻辑非常复杂&a…

Android跨进程通信:Binder机制原理

目录 1. Binder到底是什么&#xff1f; 2. 知识储备 2.1 进程空间划分 2.2 进程隔离 & 跨进程通信&#xff08; IPC &#xff09; 2.3 内存映射 2.3.1 作用 2.3.2 实现过程 2.3.3 特点 2.3.4 应用场景 2.3.5 实例讲解 ① 文件读 / 写操作 ② 跨进程通信 3. Bi…

C#中的(++)和(--)运算符

目录 背景: 的前加 效果展示:​ 的后加 效果展示 :​ 总结: 背景: 自增和自减运算符存在于C/C/C#/Java等高级语言中&#xff0c;它的作用是在运算结束前(前置自增自减运算符 )或后(后置自增自减运算符 )将 变量的值加(或减)1。 在C#中&#xff0c;和--是自增和自减运…

基于springboot小区疫情防控系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

前端Vue3+element-plus表单输入框实现Cron表达式校验

页面如下&#xff1a; 本来想手写正则表达式校验&#xff0c;结果发现很麻烦&#xff0c;cron表达式组成如下&#xff1a; 开发使用框架为vue3element-plus&#xff0c;于是选择cron-validator依赖。使用步骤如下&#xff1a; 1、通过npm install cron-validator命令安装&…

面经分享 | 某康安全开发工程师

本文由掌控安全学院 - sbhglqy 投稿 一、反射型XSS跟DOM型XSS的最大区别 DOM型xss和别的xss最大的区别就是它不经过服务器&#xff0c;仅仅是通过网页本身的JavaScript进行渲染触发的。 二、Oracle数据库了解多吗 平常用的多的是MySQL数据库&#xff0c;像Oracle数据库也有…

想要精通算法和SQL的成长之路 - 最长回文子序列

想要精通算法和SQL的成长之路 - 最长回文子序列 前言一. 最长回文子序列 前言 想要精通算法和SQL的成长之路 - 系列导航 一. 最长回文子序列 原题链接 首先&#xff0c;我们看下动态规划方程的定义&#xff0c;我们用dp[i][j] 来代表&#xff1a;字符串s在下标区间为[i,j]之间…

Unity入门教程(上)

七、运行游戏 再次保存我们的项目文件&#xff08;返回步骤四&#xff09;。保存完成后&#xff0c;让我们把游戏运行起来。 1&#xff0c;确认游戏视图标签页右上方的Maximize on Play图标处于按下状态&#xff0c;然后点击画面上方的播放按钮&#xff08;位于工具栏中间的播…

网络竞品分析:用爬虫技术洞悉竞争对手

概述 网络竞品分析是指通过互联网收集、分析和比较竞争对手的信息&#xff0c;以了解他们的优势和劣势&#xff0c;找出自己的差距和机会&#xff0c;制定有效的竞争策略。网络竞品分析涉及的信息包括竞争对手的产品、价格、渠道、营销、用户反馈等方面。爬虫技术是一种自动化…

看板系统如何异地电脑手机访问?主机内网ip端口映射域名外网访问

看板系统是一种可视化管理系统平台&#xff0c;如生产管理看板、项目管理看板、APP运营看板等将企业或工厂本地项目具体数据转换成图表模式&#xff0c;方便实时管理和汇总&#xff0c;有效提升工作效率和助力生产实践。 单位内部服务器部署了看板管理系统&#xff0c;由于无公…

Wespeaker框架数据集准备(1)

1. 数据集准备(Data preparation) 进入wespeaker目录文件/home/username/wespeaker/examples/voxceleb/v2 对run.sh文件进行编辑 vim run.sh 可以看到run.sh里面的配置内容 #数据集下载&#xff0c;解压 stage1 #插入噪音&#xff0c;制作音频文件 stop_stage2 #数据集放置…

【密码学补充知识】

&#x1f511;密码学&#x1f512;概述 &#x1f4d5; 1.基本概念 明文 &#xff1a; 要交换的信息 密文 &#xff1a; 明文经过一组规则变换成看似没有意义的随机消息。 加密 &#xff1a; 明文经过一组规则变换成密文的过程 解密 &#xff1a; 密文恢复出明文的过程 加…

2023-Chrome插件推荐

Chrome插件推荐 一键管理扩展 链接 https://chromewebstore.google.com/detail/lboblnfejcmcaplhnbkkfcienhlhpnni 介绍 一键开启、禁用Chrome插件。 Checker Plus for Gmail™ 链接 https://jasonsavard.com/zh-CN/Checker-Plus-for-Gmail https://chromewebstore.goo…

基于springboot+vue的重庆旅游网(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

小程序中如何导出会员卡的档案信息

对于医院、美容院等特殊商家&#xff0c;可能需要在给会员添加一些档案。例如今天客户是什么情况&#xff0c;做了什么服务&#xff0c;解决了什么问题。添加这些档案后&#xff0c;系统会保存这些信息&#xff0c;供下次来的时候使用&#xff0c;或者为商家日后做营销提供依据…

Leetcode 95. 不同的二叉搜索树 II

文章目录 题目代码&#xff08;9.21 首刷看解析&#xff09; 题目 Leetcode 95. 不同的二叉搜索树 II 代码&#xff08;9.21 首刷看解析&#xff09; class Solution { public:vector<TreeNode*> generateTrees(int n) {return build(1,n);}vector<TreeNode*> bu…

蓝桥杯每日一题2023.9.25

4406. 积木画 - AcWing题库 题目描述 分析 在完成此问题前可以先引入一个新的问题 291. 蒙德里安的梦想 - AcWing题库 我们发现16的二进制是 10000 15的二进制是1111 故刚好我们可以从0枚举到1 << n(相当于二的n次方的二进制表示&#xff09; 注&#xff1a;奇数个0…