How does the ViewModel retain data after configuration changes

The ViewModel class is designed to store and manage data related to the interface in a life-cycle manner, and the ViewModel class allows the data to survive configuration changes such as screen rotation.

So today we’re going to explore how data continues to be retained after configuration changes.

First let’s look at how to create a ViewModel instance:

class CustomFactory : ViewModelProvider.Factory { override fun <T : ViewModel? > create(modelClass: Class<T>): T {return when (modelClass) {
            MainViewModel::class.java -> {
                MainViewModel()
            }
            else -> throw IllegalArgumentException("Unknown class $modelClass")
        } as T
    }
}
Copy the code

Create ViewModel instance

/ / 1
val viewModelProvider = ViewModelProvider(this, CustomFactory())
/ / 2
val viewModel: MainViewModel= viewModelProvider.get(MainViewModel::class.java)
// Or use KTX to create
//val model : MainViewModel by viewModels { CustomFactory() }
Copy the code

ViewModelProvider source

public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull ViewModelProvider.Factory factory) {
	 / / 3
    this(owner.getViewModelStore(), factory);
}

public ViewModelProvider(@NonNull ViewModelStore store, @NonNull ViewModelProvider.Factory factory) {
	this.mFactory = factory;
	this.mViewModelStore = store;
}
Copy the code

ViewModelStoreOwner source

public interface ViewModelStoreOwner {
    @NonNull
    ViewModelStore getViewModelStore(a);
}
Copy the code

Take a look at what this code does:

  1. The first argument to the ViewModelProvider constructor is ViewModelStoreOwner. So why should we pass in an Activity instance? Because ComponentActivity implements the ViewModelStoreOwner interface;

  2. Get (@nonNULL Class

    modelClass) to get an instance of ViewModel;

  3. ViewModelStore is obtained via owner.getViewModelStore(), which is the getViewModelStore() method for ComponentActivity.

Then we look at the ViewModelProvider#get() method:

@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
    String canonicalName = modelClass.getCanonicalName();
    if (canonicalName == null) {
        throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
    } else {
        return this.get("androidx.lifecycle.ViewModelProvider.DefaultKey:"+ canonicalName, modelClass); }}@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
    ViewModel viewModel = this.mViewModelStore.get(key);
    if (modelClass.isInstance(viewModel)) {
        if (this.mFactory instanceof ViewModelProvider.OnRequeryFactory) {
            ((ViewModelProvider.OnRequeryFactory)this.mFactory).onRequery(viewModel);
        }

        return viewModel;
    } else {
        if(viewModel ! =null) {}if (this.mFactory instanceof ViewModelProvider.KeyedFactory) {
            viewModel = ((ViewModelProvider.KeyedFactory)this.mFactory).create(key, modelClass);
        } else {
            viewModel = this.mFactory.create(modelClass);
        }

        this.mViewModelStore.put(key, viewModel);
        returnviewModel; }}Copy the code

As you can see from the above code, ViewModelStore holds a HashMap. If there is a cached instance of ViewModel in ViewModelStore, return it directly, otherwise create a new instance and store it in ViewModelStore.

Next we’ll look at the ComponentActivity#getViewModelStore() method:

@NonNull
public ViewModelStore getViewModelStore(a) {
    if (this.getApplication() == null) {
        throw new IllegalStateException("Your activity is not yet attached to the Application instance. You can't request ViewModel before onCreate call.");
    } else {
        this.ensureViewModelStore();
        return this.mViewModelStore; }}void ensureViewModelStore(a) {
    if (this.mViewModelStore == null) {
        ComponentActivity.NonConfigurationInstances nc = (ComponentActivity.NonConfigurationInstances)this.getLastNonConfigurationInstance();
        if(nc ! =null) {
            this.mViewModelStore = nc.viewModelStore;
        }

        if (this.mViewModelStore == null) {
            this.mViewModelStore = newViewModelStore(); }}}Copy the code

