关于LiveData全面详解(附事件总线)

news2024/11/17 3:32:40

前言:缤纷色彩闪出的美丽 是因它没有 分开每种色彩

前言

MVVM 架构模式中,ViewModel 是不会持有宿主的信息,业务逻辑在 ViewModels 层中完成,而不是在 Activities 或 Fragments 中。LiveData 在里面担任数据驱动的作用:

在这里插入图片描述

以往我们使用 Handler,EventBus,RxjavaBus 进行消息通信,LiveData 也是一个种观察者模式,作用跟 RxJava 类似,是观察数据的类,相比 RxJava,一般配合 Jetpack 组件配合使用。它能够在 Activity、Fragment 和 Service 之中正确的处理生命周期。

一、什么是LiveData

1.介绍

LiveData 组件是 Jetpack 新推出的基于观察者的消息订阅/分发组件,具有宿主(Activity/Fragment)生命周期感知能力。这种感知能力可确保 LiveData 仅分发消息给与活跃状态的观察者,即只有处于活跃状态的观察者才能收到消息。

LiveData 的消息分发机制,是以往 Handler,EventBus,RxjavaBus 无法比拟的,它们不会顾及当前页面是否可见,一股脑的有消息就转发。导致即便应用在后台,页面不可见,还在做一些无用的绘制,计算(微信的消息列表是在可见状态下才会更新列表最新信息的)将有限的资源让给可见的页面使用。

活跃状态:Observer所在宿主处于STARTED,RESUMED状态。

本文需要 Lifecycle 的相关知识作为基础,如果有不了解的可以先看看《Android架构灵魂组件Lifecycle的生命周期机制详解》

基于生命周期,其实就是在当前宿主 LifecycleOnwer 注册一个 Observer,那么宿主每次生命周期的变化,都会回调给观察者的 onStateChange() 方法,即便是刚刚注册的观察者宿主也会回调 onStateChange() 方法,会有一个状态同步的过程,LiveData 也是利用这个能力,巧妙实现了当宿主销毁的时候,自动移除注册进来的 Observer,从而避免了手动移除的麻烦。更不会造成内存泄漏,这个也是它的核心思想。

2.LiveData的特点

使用 LiveData 具有以下几点优势:

  • 确保界面符合数据状态:LiveData 遵循观察者模式。当底层数据发生变化时,LiveData 会通知 Observer 对象。
  • 不会发生内存泄漏:观察者会绑定到 Lifecycle 对象,并在其关联的生命周期遭到销毁后进行自我清理。
  • 不会因 Activity 停止而导致崩溃:如果观察者的生命周期处于非活跃状态(如返回堆栈中的 Activity),它便不会接收任何 LiveData 事件。
  • 不再需要手动处理生命周期:界面组件只是观察相关数据,不会停止或恢复观察。LiveData 将自动管理所有这些操作,因为它在观察时可以感知相关的生命周期状态变化。
  • 数据始终保持最新状态:如果生命周期变为非活跃状态,它会在再次变为活跃状态时接收最新的数据。
  • 适当的配置更改:如果由于配置更改(如设备旋转)而重新创建了 Activity 或 Fragment,它会立即接收最新的可用数据。
  • 共享资源:您可以使用单例模式扩展 LiveData 对象以封装系统服务,以便在应用中共享它们。LiveData 对象连接到系统服务一次,然后需要相应资源的任何观察者只需观察 LiveData 对象。

LiveData不足之处:

  • 粘性事件不支持取消(后面注册的观察者也能接收数据,无法反注册,但有办法解决)。

3.LiveData核心方法

方法名作用
observe(LifecycleOwner owner, Observer observer)注册和宿主生命周期关联的观察者, owner当前生命周期的宿主,当宿主销毁了observer能自动解除注册
observeForever(Observer observer)注册观察者,不会反注册,需自行维护,没有owner无法管理宿主生命周期
setValue(T value)发送数据,没有活跃的观察者时不分发,只能在主线程
postValue(T value)setValue一样,但是不受线程限制,内部也是通过handelr.post到主线程,最后还是通过setValue来分发的
onActive()当且仅当有一个活跃的观察者时会触发
onInactive()不存在活跃的观察者时会触发

