Activity销毁重建导致LiveData数据倒灌

6,695 阅读7分钟

问题前因

我们做的是一个类似ofo的App,面向海外市场,有些国家存在多种语言,例如加拿大。

用户骑行完毕后,在HomeActivity请求结束行程的接口,HomeActivity中注册结束行程的LiveData监听,如果返回的结果是成功,会跳到一个评价页面,用户评价完成,再返回主页。

某一天,客服反馈了一个奇怪的问题:一个用户说在主页,什么事情都没干,立马自动进入了评价页面,经常发生。

寻找原因

最开始遇到这个问题,简直一脸懵逼,第一个感觉:这应该不是我的bug吧🐶

查看代码,进入评分页面的逻辑在整个项目中只有一个地方存在,就是在HomeActivity的LiveData监听回调,说明用户每次打开App,执行了这个LiveData监听回调。

但是发送这个LiveData的事件是一个手动点击按钮的事件,每次打开App,用户不可能点击结束行程按钮。

想了很久,突然一个词蹦到我的脑海中:数据倒灌。

那什么情况下会发生LiveData数据倒灌?

根据LiveData的设计原则:

在页面重建时,LiveData自动推送最后一次数据,而不必重新去向后台请求。

LiveData自动推送最后一次数据条件是页面重建,也就是Activity生命周期经过了销毁到重建,那什么情况下会发生Activity重建? 常见的操作是:

  • 屏幕旋转
  • 用户手动切换系统语言
  • 系统内存不足,应用在后台被系统杀掉,然后用户再进入应用

系统杀掉应用后台虽然也会导致Activity重建,但是跟屏幕旋转和切换语言还不一样,最开始我以为是一样的,就没做实验了,尴尬😓

杀掉后台其实不到导致这个问题,这里放到最后说。

示例代码

我们先模拟屏幕旋转和切换系统语言,示例代码如下:

//MainActivity.kt
private fun initObserver() {
    mViewModel.testLiveData.observe(this){
        Log.i("wutao--> ", "testLiveData value == $it: ")
        Thread{
            SystemClock.sleep(3000)
            startActivity<SecondActivity>()
        }.start()
    }
    Log.i("wutao--> ", "mViewModel: $mViewModel    ------  viewModelStore: $viewModelStore")
}

private fun onClick(){
    mBinding.btnTest.setOnClickListener { mViewModel.testLiveData.value = 3 }
}

//MainViewModel.kt
val testLiveData = MutableLiveData<Int>()

在MainActivity中点击按钮,把ViewModel中的testLiveData的值设置为3,然后在MainActivity中监听,延迟3S后跳转到下一个页面。

3S后跳转到下一个页面,然后再返回,在当前页面旋转屏幕,发现又跳转到了下一个页面。

问题所在:我在这个页面什么都没做,手机旋转了屏幕就自动跳转到了下一个页面。。。

数据倒灌.gif

Log打印结果如下:

image.png

从打印结果来看:屏幕旋转收到了LiveData监听事件,并且跳转到了第二个Activity。此外,ViewModel中的地址值在屏幕旋转前后是一致的。

数据倒灌原因

先看下官方的文档:

ViewModel 将数据保留在内存中,这意味着开销要低于从磁盘或网络检索数据。ViewModel 与一个 Activity(或其他某个生命周期所有者)相关联,在配置更改期间保留在内存中,系统会自动将 ViewModel 与发生配置更改后产生的新 Activity 实例相关联。

developer.android.com/topic/libra…

从上面实验的打印结果也证实了这一点。

LiveData是观察者订阅者模式,屏幕旋转后LiveData收到了监听,那肯定进行了事件分发,那我们从LiveData源码中寻找分发的代码。

LiveData的源码不是很难,postValue()方法最终调用的也是setValue()方法。

// LiveData.java    
@MainThread
protected void setValue(T value) {
    // 只能在主线程中使用
    assertMainThread("setValue");
    // 这个值很重要
    mVersion++;
    // 需要分发的值
    mData = value;
    // 正式分发事件
    dispatchingValue(null);
}
// LiveData.java    
void dispatchingValue(@Nullable ObserverWrapper initiator) {
    // 如果当前正在分发中,return
    if (mDispatchingValue) {
        mDispatchInvalidated = true;
        return;
    }
    mDispatchingValue = true;
    do {
        mDispatchInvalidated = false;
        // 调用setValue()方法时,initiator是null
        if (initiator != null) {
            considerNotify(initiator);
            initiator = null;
        } else {
            for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
                 mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                // 真正分发事件的方法
                considerNotify(iterator.next().getValue());
                if (mDispatchInvalidated) {
                    break;
                }
            }
        }
    } while (mDispatchInvalidated);
    mDispatchingValue = false;
}
// LiveData.java    
private void considerNotify(ObserverWrapper observer) {
    // 如果当前的宿主Activity不是Active状态,不分发
    if (!observer.mActive) {
        return;
    }
    // 宿主Activity的如果在后台,不分发
    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    // 当前宿主Activity的mLastVersion大于等于LiveData的mVersion,不分发
    // 这里就是数据倒灌的原因
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    // 分发事件
    observer.mObserver.onChanged((T) mData);
}

相对于头脑风暴,不如打断点debug。

继续做实验:把上面Activity中的跳转逻辑注释,连续点击3次按钮,相当于调用了三次 mViewModel.testLiveData.setValue() 方法,然后再旋转屏幕。

