如何防范Livedata的Sticky事件副作用

170 阅读3分钟

从LiveData源码角度分析,数据倒灌问题源于其​​版本控制机制​​:LiveData的mVersion(初始值-1)在每次setValue/postValue后自增,而新观察者的ObserverWrapper.mLastVersion初始值也为-1。当新观察者注册时,若mLastVersion < mVersion,LiveData会将最后一次历史数据分发给该观察者(详见considerNotify()方法逻辑)。以下是针对此问题的解决方案及​​Java实现​​:


⚙️ ​​一、源码级解决方案与Java实现​

✅ ​​1. SingleLiveEvent(轻量级单次事件)​

​原理​​:通过原子变量标记事件消费状态,确保数据仅分发一次。
​Java实现​​:

public class SingleLiveEvent<T> extends MutableLiveData<T> {
    private final AtomicBoolean mPending = new AtomicBoolean(false);

    @Override
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        super.observe(owner, t -> {
            // 原子操作:若标记为true则消费并重置
            if (mPending.compareAndSet(true, false)) {
                observer.onChanged(t);
            }
        });
    }

    @Override
    public void setValue(T value) {
        mPending.set(true); // 标记待消费
        super.setValue(value);
    }
}

​适用场景​​:Toast、页面跳转等​​一次性事件​​。
​局限​​:多观察者时仅第一个能消费事件。


✅ ​​2. SafeLiveData(同包名访问版本号)​

​原理​​:利用LiveData的包级方法getVersion()直接获取版本号,跳过初始分发。
​Java实现​​(需与LiveData同包名androidx.lifecycle):

package androidx.lifecycle;

public class SafeLiveData<T> extends MutableLiveData<T> {
    private static final int START_VERSION = -1;

    @Override
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        // 检查是否存在历史版本(mVersion > -1)
        boolean hasHistory = getVersion() > START_VERSION;
        super.observe(owner, new SafeObserver<>(observer, hasHistory));
    }

    static class SafeObserver<T> implements Observer<T> {
        private final Observer<? super T> mRealObserver;
        private boolean mSkipFirst;

        SafeObserver(Observer<? super T> observer, boolean skipFirst) {
            mRealObserver = observer;
            mSkipFirst = skipFirst;
        }

        @Override
        public void onChanged(T value) {
            if (mSkipFirst) {
                mSkipFirst = false;
                return; // 跳过历史数据分发
            }
            mRealObserver.onChanged(value);
        }
    }
}

​优势​​:

  • ​零反射​​:直接访问getVersion()(包级权限),性能最优 。

  • ​支持多观察者​​:每个观察者独立处理历史数据。
    ​注意​​:依赖AndroidX包名稳定性,但实际风险极低。


✅ ​​3. UnPeekLiveData(多观察者独立控制)​

​原理​​:为每个观察者维护独立消费状态(通过HashMap)。
​Java实现​​:

public class UnPeekLiveData<T> extends MutableLiveData<T> {
    private final Map<Integer, Boolean> mObserverMap = new HashMap<>();

    @Override
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        int id = System.identityHashCode(observer);
        mObserverMap.put(id, false); // 初始化未消费
        super.observe(owner, t -> {
            if (Boolean.TRUE.equals(mObserverMap.get(id))) return;
            mObserverMap.put(id, true); // 标记已消费
            observer.onChanged(t);
        });
    }

    @Override
    public void setValue(T value) {
        // 重置所有观察者状态为未消费
        for (Integer key : mObserverMap.keySet()) {
            mObserverMap.put(key, false);
        }
        super.setValue(value);
    }
}

​适用场景​​:高频更新且需支持多观察者的业务(如实时数据仪表盘)。
​代价​​:内存开销(存储所有观察者状态)。


⚠️ ​​4. 反射干预版本号(不推荐)​

​原理​​:反射修改观察者的mLastVersion,使其与LiveData版本一致。
​关键代码​​:

public void hookObserver(LiveData<?> liveData, Observer<?> observer) throws Exception {
    Field mObserversField = LiveData.class.getDeclaredField("mObservers");
    mObserversField.setAccessible(true);
    Object mObservers = mObserversField.get(liveData);
    
    Method getMethod = mObservers.getClass().getDeclaredMethod("get", Object.class);
    getMethod.setAccessible(true);
    Object entry = getMethod.invoke(mObservers, observer);
    
    Object observerWrapper = ((Map.Entry) entry).getValue();
    Field mLastVersionField = observerWrapper.getClass().getDeclaredField("mLastVersion");
    mLastVersionField.setAccessible(true);
    Field mVersionField = LiveData.class.getDeclaredField("mVersion");
    mVersionField.setAccessible(true);
    mLastVersionField.set(observerWrapper, mVersionField.get(liveData));
}

​缺点​​:性能差(每次注册需反射)、兼容性风险(AndroidX内部类变更)。


🔍 ​​二、方案对比与选型建议​

​方案​适用场景多观察者支持性能侵入性实现复杂度
SingleLiveEvent一次性事件(如弹窗)⭐⭐⭐⭐
SafeLiveData常规业务(状态/事件均可)⭐⭐⭐⭐⭐⭐⭐
UnPeekLiveData多观察者高频更新(如实时数据)⭐⭐⭐⭐⭐⭐
反射方案临时测试⭐⭐⭐⭐

​推荐选择​​:

  1. ​通用场景​​:优先使用 ​SafeLiveData​ ,性能最佳且无反射风险 。

  2. ​高频多观察者​​:选择 ​UnPeekLiveData​ ,需评估内存开销。

  3. ​简单事件​​:SingleLiveEvent 足够,但避免用于多观察者。


💎 ​​三、源码设计启示​

LiveData的粘性设计(Sticky Behavior)本质是​​为状态持久化服务​​(如UI重建后恢复数据)。若需严格区分​​状态​​(State)与​​事件​​(Event):

  • ​状态​​:使用原生LiveData,依赖其自动恢复特性。
  • ​事件​​:通过上述方案隔离历史数据,确保事件仅触发一次。
    核心逻辑在于控制mVersionmLastVersion的同步时机,理解此机制即可灵活定制解决方案