二、LiveData的几种用法

1.MutableLiveData

在使用 LiveData 做消息分发的时候,需要使用这个子类,设计的原因是考虑到单一开闭原则,只有拿到 MutableLiveData 才可以发送消息,LiveData 只能接收消息,避免拿到 LiveData 既能发送消息又能接收消息的混乱使用

public class MutableLiveData<T> extends LiveData<T> {
    @Override
    public void postValue(T value) {
        super.postValue(value);
    }

    @Override
    public void setValue(T value) {
        super.setValue(value);
    }
}

MutableLiveData 仅仅是把上面两个方法从父类的 protect 改为 public 而已,因为在 LiveData 中无法调用 postValue()setValue()。所以我们在使用 LiveData 做消息分发的时候,我们需要使用它的子类 MutableLiveData。

fun mediatorLiveData() {
    val liveData1 = MutableLiveData<String>()
    val liveData2 = MutableLiveData<String>()
    //在创建一个聚合类MediatorLiveData
    val mediatorLiveData = MediatorLiveData<String>()
    //分别把LiveData合并到mediatorLiveData中
    mediatorLiveData.addSource(liveData1, Observer { data ->
        mediatorLiveData.value = data
    })
    mediatorLiveData.addSource(liveData2, Observer { data ->
        mediatorLiveData.value = data
    })

    //数据监听,一旦liveData1或者LiveData2发送了数据,observer便能观察到,以便统一处理更新
    mediatorLiveData.observe(this, Observer { data ->
        LogUtil.e("mediatorLiveData:$data")
    })

    // 模拟发送数据
    liveData1.postValue("liveData1 苏火火苏火火")
    liveData2.postValue("liveData2 苏火火苏火火")
}

打印数据如下:

/com.sum.tea E/LogUtil: mediatorLiveData: liveData1 苏火火苏火火
/com.sum.tea E/LogUtil: mediatorLiveData: liveData2 苏火火苏火火

3.Transformations.map操作符

Transformations.map 可以对 LiveData 的数据在分发给观察者之前进行转换,并且返回一个新的 LiveData 对象。

fun transformationsMap() {
    val mapLiveData = MutableLiveData<Int>()

    //数据转换
    val transformLiveData: LiveData<Int> = Transformations.map(mapLiveData) { input ->
        input * 2
    }

    //使用转换后生成的transformLiveData去观察数据
    transformLiveData.observe(this) { output ->
        LogUtil.e("transformationsMap 数据转换变化:$output")
    }

    //使用原始的LiveData发送数据
    mapLiveData.value = 10
}

打印数据如下:

/com.sum.tea E/LogUtil: transformationsMap 数据转换变化:20

三、LiveData实现原理

1.粘性事件分发流程

先从 LiveData 注册观察者看起:

#LiveData.java
@MainThread
public void observe(LifecycleOwner owner, Observer<? super T> observer) {
    // 如果宿主是DESTROYED状态则直接退出
    if (owner.getLifecycle().getCurrentState() == DESTROYED) {
        return;
    }
    // 把observer包装一个具有生命周期边界的观察者
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    // 存储到mObservers集合
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);

    if (existing != null) {
        return;
    }
    // 注册到宿主的生命周期里面
    owner.getLifecycle().addObserver(wrapper);
}
  1. 把 observer 包装成了 LifecycleBoundObserver,它是一个具有生命周期边界的观察者,它是 LifecycleEventObserver 的子类,
  2. 接着把 LifecycleBoundObserver 存储到 mObservers 集合当中。
  3. 最后把 LifecycleBoundObserver 注册到宿主的生命周期里面。

所以 wrapper 就能接收到宿主生命周期变化的事件,当第一次注册进去的时候也会触发状态的同步,也能接收到完整的生命周期事件。

因为后面还要做数据的分发,订阅消息就是把这个 Observer 包装成 LifecycleBoundObserver,然后存储到 mObservers 集合当中,有消息的时候就遍历这个集合去分发。