image.png

果然LiveData执行了事件分发,从上图和注释可知:数据倒灌发生问题的地方在这个判断: if (observer.mLastVersion >= mVersion)

因为这个判断没有生效,导致屏幕旋转后事件进行了分发。

看下mLastVersion和mVersion是什么?

mVersion

// LiveData.java   
static final int START_VERSION = -1;
private int mVersion;

mVersion 是LiveData的成员变量,一个LiveData维护一份实例对象。

public LiveData() {
    mData = NOT_SET;
    mVersion = START_VERSION;
}

初始化LiveData()时,mVersion 设置为 -1;

protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
}

调用setValue()方法时,mVersion++,上面实验,连续点击按钮3次,mVersion++三次后值变成2。

mVersion看起来没任何问题,那问题就是出在mLastVersion上了。

mLastVersion

mLastVersion总共就三处调用的地方,默认值也是-1。如果分发事件成功,就将当前LiveData的mVersion赋值给mLastVersion

private abstract class ObserverWrapper {
    final Observer<? super T> mObserver;
    boolean mActive;
    // 第一处
    int mLastVersion = START_VERSION;
}
    private void considerNotify(ObserverWrapper observer) {
        ...
        // 第二处
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        // 第三处
        observer.mLastVersion = mVersion;
        observer.mObserver.onChanged((T) mData);
    }

从上面实验结果可知,屏幕旋转前,observer.mLastVersion == mVersion ==2。但是屏幕旋转后,mLastVersion的值却变成了-1。这里就是问题所在了。

我们在测试的Activity中,调用跟LiveData有关的就是observer()方法了:

@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
    ···
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    ···
    owner.getLifecycle().addObserver(wrapper);
}
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver

当Activity重建后,LiveData调用observe()方法,方法内会new一个新的LifecycleBoundObserver对象,LifecycleBoundObserver又是继承的ObserverWrapper类。

ObserverWrapper类初始化时会重新初始化int mLastVersion = START_VERSION;mLastVersion赋值为-1,也就是上面debug图片的结果。

因为observer.mLastVersion < mVersion,也就是-1 < 2,所以这个if判断失效,重新分发事件,导致数据倒灌。

但是这里又有个问题,Activity重建我们又没有手动调用setValue()方法,怎么会触发事件分发considerNotify()这个方法的。继续找这个方法在哪调用的即可。

有2处,从下往上依次是:

  • considerNotify() ->dispatchingValue() -> setValue()

  • considerNotify() ->dispatchingValue() -> activeStateChanged() -> onStateChanged()

很明显不是第一处,那就是第二处:

class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    @NonNull
    final LifecycleOwner mOwner;

    LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
        super(observer);
        mOwner = owner;
    }

    @Override
    boolean shouldBeActive() {
        return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
    }

    // 在页面状态发生改变时调用
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
                               @NonNull Lifecycle.Event event) {
        Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
        // 如果当前Activity的状态是onDestory,移除
        if (currentState == DESTROYED) {
            removeObserver(mObserver);
            return;
        }
        Lifecycle.State prevState = null;
        // 上一次的state跟当前的不同时,执行事件分发
        while (prevState != currentState) {
            prevState = currentState;
            activeStateChanged(shouldBeActive());
            currentState = mOwner.getLifecycle().getCurrentState();
        }
    }
}

对于onStateChanged(),官方文档写的很详细:

如果生命周期变为非活跃状态,它会在再次变为活跃状态时接收最新的数据。例如,曾经在后台的 Activity 会在返回前台后立即接收最新的数据。

developer.android.com/topic/libra…

系统杀后台

先说下结论:App在后台,系统内存不足,将App杀掉,不会导致LiveData数据倒灌。

模拟杀后台行为:

adb shell am kill package-name

切换应用到后台,然后执行adb操作

直接上Log打印:

image.png

在看下如果是屏幕旋转,Activity生命周期:

image.png

可以看出,系统杀后台跟屏幕旋转最大的不同是:杀后台Activity不会走onDestory()onRetainCustomNonConfigurationInstance()方法。

onRetainCustomNonConfigurationInstance()方法干什么用的,请点击文章末尾链接。

系统杀后台为什么不会导致数据倒灌,请点击文章末尾链接。

小结

Activity异常销毁然后重建,ViewModel会保存销毁之前的数据,然后在Activity重建完成后进行数据恢复,所以LiveData成员变量中的mVersion会恢复到重建之前的值。

但是Activity重建后会调用LiveData的observe()方法,方法内部会重新new一个实例,会将mLastVersion恢复到初始值。

由于LiveData本身的特性,Activity的生命周期由非活跃变成活跃时,LiveData会触发事件分发,导致屏幕旋转或者切换系统语言后出现数据倒灌。

但是这里有一点要非常注意:系统内存不足,杀到应用后台,也会导致Activity重建,但是不会LiveData导致数据倒灌。

问题找到了,那如何防止数据倒灌呢?

解决办法

再来回顾下,数据倒灌的常见方式:

  • 屏幕旋转
  • 用户手动切换系统语言

方案:

疑问:屏幕旋转,Activity从销毁到重建,ViewModel为什么保存之前的数据,然后在Activity重建完成后进行恢复?

预知后事如何,请点击:

屏幕旋转导致Activity销毁重建,ViewModel是如何恢复数据的