LiveData源码分析5 -- LiveData数据倒灌?

3,125 阅读7分钟

「这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战

前言

不知何时起,网上出现了大量言论说LiveData设计的有问题,其中最主要的点就是LiveData数据倒灌问题,啥是数据倒灌以及该如何解决这个问题,我们本章来好好说一下。

关于LiveData的源码解析可以查看前面系列文章:

# LiveData源码分析1 -- 概述和简单使用

# LiveData源码分析2 -- 原理分析

# LiveData源码分析3 -- MediatorLiveData的使用与原理解析

# LiveData源码分析4 -- Transformations类解析

正文

为了更好地理解LiveData数据倒灌的原因由来,我们细细来说。

事件和状态

我们一般使用使用LiveData来实现数据驱动UI的思想,即Activity/Fragment监听VM中的LiveData,然后进行展示相应的内容,这时的LiveData数据就是状态,需要保持UI的正确性,在新的状态设置之前能保持不变。

但是呢,我们经常会使用LiveData来保存事件,什么是事件呢 比如显示一个toast,完成一次页面导航等,这时再用LiveData就会有问题,具体问题我们举个例子来说一下。

使用LiveData保存toast内容

我们还是简单看个例子,我想在界面弹出toast,这个toast的内容我保存在VM中的一个LiveData变量中:

val toastMsg = MutableLiveData<String>()

然后在网络加载失败时给它赋值:

fun getUpdateInfo() {
    launchOnUI {
        _updateData.value = NetworkRequestUIState(isLoading = true)
        val result = mineRepository.getUpdateInfo("LowCarbon")
        if (result is ResponseResult.Error) {
            _updateData.value =
                NetworkRequestUIState(isLoading = false, isError = result.errorMsg)
            //给toastMsg赋值
            toastMsg.value = "获取更新失败"
        } else if (result is ResponseResult.Success) {
            _updateData.value =
                NetworkRequestUIState(isLoading = false, isSuccess = result.data)
        }
    }
}

接着在View层进行observe,代码如下:

//toast信息
toastMsg.observe(this@MineActivity){
    if (it.isNotEmpty()){
        Toast.makeText(this@MineActivity, it, Toast.LENGTH_SHORT).show()
    }
}

这里当竖屏时,网络不好时,会弹出toast,然后进行横屏后,会发现又弹了一次toast,这说明 "获取更新失败" 这个事件又被消费了一次,这明显不符合逻辑,那为什么会又弹出一次呢。

为什么会消费2次

这个原因其实很简单,在我前面的文章里说过,也就是LiveData源码分析2的原理分析中有介绍,当配置发生变化时,Activity会重新走一遍生命周期函数,而这时会再次添加观察者,而就是这个观察者中的代码导致的问题,代码如下:

//带生命周期的观察者
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    
    //省略。。。

    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
            @NonNull Lifecycle.Event event) {
        //当Activity重建时,生命周期会重新走一遍
        Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
        if (currentState == DESTROYED) {
            removeObserver(mObserver);
            return;
        }
        Lifecycle.State prevState = null;
        while (prevState != currentState) {
            prevState = currentState;
            //当走到ON_RESUME时会触发页面活跃,会notify数据
            activeStateChanged(shouldBeActive());
            currentState = mOwner.getLifecycle().getCurrentState();
        }
    }

   //省略。。。
}

简单来说就是Activity重建时,当界面重新可交互时会通知所有观察者来刷新数据,这里对于界面展示来说完全没有问题,因为我横屏后是需要和竖屏保持一样的数据,但是对于弹toast由于之前保存toast的LiveData数据不为空,所以会再弹一次。

事件和状态的区别

看了上面一个简单的例子,我们大概能看出事件和状态的区别了,主要有以下几点区别:

  • 是否粘性:状态要求长久保存的,所以是粘性的,也就是新的订阅者出现时,要为其展示原有的最新状态。事件则要求只被消费一次,当被观察者处理后则进行丢弃,以避免多次消费。

  • 覆盖性:这个主要出现在LiveData的postValue函数,当短时间内发生多次状态更新,可以抛弃中间状态只保留最新状态即可,这个原理在之前文章也说过,至于为什么也很好理解,防止UI线程处理太慢,导致界面实时性不对。事件则要求不能覆盖旧事件,因为每个事件都有作用,不能丢弃。

  • 幂等性:状态是幂等的,也就是一样的UI状态不需要响应多次,比如我列表的数据没有变化,在LiveData中再设置一次旧值,LiveData还是会通知观察者,这个现象当使用StateFlow时则不会,StateFlow在发送数据给观察者前会做比较,只有不同时才进行发送。而对于事件来说,必须每个事件都要消费,即可以发送同一个事件,进行消费。

解决问题

其实这里并不是说LiveData设计的有问题,而是在某些时候我们对LiveData进行了过度使用,LiveData对于状态的控制和提醒是完美的,没有任何问题,但是对于事件时就有了一些小缺陷, 我们可以想一下如何修改。