// 具有生命周期边界能力的Observer
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {

    @Override
    boolean shouldBeActive() {
        // 判断观察者是否处于活跃的状态
        return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
    }

    @Override
    public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
        // 判断当前宿主的状态是否为destory
        if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
            // 主动进行反注册,把Observer移除掉
            removeObserver(mObserver);
            return;
        }
        // 状态改变
        activeStateChanged(shouldBeActive());
    }
}

宿主生命周期每一次事件的通知都会回调到 LifecycleEventObserver 的 onStateChanged(),先判断当前宿主的状态是否为 DESTORYED,如果是则主动进行反注册,把 Observer 移除掉。从而主动避免内存泄漏的问题。

如果不是 DESTORYED,那就说明宿主的状态发生了别的变化,触发 activeStateChanged(shouldBeActive()) 这个方法,会先判断观察者是否处于活跃的状态,只有处于活跃状态的观察者才能接收到数据:

// 注册观察者,不会反注册,需自行维护,没有owner无法管理宿主生命周期
public void observeForever(Observer<? super T> observer) {
    //把`observer`包装成一个`AlwaysActiveObserver`对象
    AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer);
    // 将Observer存储到mObservers集合中
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    // 设置为tru则不管宿主是否处于可见状态,一直接收数据
    wrapper.activeStateChanged(true);
}
  • observeForever():它会把 observer 包装成一个 AlwaysActiveObserver 对象, shouldBeActive() 永远为 true,不管你的宿主是否处于可见状态,这就意味着它可以一直接收数据。

除了 observeForever() 这种情况外,观察者是否处于活跃状态其实就等于宿主是否处于活跃的状态。

如果场景是在后台要处理一些事情,可以使用 observeForever() 注册观察者,但是需要在宿主被销毁的时候取消注册,或者使用传统的 callback 形式。

void activeStateChanged(boolean newActive) {
    // 如果状态一致,则退出
    if (newActive == mActive) {
        return;
    }
    // 立即设置状态,就不会分发给非活跃状态
    mActive = newActive;
    // 非活跃状态,mActiveCount表示活跃状态数量
    boolean wasInactive = LiveData.this.mActiveCount == 0;
    LiveData.this.mActiveCount += mActive ? 1 : -1;
    // 注册第一个观察者并且是活跃状态
    if (wasInactive && mActive) {
        // 只有一个观察者时才会触发
        onActive();
    }
    if (LiveData.this.mActiveCount == 0 && !mActive) {
        // 没有观察者时触发
        onInactive();
    }
    if (mActive) {
        // 活跃状态,开始分发数据
        dispatchingValue(this);
    }
}

首先判断 mActiveCount == 0,如果等于0说明里面的观察者没有一个处于活跃的状态。在注册第一个观察者的时候,活跃观察者的数量肯定是等于0的,当注册了第一个观察者之后,它的状态就会发生变化,变成 mActive,此时就会触发 onActive() 方法,如果没有任何一个观察者就会触发 onInactive() 方法,如果 mActive == true,则说明当前观察者处于活跃状态,它是可以接收数据的。

// 分发数据
void dispatchingValue(ObserverWrapper initiator) {
    // ······
    do {
        mDispatchInvalidated = false;
        if (initiator != null) {
            // 1.把数据分发给自己
            considerNotify(initiator);
            initiator = null;
        } else {
            // 2.有新的数据,把mObservers集合当中所有的观察者遍历分发数据
            for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
                    mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                considerNotify(iterator.next().getValue());
                if (mDispatchInvalidated) {
                    break;
                }
            }
        }
    } while (mDispatchInvalidated);
    mDispatchingValue = false;
}

// 设置数据
protected void setValue(T value) {
    mVersion++;
    mData = value;
    dispatchingValue(null);
}
  1. 如果 initiator 为空,本次分发数据就把数据分发给自己 considerNotify(initiator)
  2. 如果 initiator 不为空,一般是从 setVlue() 过来的,这时候就说明有了新的数据,就会把 mObservers 集合当中所有的观察者遍历分发数据。

