LiveData数据倒灌问题

1,368 阅读4分钟

LiveData 是 Android 架构组件中的一个类,用于观察数据的变化并在界面发生变化时更新 UI。然而,在使用 LiveData 时可能会遇到数据倒灌(Data Inundation)的问题,即观察者在订阅时会立即收到一个数据更新,这有时可能不是预期行为。

1. 示例

所谓的“数据倒灌”:其实是类似粘性广播那样,当新的观察者开始注册观察时,会把上次发的最后一次的历史数据传递给当前注册的观察者。

val liveData = MutableLiveData<String>()
liveData.postValue("在添加观察者之前发送数据")
liveData.observe(lifecycleOwner, Observer<String> { value ->
    Log.d("TAG", "onChanged: $value")
})

在添加观察者之前发送了一条数据,当调用 liveData.observe 方法后,观察者会立即收到一条数据更新。

2. 发生原因

mVersionmLastVersion 这两个值的变化是数据倒灌的核心原因之一。

mVersion

mVersion 是 LiveData 内部维护的一个版本号,初始值为 -1,每次数据更新(setValue)时会递增。

public abstract class LiveData<T> {
    static final int START_VERSION = -1;
    private int mVersion;

    public LiveData() {
        mVersion = START_VERSION;
    }

    protected void setValue(T value) {
        mVersion++;
        dispatchingValue(null);
    }
}

mLastVersion

mLastVersion 是每个观察者持有的版本号,用于记录该观察者最后一次接收到的数据版本,只有 mLastVersion 的值小于 mVersion 才会通知观察者。

public abstract class LiveData<T> {
    static final int START_VERSION = -1;

    private abstract class ObserverWrapper {
        int mLastVersion = START_VERSION;
    }

    void dispatchingValue(@Nullable ObserverWrapper initiator) {
        // ...
        considerNotify(initiator);
    }

    private void considerNotify(ObserverWrapper observer) {
        // ...
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        observer.mLastVersion = mVersion;
        observer.mObserver.onChanged((T) mData);
    }
}

分析

  • 在创建LiveData 对象时,mVersion 初始化为 -1。
  • 在注册新的观察者之前发送了一条数据:LiveData 中的 mVersion 值会加 1,即 mVersion 值为 0。
  • 在注册新的观察者后:新的观察者中的 mLastVersion 为 初始值 -1。
  • 注册新的观察者后:如果当前页面处于活跃状态,会调用一次dispatchingValue方法,由于此时 mLastVersion 值小于 mVersion,LiveData 会立即将当前的数据发送给这个新的观察者。

3. 具体场景分析

常见的LiveData数据倒灌场景:

Fragment 切换:

在使用 ViewPager 或 FragmentTransaction 切换 Fragment 时,如果多个 Fragment 共享同一个 ViewModel,重新显示的 Fragment 可能会收到 LiveData 的数据倒灌。

配置变化(如屏幕旋转):

当设备发生配置变化时(如屏幕旋转),Activity 或 Fragment 会重新创建。此时重新订阅 LiveData 会导致数据倒灌,显示旧数据。

短生命周期组件

比如使用 LiveData 的对话框、弹出菜单等组件,在重新显示或创建时会重新订阅 LiveData,导致数据倒灌。

具体分析

这里,我们以Fragment 切换场景为例,分析 mVersionmLastVersion 的变化,从而更好的理解数据倒灌发生的原因。

1. 初始化和首次订阅

当第一个 Fragment 创建并订阅 LiveData 时,LiveData 的 mVersion 可能是初始值 -1,该 Fragment 中观察者 observermLastVersion 也为初始值 -1。

2. 数据更新

当 LiveData 的数据发生变化时,mVersion 会递增(例如变为 0),并通知所有活跃的观察者更新数据。这时,所有活跃观察者的 mLastVersion 也会更新为 0。

3. Fragment 切换

当第一个 Fragment 切换到第二个 Fragment,第二个 Fragment 开始订阅 LiveData。因为第二个 Fragment 是新创建的观察者,其 mLastVersion 初始值为 -1。由于此时 LiveData 的 mVersion 为 0,而新的观察者 mLastVersion 为 -1,导致新观察者会立即收到最新的数据更新(即数据倒灌)。

4. 解决方法

1. 单一事件模式(SingleLiveEvent)

创建一个只发送一次事件的 LiveData 类,确保事件只被消费一次。

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

    public SingleLiveData() {
    }

    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        super.observe(owner, t -> {
            if (mPending.compareAndSet(true, false)) {
                observer.onChanged(t);
            }
        });
    }

    @MainThread
    public void setValue(@Nullable T t) {
        mPending.set(true);
        super.setValue(t);
    }
}

2. 使用反射

通过反射获取 LiveData 的版本号,然后通过反射修改当前 Observer 的版本号。

public class UnPeekLiveData<T> extends LiveData<T> {
    private final HashMap<Observer<? super T>, 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<Observer<? super T>, Boolean> entry : observers.entrySet()) {
                entry.setValue(false);
            }
            super.setValue(value);
        }
    }

    protected void clear() {
        super.setValue(null);
    }
}

3. 同包名

通过创建一个与 LiveData 同包名的类,可以直接访问 getVersion 方法,而不需要通过反射获取版本号。

// 与 LiveData 同包名
package androidx.lifecycle;

public class SafeLiveData<T> extends MutableLiveData<T> {
    @Override
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        // 直接可以通过 this.version 获取到版本号
        PictorialObserver pictorialObserver = new PictorialObserver(observer, this.version > START_VERSION);
        super.observe(owner, pictorialObserver);
    }

    private static class PictorialObserver<T> implements Observer<T> {
        private final Observer<? super T> realObserver;
        private boolean preventDispatch;

        PictorialObserver(Observer<? super T> realObserver, boolean preventDispatch) {
            this.realObserver = realObserver;
            this.preventDispatch = preventDispatch;
        }

        @Override
        public void onChanged(T value) {
            if (preventDispatch) {
                preventDispatch = false;
                return;
            }
            realObserver.onChanged(value);
        }
    }
}

这种方法利用同包名访问权限获取版本号,不需要反射,改动小,性能好。但如果后续 AndroidX 库的访问权限或包名修改,则需要调整代码。