As can be seen from the above code:

  1. First, from the Activity of getLastNonConfigurationInstance () to obtain a nc NonConfigurationInstances instance;

  2. If nc is not null, assign the viewModelStore of NC to the mViewModelStore field for ComponentActivity;

  3. If mViewModelStore is still null, a new mViewModelStore object is created;

Obviously the Activity getLastNonConfigurationInstance () is the core of the cache ViewModelStore.

@Nullable
public Object getLastNonConfigurationInstance(a) {
	returnmLastNonConfigurationInstances ! =null
        ? mLastNonConfigurationInstances.activity : null;
}
Copy the code

This method returns the Activity of onRetainNonConfigurationInstance (), but the Activity of this method returns null.

According to the source code notes, this method is called by the Android system when a configuration change destroys the Activity and the system creates a new instance of the Activity for the new configuration.

We can return any object in this method, including the Activity instance itself, can be in the new Activity instance call later getLastNonfigurationInstance () to retrieve it.

Continue to track the source code found in the Activity of retainNonConfigurationInstances () calls the onRetainNonConfigurationInstance () method.

NonConfigurationInstances retainNonConfigurationInstances(a) {
	/ / 1Object activity = onRetainNonConfigurationInstance(); HashMap<String, Object> children = onRetainNonConfigurationChildInstances(); FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig(); . ArrayMap<String, LoaderManager> loaders = mFragments.retainLoaderNonConfig();/ / 2
    if (activity == null && children == null && fragments == null && loaders == null
            && mVoiceInteractor == null) {
        return null;
    }
	/ / 3
    NonConfigurationInstances nci = newNonConfigurationInstances(); nci.activity = activity; nci.children = children; nci.fragments = fragments; nci.loaders = loaders; .return nci;
}
Copy the code

As can be seen from the above code:

  1. First calls onRetainNonConfigurationInstance () to obtain an object, the object can be anything we want to be in time to save configuration changes object;

  2. If all of these objects are null, no data is returned;

  3. NonConfigurationInstances creating a static inner class instances of the nci, then will be assigned to the nci the activity field;

So who implements the Activity of onRetainNonConfigurationInstance (), by tracking code detection is ComponentActivity rewrite the method:

public final Object onRetainNonConfigurationInstance(a) {
    Object custom = this.onRetainCustomNonConfigurationInstance();
    ViewModelStore viewModelStore = this.mViewModelStore;
    ComponentActivity.NonConfigurationInstances nci;
    if (viewModelStore == null) {
        nci = (ComponentActivity.NonConfigurationInstances) this.getLastNonConfigurationInstance();
        if(nci ! =null) { viewModelStore = nci.viewModelStore; }}if (viewModelStore == null && custom == null) {
        return null;
    } else {
        nci = new ComponentActivity.NonConfigurationInstances();
        nci.custom = custom;
        nci.viewModelStore = viewModelStore;
        returnnci; }}Copy the code

To sum up:

  1. If we want to save any object when the system configuration changes and restore it when the system creates a new instance of the Activity for the new configuration, we just need to rewrite the Activity’sonRetainNonConfigurationInstance()Method can.
  2. According to the first rule, when a configuration change destroys the Activity,onRetainNonConfigurationInstance()Save theViewModelStoreAnd theViewModelAnd through theViewModelStoreTo access, so whenActivityWhen you rebuild it, you get the previous oneViewModel.

How is the system preserved and restoredNonConfigurationInstancesthe

From the previous chapter we know that the Activity of onRetainNonConfigurationInstance () is a system call, the system in time to call?

According to the experience, ActivityThread responsible for scheduling and execution of the Activity, then we will go to the search ActivityThread retainNonConfigurationInstances Activity under () method.

Note: Android Framework source code generally has the following relationship link:

Perform perform Perform perform perform perform perform