无论是新注册的观察者还是 setVlue() 触发的消息分发,都会调用 considerNotify() 方法

// 准备通知更新,宿主在恢复活跃状态时也会执行到这里
private void considerNotify(ObserverWrapper observer) {
    // observer非活跃状态直接退出
    if (!observer.mActive) {
        return;
    }
    // 也许它改变了状态,但我们还没有得到事件
    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    // observer的版本大于LiveData版本不会发送数据,避免多次重复发送 
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    // 同步观察者的Version数据
    observer.mLastVersion = mVersion;
    observer.mObserver.onChanged((T) mData);
}

这里是数据最终发送的地方,先判断观察者是否处于活跃状态,如果说观察者不活跃,就会 return,当宿主恢复活跃状态的时候就会触发 onStateChanged(),最后也会到这里。

重点来了,LiveData 的 Version 字段和 Observer 的 Vierson 在刚开始创建的时候都是-1,如果 LiveData 已经发送数据了,它的 Version 字段就会加1,如果这个时候新注册了一个 Observer,那么在触发消息分发的时候,这两个字段就不相等,所以 Observer 就能接收到之前发送的消息,在第一次注册的数据的时候(先发数据,后注册的 Observer 也会收到数据)

这就是粘性事件,目的是为了避免数据多次重复发送,因为每次生命周期的变化都会走到这里。最后调用 observer.mObserver.onChanged() 回调数据。

在这里插入图片描述

这就是一个新的注册的 Observer 是如何接收到之前发送的数据的流程。

2.普通消息分发流程

普通消息分发流程即调用 postValue()setValue() 才会触发消息的分发。

postValue() 发送数据的流程:

#LiveData.java
// 不限制线程,主线程,子线程都可以调用
protected void postValue(T value) {
    boolean postTask;
    // 加锁
    synchronized (mDataLock) {
        postTask = mPendingData == NOT_SET;
        mPendingData = value;
    }
    if (!postTask) {
        return;
    }
    // 发送到主线程执行
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

因为需要 Handler 把这个消息 post 到主线程里面,所以需要把传递进来的 value 保存 mPendingData,当这条消息被执行的时候,就会触发 mPostValueRunnable,里面实际也是调用 setValue(),把上面存储的 mPendingData 传递进去。

private final Runnable mPostValueRunnable = new Runnable() {
    @Override
    public void run() {
        Object newValue;
        synchronized (mDataLock) {
            newValue = mPendingData;
            mPendingData = NOT_SET;
        }
        // 执行setValue()
        setValue((T) newValue);
    }
};

无论是从哪里发送的数据,接收的地方始终都会发送在主线程,每发送一条消息 mVersion 增加了1,在消息派发的时候就会和 Observer 的 version 进行对比,防止消息重复发送的问题

就会调用 dispatchingValue(null) 传递了 null,根据上面的分析,如果参数为 null,就会遍历 mObserver 集合中的观察者去逐一判断是否能把数据分发给他们。

@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    // 分发数据
    dispatchingValue(null);
}

setValue() 里面调用 dispatchingValue(null) 进行分发数据,就又回到了上面的流程。

在这里插入图片描述

四、总结

1.粘性事件分发流程

  1. 通过 observe(owner,observer) 向 LiveData 注册观察者,并且把 observer 包装成一个 LifecycleBoundObserver,它是一个具有生命周期边界的观察者,因为这个观察者只有当宿主处于 STARTED 或者 RESUMED 状态的它才会接收数据,其他时候它是不会接收数据的。

  2. 把包装好的 Observer 注册到 Lifecycle 当中,handlerLifecycleEvent(event) 利用 Lifecycle 能力,它能感知宿主生命周期能力的关键地方。注册时和宿主每次生命周期变化都会回调 onStateChanged() 方法,刚进去的时候会触发方法的同步。

  3. 会判断这个事件宿主是否被销毁了,从而主动地把 Observer 从 LiveData 中移除掉,流程结束。如果不是 DESTORY,说明宿主当前的状态发生了变化,它会触发 activeStateChanged(boolean newActive) 方法,它会判断当前 Observer 是否处于活跃的状态,如果宿主的状态为 STARTED,RESUMED 则会分发最新数据到每个观察者。

  4. 进而调用 dispatchingValue(ObserverWrapper) 分发数据,如果 ObserverWrapper 为空则分发数据给 liveData 中存储的所有观察者,如果不为空,则分发数据给该 Observer。

  5. considerNotify(ObserverWrapper) 中先判断观察者所在的宿主不活跃,则不分发;接着如果 observer 的 mLastVersion 大于或等于 LiveData 的 mVersion 则不分发,防止重复发送数据;最后通过 observer.mObserver.onChanged((T) mData) 分发数据,同步 mVersion 数据。

