美文网首页Android使用场景Android
关于LiveData粘性事件所带来问题的解决方案

关于LiveData粘性事件所带来问题的解决方案

作者: 慕尼黑凌晨四点 | 来源:发表于2020-12-20 19:02 被阅读0次

    参考文章

    KunMinX:重学安卓:LiveData 数据倒灌 背景缘由全貌 独家解析

    Android开发者:[译] 在 SnackBar,Navigation 和其他事件中使用 LiveData(SingleLiveEvent 案例)

    美团技术团队:Android消息总线的演进之路:用LiveDataBus替代RxBus、EventBus

    我们之前研究LiveData时候有讨论到LiveData天生就是支持“粘性”事件传递的,但和EventBus不同,LiveData并没有开关让我们将其配置为"非粘性"状态,这也就造成在我们不需要处理这类事件时,会变得束手无策。这篇文章主要针对这个问题进行探讨。

    方法一:反射干涉Version

    通过上一篇文章的源码解析,我们可以清晰的了解到,LiveData判断这个事件是否分发出去的关键在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;
        //noinspection unchecked
        observer.mObserver.onChanged((T) mData);
    }
    

    每次setValuepostValue时,mVersion会+1,只要mLastVersion>=mVersion即证明之前有过setValuepostValue。现在我们想使在observer调用前的setValue方法不被分发出去,只需要在调用observer之前的某个节点处改,变使其mLastVersion = mVersion即可。

    通过源码我们发现可以通过反射在observer中找到mObservers对象和当前mVersion,然后便可以在这里将mVersion赋值给mLastVersion

    private void hook(@NonNull Observer<T> observer) throws Exception {
                //get wrapper's version
                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);
                //get livedata's version
                Field fieldVersion = classLiveData.getDeclaredField("mVersion");
                fieldVersion.setAccessible(true);
                Object objectVersion = fieldVersion.get(this);
                //set wrapper's version
                fieldLastVersion.set(objectWrapper, objectVersion);
            }
        }
    

    然后重写继承重写LiveData,将这个hook方法放在observe方法中。

    这样一来,使用该自定义的LiveData时就会发现,先setValue,后observe的做法已经行不通了,这就是所谓的非粘性

    方法二:使用 SingleLiveEvent

    SingleLiveEvent,顾名思义,是一个只会发送一次更新的 LiveData。其代码实现如下:

    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.");
            }
    
            // Observe the internal MutableLiveData
            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);
        }
    
        /**
         * Used for cases where T is Void, to make calls cleaner.
         */
        @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的时候获取到的数据是第一次的旧数据。这样会带来不可预期的后果。

    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 的问题在于它仅限于一个观察者。如果您无意中添加了多个,则只会调用一个,并且不能保证哪一个。

    方法三:使用事件包装器

    其实思路和第三种差不多,不过把其逻辑封装到了外面一层,这就解决了上文中只能添加一个观察者的问题,并且可以在外层增加一些自己独有的业务逻辑,使用起来更加优雅。

    /**
     * Used as a wrapper for data that is exposed via a LiveData that represents an event.
     */
    open class Event<out T>(private val content: T) {
    
        var hasBeenHandled = false
            private set // Allow external read but not write
    
        /**
         * Returns the content and prevents its use again.
         */
        fun getContentIfNotHandled(): T? {
            return if (hasBeenHandled) {
                null
            } else {
                hasBeenHandled = true
                content
            }
        }
    
        /**
         * Returns the content, even if it's already been handled.
         */
        fun peekContent(): T = content
    }
    
    //----------------------------使用时 --------------------------------
    val l = MutableLiveData<Event<String>>()
    l.observe(this, Observer {
        it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
            ...
        }
    })
    

    所以其解决的问题也还是“数据倒灌”的问题,并非“粘性事件"。

    方法四:UnPeekLiveData

    这个是KunMinX大神所开源的一个解决此类问题的方法。

    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);//源码这里是activity.getViewModelStore(),是为了保证同一个ViewModel环境下"唯一可信源"
            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
    将“粘性”变为“非粘性”
    解决“数据倒灌”

    相关文章

      网友评论

        本文标题:关于LiveData粘性事件所带来问题的解决方案

        本文链接:https://www.haomeiwen.com/subject/vbbknktx.html