ActivityThread’s performDestroyActivity() method

ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
                                            int configChanges, boolean getNonConfigInstance, String reason) {
    ActivityClientRecord r = mActivities.get(token);
    Class<? extends Activity> activityClass = null;
    if (localLOGV) Slog.v(TAG, "Performing finish of " + r);
    if(r ! =null) {
        activityClass = r.activity.getClass();
        r.activity.mConfigChangeFlags |= configChanges;
        if (finishing) {
            r.activity.mFinished = true;
        }
        performPauseActivityIfNeeded(r, "destroy");
        if(! r.stopped) { callActivityOnStop(r,false /* saveState */."destroy");
        }
        / / 1
        if (getNonConfigInstance) {
            try {
                r.lastNonConfigurationInstances
                        = r.activity.retainNonConfigurationInstances();
            } catch (Exception e) {
                if(! mInstrumentation.onException(r.activity, e)) { ... }}}try {
            r.activity.mCalled = false;
            mInstrumentation.callActivityOnDestroy(r.activity);
            if(! r.activity.mCalled) { ... }if(r.window ! =null) { r.window.closeAllPanels(); }}catch (SuperNotCalledException e) {
            throw e;
        } catch (Exception e) {
            ...
        }
        r.setState(ON_DESTROY);
    }
    schedulePurgeIdler();
    
    synchronized (mResourcesManager) {
        mActivities.remove(token);
    }
    StrictMode.decrementExpectedActivityCount(activityClass);
    return r;
}
Copy the code

When the Activity was destroyed rebuilt for the configuration changes, will call ActivityThread handleRelaunchActivityInner () method:

private void handleRelaunchActivityInner(ActivityClientRecord r, int configChanges,
                                         List<ResultInfo> pendingResults, List<ReferrerIntent> pendingIntents,
                                         PendingTransactionActions pendingActions, boolean startsNotResumed,
                                         Configuration overrideConfig, String reason) {.../ / 1
    handleDestroyActivity(r.token, false, configChanges, true, reason); ./ / 2
    handleLaunchActivity(r, pendingActions, customIntent);
}
Copy the code

As can be seen from the above code:

  1. Executed firsthandleDestroyActivityAnd will begetNonConfigInstanceSet totrue, so that you canActivitytheonRetainNonConfigurationInstance()The reserved data is saved toActivityClientRecord;
  2. Then performhandleLaunchActivity()To reconstructActivityAnd restore the data.

ActivityThread performLaunchActivity ()

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ActivityInfo aInfo = r.activityInfo;
    if (r.packageInfo == null) {
        r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                Context.CONTEXT_INCLUDE_CODE);
    }

    ComponentName component = r.intent.getComponent();
    if (component == null) {
        component = r.intent.resolveActivity(
                mInitialApplication.getPackageManager());
        r.intent.setComponent(component);
    }

    if(r.activityInfo.targetActivity ! =null) {
        component = new ComponentName(r.activityInfo.packageName,
                r.activityInfo.targetActivity);
    }

    ContextImpl appContext = createBaseContextForActivity(r);
    / / 1
    Activity activity = null;
    try {
        java.lang.ClassLoader cl = appContext.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if(r.state ! =null) { r.state.setClassLoader(cl); }}catch (Exception e) {
        if(! mInstrumentation.onException(activity, e)) { ... } } Application app = r.packageInfo.makeApplication(false, mInstrumentation); .if(activity ! =null) {... appContext.getResources().addLoaders( app.getResources().getLoaders().toArray(new ResourcesLoader[0]));
		/ / 2
        appContext.setOuterContext(activity);
        activity.attach(appContext, this, getInstrumentation(), r.token,
                r.ident, app, r.intent, r.activityInfo, title, r.parent,
                r.embeddedID, r.lastNonConfigurationInstances, config,
                r.referrer, r.voiceInteractor, window, r.configCallback,
                r.assistToken);

        if(customIntent ! =null) {
            activity.mIntent = customIntent;
        }
        r.lastNonConfigurationInstances = null; .int theme = r.activityInfo.getThemeResource();
        if(theme ! =0) {
            activity.setTheme(theme);
        }

        activity.mCalled = false;
        if (r.isPersistable()) {
            mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
        } else{ mInstrumentation.callActivityOnCreate(activity, r.state); }... r.activity = activity; . } r.setState(ON_CREATE);synchronized(mResourcesManager) { mActivities.put(r.token, r); }...return activity;
}