那么 LiveData 先发送数据,后注册的 Observer 能接收到数据吗? 答案是可以的。

2.普通消息发送流程

  1. postValue() 发送一条数据,它可以在任意线程使用的,里面实际使用了 Handler.post 先把这个事件发送到主线程,然后在调用 setValue() 发送数据;

  2. setValue() 代表着 LiveData 发送数据,每发送一次 mVersion++,另外 LifecycleBoundObserver 中也有一个,它代表这个 Observer 接收了几次数据,在分发数据的时候,这两个 version 会进行比对,防止数据重复发送;

  3. setValue() 里面也会触发 dispatchingValue(ObserverWrapper),ObserverWrapper 为 null,dispatchingValue() 它会遍历 Observer 集合里面所有观察者,然后逐一调用 considerNotify(ObserverWrapper) 去做消息的分发。

五、使用LiveData打造消息总线

基于 LiveData 打造一款不会内存泄漏不用反注册的消息总线,且支持粘性事件。

// 事件总线
private fun liveDataBus() {
    LiveDataBus.with<String?>("eventName").observeSticky(this, { data ->
        mBinding.tvUserInfo.text = data
        LogUtil.e("事件总线 数据变化:$data")
    }, true)
}
  • 问题:LiveData 默认是支持粘性事件的,而且无法取消。
private void considerNotify(ObserverWrapper observer) {
    //观察者没有处于活跃状态,则不分发
    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    //观察者接受的消息的次数>=liveData发送消息的次数,不分发
    //如果之前已经发送过数据,新注册的Observer也能接受到最后一条数据
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    
    //根本原因在于ObserverWrapper的version字段在创建时=-1,没有主动和LiveData字段的mVersion字段对齐
    observer.mLastVersion = mVersion;
    observer.mObserver.onChanged((T) mData);
}

满足上面两个条件才会触发 observer.mObserver.onChanged() 事件的分发:

  1. 首先判断 observer.shouldBeActive() 是否处于活跃状态,如果 observer 是根据 observer() 注册的,这个观察者是否处于活跃就等于它的宿主是否处于活跃状态,如果是用 observerForever() 注册的,它就一直处于活跃状态。这里不需要控制它,能很好完成与宿主生命周期相关联。

  2. 关键在于 observer.mLastVersion >= mVersion,mLastVersion 和 mVersion 默认值都是为-1,在首次注册流程分发的时候,如果 LiveData 之前发送过数据,mVersion 就会为1,而新注册进来的 Observer 的 mLastVersion 还是-1,上就会往下执行消息分发。这样就导致了新注册的 Observer 也能够接收到之前发送消息的最后一条数据。

  3. 但是如果我们在新注册 Observer 的时候主动和 LiveData 的 mVersion 对齐,保持一致,那么就不会继续分发消息,这是根本原因。在于 Observer 的 mLastVersion 的值,但是 LifecycleBoundObserver 这个类我们是访问不到的,我们也不能直接控制 mLastVersion 的值,但是我们可以重新包装 Observer。

