问题前因
我们做的是一个类似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后跳转到下一个页面,然后再返回,在当前页面旋转屏幕,发现又跳转到了下一个页面。
问题所在:我在这个页面什么都没做,手机旋转了屏幕就自动跳转到了下一个页面。。。
Log打印结果如下:
从打印结果来看:屏幕旋转收到了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()
方法,然后再旋转屏幕。
果然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打印:
在看下如果是屏幕旋转,Activity生命周期:
可以看出,系统杀后台跟屏幕旋转最大的不同是:杀后台Activity不会走onDestory()
和onRetainCustomNonConfigurationInstance()
方法。
onRetainCustomNonConfigurationInstance()
方法干什么用的,请点击文章末尾链接。
系统杀后台为什么不会导致数据倒灌,请点击文章末尾链接。
小结
Activity异常销毁然后重建,ViewModel会保存销毁之前的数据,然后在Activity重建完成后进行数据恢复,所以LiveData成员变量中的mVersion
会恢复到重建之前的值。
但是Activity重建后会调用LiveData的observe()
方法,方法内部会重新new一个实例,会将mLastVersion
恢复到初始值。
由于LiveData本身的特性,Activity的生命周期由非活跃变成活跃时,LiveData会触发事件分发,导致屏幕旋转或者切换系统语言后出现数据倒灌。
但是这里有一点要非常注意:系统内存不足,杀到应用后台,也会导致Activity重建,但是不会LiveData导致数据倒灌。
问题找到了,那如何防止数据倒灌呢?
解决办法
再来回顾下,数据倒灌的常见方式:
- 屏幕旋转
- 用户手动切换系统语言
方案:
如果应用不需要横屏,就设置为永久竖屏。
-
如果当前Activity回到前台LiveData不需要接收最新的数据,可以使用下面三中扩展的LiveData
疑问:屏幕旋转,Activity从销毁到重建,ViewModel为什么保存之前的数据,然后在Activity重建完成后进行恢复?
预知后事如何,请点击: