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

问题前因

我们做的是一个类似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 实例相关联。

https://developer.android.com/topic/libraries/architecture/saving-states?hl=zh-cn

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

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 会在返回前台后立即接收最新的数据。

https://developer.android.com/topic/libraries/architecture/livedata?hl=zh-cn

系统杀后台

先说下结论: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是如何恢复数据的

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容