//包装StickyObserver,有新的消息会回调onChanged方法,从这里判断是否要分发这条消息
//这只是完成StickyObserver的包装,用于控制事件的分发,但是事件的发送还是依靠LiveData来完成的
internal class StickyObserver<T>(
    liveData: StickyLiveData<T>,
    observer: Observer<T>,
    sticky: Boolean
) : Observer<T> {
    private val mLiveData: StickyLiveData<T>
    private val mObserver: Observer<T>

    //是否开启粘性事件,为false则只能接受到注册之后发送的消息,如果需要接受粘性事件则传true
    private val mSticky: Boolean

    //标记该Observer已经接收几次数据了,过滤老数据防止重复接收
    private var mLastVersion = 0

    init {
        //比如先使用StickLiveData发送了一条数据,StickLiveData#version=1
        //那么当创建WrapperObserver注册进去的时候,需要把它的version和StickLiveData的version保持一致
        mLastVersion = liveData.mVersion
        mLiveData = liveData
        mSticky = sticky
        mObserver = observer
    }

    override fun onChanged(t: T) {
        if (mLastVersion >= mLiveData.mVersion) { //如果相等则说明没有更新的数据要发送
            //但是如果当前Observer是关系粘性事件的,则分发给他
            if (mSticky && mLiveData.mStickyData != null) {
                mObserver.onChanged(mLiveData.mStickyData)
            }
            return
        }
        mLastVersion = mLiveData.mVersion
        mObserver.onChanged(t)
    }
}

扩展 LiveData,支持粘性事件的订阅,分发的 StickyLiveData:

//扩展LiveData,支持粘性事件的订阅,分发的StickyLiveData
class StickyLiveData<T>(private val mEventName: String) : LiveData<T>() {
    var mStickyData: T? = null
    // 版本标记
    var mVersion = 0
    // 事件存储集合
    var mHashMap: ConcurrentHashMap<String, StickyLiveData<T>>? = null

     //调用mVersion++
     //注册一个Observer的时候,把它包装一下,目的是为了让Observer的version和LiveData的version对齐
     //但是LiveData的version字段拿不到,所以需要管理version,在对齐的时候
    override fun setValue(value: T?) {
        mVersion++
        super.setValue(value)
    }

    override fun postValue(value: T?) {
        mVersion++
        super.postValue(value)
    }

    //发送粘性事件,只能在主线程发送数据
    fun setStickData(stickyData: T) {
        mStickyData = stickyData
        value = stickyData
    }

    //发送粘性事件,不受线程限制
    fun postStickData(stickyData: T) {
        mStickyData = stickyData
        postValue(stickyData)
    }

    override fun observe(owner: LifecycleOwner, observer: Observer<in T?>) {
        observeSticky(owner, observer, false)
    }

     //暴露方法,是否关心之前发送的数据,再往宿主上面添加一个addObserver监听生命周期事件,如果是DESTORYED则主动移除LiveData
     //sticky  是否为粘性事件,sticky=true,如果之前存在已经发送数据,那么Observer就会收到之前的粘性事件消息
    fun observeSticky(owner: LifecycleOwner, observer: Observer<in T>, sticky: Boolean) {
        owner.lifecycle.addObserver(LifecycleEventObserver { source, event ->
            if (event == Lifecycle.Event.ON_DESTROY) {
                mHashMap?.remove(mEventName)
            }
        })
        super.observe(owner, StickyObserver(this, observer as Observer<T>, sticky))
    }
}

这个还有另外一种处理方式就是通过反射,获取 LiveData 中的 mVersion 字段,来控制粘性事件的分发。

一个大型的 Android 项目架构最佳实践,基于Jetpack组件 + MVVM架构模式,加入 组件化模块化协程Flow短视频。项目地址:https://github.com/suming77/SumTea_Android

点关注,不迷路


好了各位,以上就是这篇文章的全部内容了,很感谢您阅读这篇文章。我是suming,感谢各位的支持和认可,您的点赞就是我创作的最大动力。山水有相逢,我们下篇文章见!

本人水平有限,文章难免会有错误,请批评指正,不胜感激 !

参考链接:

  • LiveData官网

希望我们能成为朋友,在 Github博客 上一起分享知识,一起共勉!Keep Moving!

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

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

相关文章

ElementPlus的Collapse 折叠面板问题

我也不明白为什么会报这个错误&#xff0c;把关于ts的代码改成js的就可以了。。 ERROR in ./src/views/Home.vue?vue&typescript&langts&setuptrue (./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/views/Home.vue?vue&typescript&…