Copy the code

As can be seen from the above code:

  1. createActivityAn instance of the;
  2. attachData will be destroyed due to configuration changesActivityRetained toActivityClientRecordHolding thelastNonConfigurationInstancesAssign to the newActivity;
  3. And then you can call itActivitythegetLastNonConfigurationInstance()The retrievedonRetainNonConfigurationInstance()Method to preserve objects.

The attach() method of the Activity

final void attach(Context context, ActivityThread aThread,
                  Instrumentation instr, IBinder token, int ident,
                  Application application, Intent intent, ActivityInfo info,
                  CharSequence title, Activity parent, String id,
                  NonConfigurationInstances lastNonConfigurationInstances,
                  Configuration config, String referrer, IVoiceInteractor voiceInteractor,
                  Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);

    mWindow = new PhoneWindow(this, window, activityConfigCallback); . mLastNonConfigurationInstances = lastNonConfigurationInstances; . mWindow.setWindowManager( (WindowManager)context.getSystemService(Context.WINDOW_SERVICE), mToken, mComponent.flattenToString(), (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) ! =0);
    if(mParent ! =null) { mWindow.setContainer(mParent.getWindow()); } mWindowManager = mWindow.getWindowManager(); mCurrentConfig = config; . }@Nullable
public Object getLastNonConfigurationInstance(a) {
    returnmLastNonConfigurationInstances ! =null
            ? mLastNonConfigurationInstances.activity : null;
}
Copy the code

Pictured above is retained and restore NonConfigurationInstances Framework source code roughly the calling process.

Is ViewModel the same as onSaveInstanceState

public override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putSerializable("currentScreen", currentScreen)
    outState.putParcelableArrayList("backstack", backstack)
}
Copy the code

The onSaveInstanceState method is called before the Activity can be killed so that it can restore its state when it returns at some future time. For example, if Activity B is started before Activity A, and at some point Activity A is killed to reclaim resources, Activity A will have the opportunity to save the current state of its user interface through this method, so that when the user returns to Activity A, The state of the user interface can be restored by onCreate() or onRestoreInstanceState().

If this method is called, it will occur after onStop of the application with the android.os.build.version_codesp platform version. For applications targeting older platform versions, this method will appear before onStop, and there is no guarantee that it will appear before or after onPause.

override fun onCreate(savedInstanceState: Bundle?). {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.leak_canary_leak_activity)
    if (savedInstanceState == null) {... }else {
        currentScreen = savedInstanceState.getSerializable("currentScreen") as Screen
        backstack = savedInstanceState.getParcelableArrayList<Parcelable>(
            "backstack"
        ) as ArrayList<BackstackFrame>
    }
}
Copy the code

An Activity is usually destroyed in one of three ways:

  1. Permanently removed from the current interface: the user navigates to another interface or simply closes the Activity (the finish method is invoked by clicking the back button or performing an action). The corresponding Activity instance is permanently closed;

  2. The configuration of the Activity is changed: for example, actions such as rotating the screen make the Activity need to be rebuilt immediately;

  3. When an application is in the background, its process is killed by the system: this happens when the device is running out of memory and the system needs to release some memory. The Activity also needs to be rebuilt when the user returns to the application after the process is killed in the background.

In the latter two cases, we usually want to rebuild the Activity. The ViewModel will help us with the second case, because in this case the ViewModel is not destroyed; In the third case, the ViewModel is destroyed. So once the third case occurs, you need to save and restore the data in the ViewModel in the Activity’s onSaveInstanceState related callback.