关于LiveData粘性事件所带来问题的解决方案 参考文章 KunMinX :重学安卓:LiveData 数据倒灌 背景缘由全貌 独家解析
Android开发者:[译] 在 SnackBar,Navigation 和其他事件中使用 LiveData(SingleLiveEvent 案例)
美团技术团队:Android消息总线的演进之路:用LiveDataBus替代RxBus、EventBus
我们之前研究LiveData时候有讨论到LiveData天生就是支持“粘性”事件传递的,但和EventBus不同,LiveData并没有开关让我们将其配置为”非粘性”状态,这也就造成在我们不需要处理这类事件时,会变得束手无策。这篇文章主要针对这个问题进行探讨。
方法一:反射干涉Version 通过上一篇文章的源码解析,我们可以清晰的了解到,LiveData判断这个事件是否分发出去的关键在considerNotify
方法中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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); }
每次setValue
或postValue
时,mVersion
会+1,只要mLastVersion>=mVersion
即证明之前有过setValue
或postValue
。现在我们想使在observer
调用前的setValue
方法不被分发出去,只需要在调用observer
之前的某个节点处改,变使其mLastVersion = mVersion
即可。
通过源码我们发现可以通过反射在observer中找到mObservers
对象和当前mVersion
,然后便可以在这里将mVersion
赋值给mLastVersion
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private void hook (@NonNull Observer<T> observer) throws Exception { Class<LiveData> classLiveData = LiveData.class; Field fieldObservers = classLiveData.getDeclaredField("mObservers" ); fieldObservers.setAccessible(true ); Object objectObservers = fieldObservers.get(this ); Class<?> classObservers = objectObservers.getClass(); Method methodGet = classObservers.getDeclaredMethod("get" , Object.class); methodGet.setAccessible(true ); Object objectWrapperEntry = methodGet.invoke(objectObservers, observer); Object objectWrapper = null ; if (objectWrapperEntry instanceof Map.Entry) { objectWrapper = ((Map.Entry) objectWrapperEntry).getValue(); } if (objectWrapper == null ) { throw new NullPointerException("Wrapper can not be bull!" ); } Class<?> classObserverWrapper = objectWrapper.getClass().getSuperclass(); Field fieldLastVersion = classObserverWrapper.getDeclaredField("mLastVersion" ); fieldLastVersion.setAccessible(true ); Field fieldVersion = classLiveData.getDeclaredField("mVersion" ); fieldVersion.setAccessible(true ); Object objectVersion = fieldVersion.get(this ); fieldLastVersion.set(objectWrapper, objectVersion); } }
然后重写继承重写LiveData
,将这个hook方法放在observe
方法中。
这样一来,使用该自定义的LiveData
时就会发现,先setValue
,后observe
的做法已经行不通了,这就是所谓的非粘性 。
方法二:使用 SingleLiveEvent SingleLiveEvent,顾名思义,是一个只会发送一次更新的 LiveData。其代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class SingleLiveEvent <T > extends MutableLiveData <T > { private static final String TAG = "SingleLiveEvent" ; private final AtomicBoolean mPending = new AtomicBoolean(false ); @MainThread public void observe (LifecycleOwner owner, final Observer<T> observer) { if (hasActiveObservers()) { Log.w(TAG, "Multiple observers registered but only one will be notified of changes." ); } super .observe(owner, new Observer<T>() { @Override public void onChanged (@Nullable T t) { if (mPending.compareAndSet(true , false )) { observer.onChanged(t); } } }); } @MainThread public void setValue (@Nullable T t) { mPending.set(true ); super .setValue(t); } @MainThread public void call () { setValue(null ); } }
compareAndSet:比较并设置。
m.compareAndSet(a,b)
,如果m==a ,返回true,同时将m置为b; 如果m==b,返回false。
其实这个方法解决的并不是粘性事件的问题,而是“数据倒灌”的问题。“数据倒灌”一词出自KunMinX 的Blog重学安卓:LiveData 数据倒灌 背景缘由全貌 独家解析 ,即在setValue后,observe对此次set的value值会进行多次消费。比如进行第二次observe的时候获取到的数据是第一次的旧数据。这样会带来不可预期的后果。
1 2 3 4 5 6 7 8 9 10 11 val msg = MutableLiveData<Event<String>>()msg.value = Event("1" ) button3.setOnClickListener { msg.observe(this ,MyObs()) } class MyObs :Observer <Event<String >> { override fun onChanged (t: Event <String >) { t.getContentIfNotHandled()?.let { Log.e(">>>" , it) } } }
多次点击button3,会多次回调onChanged。实际上,只有第一次数据是我们想要的。SingleLiveEvent的思路是,在每次onChanged触发时,会通过一个布尔值mPending来判断上一次的setValue事件有没有被消费,如果被消费过了,则不再将消费传递下去。
实际上,SingleLiveEvent并没有解决‘粘性’的问题。
它所适用的场景如代码中所示,你一次setValue后,多次observe,却只想消费一个observe。但是 ,SingleLiveEvent
的问题在于它仅限于一个观察者。如果您无意中添加了多个,则只会调用一个,并且不能保证哪一个。
方法三:使用事件包装器 其实思路和第三种差不多,不过把其逻辑封装到了外面一层,这就解决了上文中只能添加一个观察者的问题,并且可以在外层增加一些自己独有的业务逻辑,使用起来更加优雅。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 open class Event<out T>(private val content: T) { var hasBeenHandled = false private set fun getContentIfNotHandled () : T? { return if (hasBeenHandled) { null } else { hasBeenHandled = true content } } fun peekContent () : T = content } val l = MutableLiveData<Event<String>>() l.observe(this , Observer { it.getContentIfNotHandled()?.let { ... } })
所以其解决的问题也还是“数据倒灌 ”的问题,并非“粘性事件”。
方法四:UnPeekLiveData 这个是KunMinX 大神所开源 的一个解决此类问题的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class ProtectedUnPeekLiveData <T > extends LiveData <T > { protected boolean isAllowNullValue; private final HashMap<Integer, Boolean> observers = new HashMap<>(); public void observeInActivity (@NonNull AppCompatActivity activity, @NonNull Observer<? super T> observer) { LifecycleOwner owner = activity; Integer storeId = System.identityHashCode(observer); observe(storeId, owner, observer); } private void observe (@NonNull Integer storeId, @NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) { if (observers.get(storeId) == null ) { observers.put(storeId, true ); } super .observe(owner, t -> { if (!observers.get(storeId)) { observers.put(storeId, true ); if (t != null || isAllowNullValue) { observer.onChanged(t); } } }); } @Override protected void setValue (T value) { if (value != null || isAllowNullValue) { for (Map.Entry<Integer, Boolean> entry : observers.entrySet()) { entry.setValue(false ); } super .setValue(value); } } protected void clear () { super .setValue(null ); } }
其思路也很清晰,为每个传入的observer对象携带一个布尔类型的值,作为其是否能进入observe方法的开关。每当有一个新的observer存进来的时候,开关默认关闭。
每次setValue后,打开所有Observer的开关,允许所有observe执行。
同时方法进去后,关闭当前执行的observer开关,即不能对其第二次执行了,除非你重新setValue。
通过这种机制,使得 不用反射技术实现LiveData的非粘性态 成为了可能。
粘性与数据倒灌 最后,要说明下文章中出线的粘性 和数据倒灌 两个词。
粘性:具体代码中指的是,先setValue/postValue,后调用observe(),如果成功收到了回调,即为粘性事件。
数据倒灌:“数据倒灌”一词最先由大佬KunMinX 提出,虽然给出了示例,但并没有给出文字定义。我的理解 是,先setValue/postValue,后调用observe(new Obs()),至此收到了回调。然后再第二次调用observe(new anotherObs()),如果还能收到第一次的回调,则为“数据倒灌”。
所以只要将LiveData变为“非粘性”的,就一定不会出现数据倒灌的问题了。再看以上四种方法所解决的问题。
反射干涉Version
SingleLiveEvent
事件包装器
UnPeekLiveData
将“粘性”变为“非粘性”
✔
❌
❌
✔
解决“数据倒灌”
✔
✔
✔
✔