大厂经验,基于资产健康度量化的小米数据治理实践

摘要&#xff1a;随着小米公司各项业务的快速发展&#xff0c;数据中的商业价值也愈发突显。而与此同时&#xff0c;各业务团队在数据查询、分析等方面的压力同样正在剧增。小米大数据团队用大数据管理大数据&#xff0c;从存储、计算、规范、质量、安全五方面着手治理&#xf…

java swing实现JTextField文本框的输入提示补全功能,以登陆界面自动提示补全用户账号为例,自动填充账号密码

前言 如果您想了解更多的java项目功能源码,请订阅我的专栏: java项目源码合集100+ 一、实现的功能 在使用java做界面实现输入功能的时候,使用了JTextField文本输入组件,希望能够根据用户的输入,自动提示,之后用户可以选择提示的内容作为文本框的输入内容,效果如下: 当…

Java中的阻塞队列使用以及详解

文章目录 一、Queue接口1. 常见方法以及功能(不具有阻塞队列特性)1.1 add(E e)1.2. offer():1.3. remove()1.4. poll()1.5. element()1.6. peek() 2. add和offer对比&#xff1a;3. remove和poll对比&#xff1a;4. element#peek方法对比&#xff1a; 二、BlockingQueue阻塞队列…

MySQL:子查询(全面详解)

MySQL&#xff1a;子查询 前言一、需求分析与问题解决1、实际问题2、子查询的基本使用3、子查询的分类 二、单行子查询1、单行比较操作符2、代码示例3、HAVING 中的子查询4、CASE中的子查询5、子查询中的空值问题6、非法使用子查询 三、多行子查询1、多行比较操作符2、代码示例…

Unity VR 开发教程 OpenXR+XR Interaction Toolkit(八)手指触控 Poke Interaction

文章目录 &#x1f4d5;教程说明&#x1f4d5;XR Poke Interactor&#x1f4d5;与 UI 进行触控交互⭐添加 Tracked Device Graphic Raycaster 和 XR UI Input Module 让 UI 可被交互 &#x1f4d5;与物体进行交互⭐XR Simple Interactable⭐XR Poke Filter 往期回顾&#xff1a…

偏爱console.log的你,肯定会觉得这个插件泰裤辣!

前言 毋庸置疑&#xff0c;要说前端调试代码用的最多的&#xff0c;肯定是console.log&#xff0c;虽然我现在 debugger 用的比较多&#xff0c;但对于生产环境、小程序真机调试&#xff0c;还是需要用到 log 来查看变量值&#xff0c;比如我下午遇到个场景&#xff1a;选择完…

单片机-串口通信

1.串口向电脑发送数据 1.配置串口 T1定时器&#xff0c;方式二8位重装 void UartInit(void) //4800bps11.0592MHz {PCON & 0x7F; //波特率不倍速SCON 0x50; //8位数据,可变波特率TMOD & 0x0F; //清除定时器1模式位TMOD | 0x20; //设定定时器1为8位自动重装方式…

Win11桌面右键菜单怎么变成老版本?

Win11桌面右键菜单怎么变成老版本&#xff1f;Win11系统带来了许多新的功能和改进&#xff0c;其中一个显著的变化是Win11桌面右键菜单的设计和布局&#xff0c;所以有些用户就想把Win11桌面右键菜单变成老版本&#xff0c;那具体怎么操作&#xff0c;可以按照以下小编分享的方…

chatgpt赋能python:用Python关机:简单、快捷、高效的关机方式

用Python关机&#xff1a;简单、快捷、高效的关机方式 介绍 在我们使用计算机的日常生活中&#xff0c;关机是必不可少的一个要素。传统的关机方式通常是通过点击“开始”菜单——“关机”按钮来完成的。但是&#xff0c;使用Python可以让我们更加方便和高效地完成关机操作。…

海外媒体发稿:海外新闻发表文章的必要性

海外新闻发表文章&#xff0c;是指国外企业在当地新闻媒体上发表企业信息和企业企业形象活动&#xff0c;通过专业媒体网络资源&#xff0c;依靠技术专业的报道发稿平台&#xff0c;让国外企业迅速、精确、低成本开展海外新闻稿子的出台&#xff0c;根据媒体报导&#xff0c;提…

PyQt5入门1——PyQt5的安装与Hello World应用

PyQt5入门1——PyQt5的安装与Hello World应用 学习前言简介&#xff08;From 百度百科&#xff09;安装Hello World1、窗口构建a、构建基础类b、编写UI 2、主程序运行 全部代码 学习前言 搞搞可视化界面哈&#xff0c;虽然不一定有用&#xff0c;但是搞一下。 简介&#xff…

Makefile实战

文章目录 1、准备环境1.2、规则1.3、假目标1.4、变量1.5、自动变量1.6、函数addprefix 函数filter函数filter-out函数patsubst函数strip函数wildcard函数 1、准备环境 准备一台Linux机器&#xff0c;验证环境中是否呗正确安装了&#xff0c;使用make -v命令进行验证 1.2、规则 …

React中useState的setState方法请求了好多次

1、问题描述 最近在写react的时候碰到了一个很奇怪的问题。 可以看到那个getXXX()的方法一直不断的被调用&#xff0c;网页一直请求&#xff0c;根本停不下来了。 2、产生原因 要弄明白这个原因&#xff0c;首先要先了解一下react生命周期。 react是组件式的编程&#xff0c;一…

青岛大学_王卓老师【数据结构与算法】Week04_01_循环链表_学习笔记

本文是个人学习笔记&#xff0c;素材来自青岛大学王卓老师的教学视频。 一方面用于学习记录与分享&#xff0c;另一方面是想让更多的人看到这么好的《数据结构与算法》的学习视频。 如有侵权&#xff0c;请留言作删文处理。 课程视频链接&#xff1a; 数据结构与算法基础–…

GO SDK使用说明

简介 欢迎使用腾讯云开发者工具套件&#xff08;SDK&#xff09;3.0&#xff0c;SDK 3.0 是云 API 3.0 平台的配套工具。SDK 3.0 实现了统一化&#xff0c;各个语言版本的 SDK 具备使用方法相同、接口调用方式相同、错误码和返回包格式相同等优点。本文以 GO SDK 3.0 为例&…

chatgpt赋能python:用Python编写BMI计算器,实现身体指数(BMI)的快速计算和分析

用Python编写BMI计算器&#xff0c;实现身体指数&#xff08;BMI&#xff09;的快速计算和分析 在当今的健康意识逐渐增强的时代&#xff0c;身体指数&#xff08;BMI&#xff09;成为了一个非常重要的健康指标。BMI是根据身高和体重计算出来的数值&#xff0c;用于评估一个人…

java pdf2html 和html2pdf 两款插件

1 pdf2html docker 启动&#xff0c;java 调用服务即可转化 https://beltxman.com/2567.html docker run -ti --rm -v ~/pdf:/pdf -w /pdf docker.io/pdf2htmlex/pdf2htmlex:0.18.8.rc2-master-20200820-ubuntu-20.04-x86_64 test.pdf 2 html2pdf java .Runtime 类型调用命…

超详细讲解在Ubuntu 20.04上安装ROS Noetic Ninjemys并测试

文章目录 前言安装STEP-1: 配置Ubuntu软件仓库STEP-2: 设置sources.listSTEP-3: 设置密钥STEP-4: 更新Ubuntu软件包确保安装索引是最新的STEP-5: 执行安装命令STEP-6: 配置ROS环境 测试ROS 前言 ROS是Robot Operating System的缩写, 即机器人操作系统, ROS是一组软件库和工具&…

LR录制https协议脚本前配置

LR录制https安全协议脚本前的设置 在IE中添加安全证书 打开IE浏览器&#xff0c;选择“工具—Internet选项—内容”&#xff0c;点击“证书”导入相应的证书&#xff0c;如下图 在LR中配置证书 获取pem格式证书 因为loadrunner只支持pem格式的证书&#xff0c;所以要将证书…