Problem before because

What we’re doing is an ofo-like App for overseas markets, some countries have multiple languages, such as Canada.

After the user finishes riding, he/she requests the interface for ending the trip in HomeActivity and registers the LiveData listener for ending the trip in HomeActivity. If the result returned is successful, it will jump to a evaluation page. After the user finishes the evaluation, he/she will return to the home page.

One day, customer service responded to a strange problem: a user said that on the home page, nothing was done, and immediately automatically entered the review page.

Look for reason

At the beginning encountered this problem, simply a face meng, the first feeling: this should not be my bug 🐶

Looking at the code, there is only one place in the whole project where the logic to enter the scoring page exists, which is in the LiveData listening callback of HomeActivity, indicating that the user executes this LiveData listening callback every time he opens the App.

However, the event of sending this LiveData is an event of manually clicking the button. Every time the App is opened, the user cannot click the end of the trip button.

After thinking for a long time, a word suddenly came to my mind: data inversion.

So when does LiveData backflow happen?

According to LiveData design principles:

When the page is rebuilt, LiveData automatically pushes the last data without having to go back to the background to request it.

The last data condition that LiveData automatically pushes is when the page is rebuilt, that is, when the Activity lifecycle has passed from destruction to reconstruction. When does an Activity rebuild occur? Common operations are:

  • Screen rotation
  • The user manually switched the system language
  • The system ran out of memory, the application was killed in the background, and then the user re-entered the application

Although the system kills the application background, it will also cause the Activity to be rebuilt, but it is different from screen rotation and language switching. At first, I thought it was the same, so I did not do the experiment, embarrassing 😓

Killing the background doesn’t actually cause this problem, but I’ll leave that to the end.

The sample code

Let’s first simulate screen rotation and switching system languages with the following example code:

//MainActivity.kt
private fun initObserver(a) {
    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(a){
    mBinding.btnTest.setOnClickListener { mViewModel.testLiveData.value = 3}}//MainViewModel.kt
val testLiveData = MutableLiveData<Int> ()Copy the code

Click the button in MainActivity, set the value of testLiveData in ViewModel to 3, and then listen in MainActivity. After a delay of 3S, jump to the next page.

After 3S jump to the next page, and then back, in the current page rotation screen, found to jump to the next page.

The problem: I’m not doing anything on this page, the phone rotates the screen and automatically jumps to the next page…

The Log output is as follows:

From the print: the screen rotation received the LiveData listener event and jumped to the second Activity. In addition, the address values in the ViewModel are consistent before and after screen rotation.

Reason for data backflow

Take a look at the official document:

The ViewModel keeps the data in memory, which means the overhead is lower than retrieving it from disk or the network. The ViewModel is associated with an Activity (or some other lifecycle owner), remains in memory during configuration changes, and the system automatically associates the ViewModel with new Activity instances that arise as a result of configuration changes.

Developer.android.com/topic/libra…

The printout from the above experiment also confirms this.

LiveData is observer subscriber mode, the screen rotates and LiveData receives the listen, so there must have been event distribution, so let’s look for the distribution code in the LiveData source code.

The LiveData source code is not too difficult, and the postValue() method ends up calling the setValue() method as well.

// LiveData.java    
@MainThread
protected void setValue(T value) {
    // Only in the main thread
    assertMainThread("setValue");
    // This value is important
    mVersion++;
    // The value to be distributed
    mData = value;
    // Distribute the event officially
    dispatchingValue(null);
}
// LiveData.java    
void dispatchingValue(@Nullable ObserverWrapper initiator) {
    // If the current distribution is in progress, return
    if (mDispatchingValue) {
        mDispatchInvalidated = true;
        return;
    }
    mDispatchingValue = true;
    do {
        mDispatchInvalidated = false;
        // When the setValue() method is called, initiator is null
        if(initiator ! =null) {
            considerNotify(initiator);
            initiator = null;
        } else {
            for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
                 mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                // How to actually distribute the event
                considerNotify(iterator.next().getValue());
                if (mDispatchInvalidated) {
                    break; }}}}while (mDispatchInvalidated);
    mDispatchingValue = false;
}
// LiveData.java    
private void considerNotify(ObserverWrapper observer) {
    // Do not distribute if the current host Activity is not in the Active state
    if(! observer.mActive) {return;
    }
    // If the host Activity is in the background, do not distribute
    if(! observer.shouldBeActive()) { observer.activeStateChanged(false);
        return;
    }
    // the mLastVersion of the current host Activity is greater than or equal to the mVersion of the LiveData
    // This is the reason for the data backflow
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    // Distribute events
    observer.mObserver.onChanged((T) mData);
}
Copy the code

Instead of brainstorming, interrupt a debug.

Continue to do the experiment: the Activity of the jump logic above notes, click on the button three times in a row, equivalent to call three times mViewModel. TestLiveData. SetValue () method, and then rotate the screen.

If (observer.mlastversion >= mVersion) if (observer.mVersion >= mVersion)

