技术分享
AndroidLiveData

解决LiveData数据倒灌的新思路

逍十一郎逍十一郎
2023年8月9日
2

数据倒灌现象

对于LiveData“数据倒灌”的问题,我相信很多人已经都了解了,这里提一下。所谓的“数据倒灌”:其实是类似粘性广播那样,当新的观察者开始注册观察时,会把上次发的最后一次的历史数据传递给当前注册的观察者

比如在在下面的例子代码中:

1val testViewModel = ViewModelProvider(this)[TestViewModel::class.java] 2testViewModel.updateData("第一次发送数据") 3testViewModel.testLiveData.observe(this,object :Observer<String>{ 4 override fun onChanged(value: String) { 5 println("==============$value") 6 } 7})

updateData方法发送了一次数据,当下面调用LiveData的observe方法时,会立即打印==============第一次发送数据,这就是上面说的“数据倒灌”现象。

发生原因

原因其实也很简单,其实就是 LiveData内部有一个mVersion字段,记录版本,其初始的 mVersion 是-1,当我们调用了其 setValue 或者 postValue,其 mVersion+1;对于每一个观察者的封装 ObserverWrapper,其初始 mLastVersion 也为-1,也就是说,每一个新注册的观察者,其 mLastVersion 为-1;当 LiveData 设置这个 ObserverWrapper 的时候,如果 LiveDatamVersion 大于 ObserverWrappermLastVersionLiveData 就会强制把当前 value 推送给 Observer

也就是下面这段代码

1 private void considerNotify(ObserverWrapper observer) { 2 if (!observer.mActive) { 3 return; 4 } 5 6 if (!observer.shouldBeActive()) { 7 observer.activeStateChanged(false); 8 return; 9 } 10 // 判断observer的版本是否大于LiveData的版本mVersion 11 if (observer.mLastVersion >= mVersion) { 12 return; 13 } 14 observer.mLastVersion = mVersion; 15 observer.mObserver.onChanged((T) mData); 16 }

所以要解决这个问题,思路上有两种方式:

  • 通过改变每个ObserverWrapper的版本号的值
  • 通过某种方式,保证第一次分发不响应

解决方法

目前网络上可以看到有三种解决方式

每次只响应一次

1public class SingleLiveData<T> extends MutableLiveData<T> { 2 private final AtomicBoolean mPending = new AtomicBoolean(false); 3 4 public SingleLiveData() { 5 } 6 7 public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) { 8 super.observe(owner, (t) -> { 9 if (this.mPending.compareAndSet(true, false)) { 10 observer.onChanged(t); 11 } 12 13 }); 14 } 15 16 @MainThread 17 public void setValue(@Nullable T t) { 18 this.mPending.set(true); 19 super.setValue(t); 20 } 21 22 @MainThread 23 public void call() { 24 this.setValue((Object)null); 25 } 26}

这个方法能解决历史数据往回发的问题,但是对于多Observe监听就不行了,只能单个监听,如果是多个监听,只有一个能正常收到,其他的就无法正常工作

反射

这种方式就是每次注册观察者时,通过反射获取LiveData的版本号,然后又通过反射修改当前Observer的版本号值。这种方式的优点是:

  • 能够多 Observer 监听
  • 解决粘性问题

但是也有缺点:

  • 每次注册 observer 的时候,都需要反射更新版本,耗时有性能问题

UnPeekLiveData

1public class UnPeekLiveData extends LiveData { 2 3 protected boolean isAllowNullValue; 4 5 private final HashMap observers = new HashMap(); 6 7 public void observeInActivity(@NonNull AppCompatActivity activity, @NonNull Observer super T> observer) { 8 LifecycleOwner owner = activity; 9 Integer storeId = System.identityHashCode(observer); 10 observe(storeId, owner, observer); 11 } 12 13 private void observe(@NonNull Integer storeId, 14 @NonNull LifecycleOwner owner, 15 @NonNull Observer super T> observer) { 16 17 if (observers.get(storeId) == null) { 18 observers.put(storeId, true); 19 } 20 21 super.observe(owner, t -> { 22 if (!observers.get(storeId)) { 23 observers.put(storeId, true); 24 if (t != null || isAllowNullValue) { 25 observer.onChanged(t); 26 } 27 } 28 }); 29 } 30 31 @Override 32 protected void setValue(T value) { 33 if (value != null || isAllowNullValue) { 34 for (Map.Entry entry : observers.entrySet()) { 35 entry.setValue(false); 36 } 37 super.setValue(value); 38 } 39 } 40 41 protected void clear() { 42 super.setValue(null); 43 } 44}

这个其实就是上面 SingleLiveData 的升级版,SingleLiveData 是用一个变量控制所有的 Observer,而上面采用的每个 Observer 都采用一个控制标识进行控制。 每次 setValue 的时候,就打开所有 Observer 的开关,表示可以接受分发。分发后,关闭当前执行的 Observer 开关,即不能对其第二次执行了,除非你重新 setValue。 这种方式基本上是比价完美了,除了内部多一个用HashMap存放每个Observer的标识,如果Observer比较多的话,会有一定的内存消耗。

新的思路

我们先看下LiveData获取版本号方法:

1int getVersion() { 2 return mVersion; 3}

这个方法是一个包访问权限的方法,如果我新建一个和LiveData同包名的类,是不是就可以不需要反射就能获取这个值呢?其实这是可行的

1// 跟LiveData同包名 2package androidx.lifecycle 3 4open class SafeLiveData<T> : MutableLiveData<T>() { 5 6 override fun observe(owner: LifecycleOwner, observer: Observer<in T>) { 7 // 直接可以通过this.version获取到版本号 8 val pictorialObserver = PictorialObserver(observer, this.version > START_VERSION) 9 super.observe(owner, pictorialObserver) 10 } 11 12 class PictorialObserver<T>(private val realObserver: Observer<in T>, private var preventDispatch: Boolean = false) : 13 Observer<T> { 14 15 override fun onChanged(value: T) { 16 // 如果版本有差异,第一次不处理 17 if (preventDispatch) { 18 preventDispatch = false 19 return 20 } 21 realObserver.onChanged(value) 22 } 23 24 } 25}

这种取巧的方式的思路就是:

  • 利用同包名访问权限可以获取版本号,不需要通过反射获取
  • 判断LiveDataObserver是否有版本差异,如果有,第一次不响应,否则就响应

我个人是偏向这种方式,也应用到了实际的开发中。这种方式的优点是:改动小,不需要反射,也不需要用HashMap存储等,缺点是:有一定的侵入性,假如后面这个方法的访问权限修改或者包名变动,就无效了,但是我认为这种可能性是比较小,毕竟androidx库迭代了这么多版本,算是比较稳定了。

评论讨论

评论需要审核后才能显示