修改Activity的配置

既然ViewModel是解决Activity被销毁重建时保留数据,那我就当配置发生变化时不走生命周期,这个其实是可行的。

这里注意界面销毁重建一般会发生在那些情况,一个是系统配置发生变化即横竖屏切换、语言切换等,一个是界面退到后台,由于内存不足被杀死,而ViewModel能保存数据只在第一种情况,而第一种情况我们遇到最多的情况就是横竖屏切换,所以干脆一刀切在横竖屏切换时不走生命周期即可。

在Manifest中对Activity做以下操作:

<activity
    android:name=".main.gasesEmissionList.GasesEmissionDesActivity"
    android:configChanges="orientation|keyboardHidden|screenSize"/>

这样便可以在横竖屏切换时不走生命周期了,会走特定的函数,也就不会出现上述问题。

使用UnPeekLiveData

这里我们主要解决的是粘性问题,既然事件不需要进行二次消费,所以想办法解决这个即可,网上其实有不少做法,基本都是修改LiveData,这里推荐使用一个UnPeekLiveData,GitHub地址是:

github.com/KunMinX/UnP…

我们直接来看一下这个UnPeekLiveData到底是如何实现的,直接看一下源码:

//这个类相当于MutableLiveData,有设置的方法
public class UnPeekLiveData<T> extends ProtectedUnPeekLiveData<T> {

  //这里的setValue方法调用的是父类ProtectUnPeekLiveData的方法
  @Override
  public void setValue(T value) {
    super.setValue(value);
  }

  //调用MutableLiveData的postValue方法
  @Override
  public void postValue(T value) {
    super.postValue(value);
  }

  //省略
}

然后我们来看一下关键的ProtectUnPeekLiveData是如何实现的:

//类似LiveData
public class ProtectedUnPeekLiveData<T> extends LiveData<T> {
  //初始版本号
  private final static int START_VERSION = -1;
  //线程安全的变量,保存当前版本
  private final AtomicInteger mCurrentVersion = new AtomicInteger(START_VERSION);
  //是否允许空值
  protected boolean isAllowNullValue;

  //当把livedata当做事件来看,非粘性,且具有生命周期感知能力
  @Override
  public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
    super.observe(owner, createObserverWrapper(observer, mCurrentVersion.get()));
  }

  //把livedata当做事件来看,非粘性,不具有生命周期感知能力
  @Override
  public void observeForever(@NonNull Observer<? super T> observer) {
    super.observeForever(createObserverWrapper(observer, mCurrentVersion.get()));
  }

  //把liveData当做状态来看,粘性,具有生命周期感知能力
  public void observeSticky(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {
    super.observe(owner, createObserverWrapper(observer, START_VERSION));
  }

  //把liveData当做状态来看,粘性,不具有生命周期感知能力
  public void observeStickyForever(@NonNull Observer<? super T> observer) {
    super.observeForever(createObserverWrapper(observer, START_VERSION));
  }

  //设置值
  @Override
  protected void setValue(T value) {
    //LiveData当前版本加1
    mCurrentVersion.getAndIncrement();
    super.setValue(value);
  }

  //前面添加观察者都必须进行的封装
  class ObserverWrapper implements Observer<T> {
    //观察者
    private final Observer<? super T> mObserver;
    //观察者自己的版本号
    private int mVersion = START_VERSION;

    //当新观察者添加时,这个版本号是-1
    public ObserverWrapper(@NonNull Observer<? super T> observer, int version) {
      this.mObserver = observer;
      this.mVersion = version;
    }

    @Override
    public void onChanged(T t) {
      //见分析1
      if (mCurrentVersion.get() > mVersion && (t != null || isAllowNullValue)) {
        mObserver.onChanged(t);
      }
    }

//省略。。

}

分析1:当Activity重建时,因为LiveData特性会重新notify所有观察者,所以onChange方法会被调用,但是这里加了一个判断,当LiveData持有的版本号大于观察者持有的版本号,才进行分发数据。

这里因为当界面销毁重建时,LiveData保存的版本号是不会改变的,而销毁重建重新加入的观察者,其版本号为LiveData保存的版本号,这时2者相同,所以不会进行分发。

当调用setValue时,LiveData的版本号加1,这时就大于观察者持有的版本号了,就可以正常分发了。

其他方法

除了上面的UnPeekLiveData还有别的方法,基本都是修改LiveData来实现的,比如SingleLiveEvent、美团通过反色修改源LiveData等等,他们不外乎就是让数据可以不在具有粘性,从而解决问题。

总结

从这篇文章我们可以看出,LiveData的数据粘性或者叫做数据倒灌其实并不是啥大问题,只是对它的过度使用,至于解决方案也非常多,所以大家看到新技术,还是要多看源码,多了解,不能一股脑的使用。