Because this judgment did not take effect, the event was distributed after the screen was rotated.

Let’s see what mLastVersion and mVersion are.

mVersion

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

MVersion is a member variable of LiveData. Each LiveData maintains a copy of the instance object.

public LiveData(a) {
    mData = NOT_SET;
    mVersion = START_VERSION;
}
Copy the code

When initializing LiveData(), mVersion is set to -1;

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

When the setValue() method is called, mVersion++, the above experiment, click the button three times continuously, mVersion++ three times after the value becomes 2.

MVersion doesn’t seem to be a problem, but mLastVersion is.

mLastVersion

There are only three places where mLastVersion is called, and the default is -1. If the event distribution is successful, the mVersion of the current LiveData is assigned to mLastVersion.

private abstract class ObserverWrapper {
    final Observer<? super T> mObserver;
    boolean mActive;
    / / first place
    int mLastVersion = START_VERSION;
}
    private void considerNotify(ObserverWrapper observer) {.../ / in the second place
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        / / the third place
        observer.mLastVersion = mVersion;
        observer.mObserver.onChanged((T) mData);
    }
Copy the code

From the above experimental results, observer.mlastversion == mVersion ==2 before screen rotation. But when the screen is rotated, the value of mLastVersion becomes -1. Here’s the problem.

In the Activity we are testing, we call the observer() method related to LiveData:

@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {LifecycleBoundObserver Wrapper =newLifecycleBoundObserver(owner, observer); ... the owner. GetLifecycle (.) addObserver (wrapper); }class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver
Copy the code

When the Activity is rebuilt, LiveData calls the Observe () method, and a new LifecycleBoundObserver object is inserted into the method, which in turn is the inherited ObserverWrapper class.

Int mLastVersion = START_VERSION; int mLastVersion = START_VERSION; Assign mLastVersion to -1, which is the result of the debug image above.

Because observer.mLastVersion < mVersion, that is, -1 < 2, the if judgment is invalid and the event is redistributed, resulting in data backflow.

If we don’t call the setValue() method manually, how can we let the event dispatch notice ()? Just keep looking for where this method was called.

There are two places, from bottom to top:

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

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

It’s obviously not the first, it’s the second:

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(a) {
        return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
    }

    // called when the page state changes
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
                               @NonNull Lifecycle.Event event) {
        Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
        // If the current Activity state is onDestory, remove it
        if (currentState == DESTROYED) {
            removeObserver(mObserver);
            return;
        }
        Lifecycle.State prevState = null;
        // When the last state was different from the current one, execute event distribution
        while(prevState ! = currentState) { prevState = currentState; activeStateChanged(shouldBeActive()); currentState = mOwner.getLifecycle().getCurrentState(); }}}Copy the code

For onStateChanged(), the official documentation is quite detailed:

If the lifecycle becomes inactive, it receives the latest data when it becomes active again. For example, an Activity that was in the background will receive the latest data as soon as it returns to the foreground.

Developer.android.com/topic/libra…

System kill background

First say conclusion: App in the background, the system memory is insufficient, kill the App, will not lead to LiveData backfilling.

Simulate killing background behavior:

adb shell am kill package-name

Switch the application to the background, and then perform the ADB operation

Print directly on Log:

Take a look at the Activity lifecycle if the screen is rotated:

It can be seen that system to kill the background are the main differences with the screen rotation: kill background activities won’t go onDestory () and onRetainCustomNonConfigurationInstance () method.

What onRetainCustomNonConfigurationInstance () method is used for, please click on the link at the end of the article.

Why system killing background will not lead to data backflow, please click the link at the end of the article.

summary

When the Activity is destroyed and then rebuilt, the ViewModel saves the data before the destruction, and then recovers the data after the Activity is rebuilt, so the mVersion in the LiveData member variable is restored to the value before the rebuild.

However, when the Activity is rebuilt, it calls LiveData’s observe() method, which internally creates a new instance and restores mLastVersion to its original value.

Due to the characteristics of LiveData, when the life cycle of an Activity changes from inactive to active, LiveData triggers event distribution, causing data backflow after screen rotation or switching system languages.

However, there is one important point to be aware of: the system is out of memory, killing the application background, will also cause the Activity to rebuild, but LiveData will not cause data backflow.

The problem has been identified, so how do you prevent data backflow?

The solution

To review the common ways of data backflow:

  • Screen rotation
  • The user manually switched the system language

Solution:

  • If the application does not require landscape, set it to permanent portrait.

  • If the current Activity goes back to the foreground and does not need to receive the latest data, you can use the extended LiveData in the following three

    • Official extension SingleLiveEvent
    • Meituan Reflection modified mVersion
    • UnPeek-LiveData

Question: Screen rotation, Activity destruction to rebuild, why does the ViewModel save the previous data, and then restore the Activity after the rebuild is complete?

For a preview, please click:

How does the ViewModel restore data when the screen rotation causes the Activity to destroy and rebuild