Android interview summary - how is ViewModel saved and restored?

Combined with the previous article Android interview summary - ViewModel We know:

When the configuration changes, the Activity#onRetainNonConfigurationInstance() will be called to save the ViewModel
The object mViewModelStore of the example is called and getViewModelStore() is called after the Activity is rebuilt.
ensureViewModelStore() calls getLastNonConfigurationInstance() internally
Method to obtain whether there is a cached ViewModelStore object. If there is, it returns. If not, a new ViewModelStore instance is created.

This article mainly looks at how to call Activity#onRetainNonConfigurationInstance() after configuration changes.

Know through breakpoint debugging that the ActivityThread#handleRelaunchActivity method will be called after configuration change

Why do you call ActivityThread#handleRelaunchActivity? I learned about reanalysis later (I just read it again, but I didn't understand it, and I'll talk about it again ~ Lala la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la la

    @Override
    public void handleRelaunchActivity(ActivityClientRecord tmp,
            PendingTransactionActions pendingActions) {
        ...
        handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
                pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");
        ...

handleRelaunchActivityInner

    private void handleRelaunchActivityInner(ActivityClientRecord r, int configChanges,
            List<ResultInfo> pendingResults, List<ReferrerIntent> pendingIntents,
            PendingTransactionActions pendingActions, boolean startsNotResumed,
            Configuration overrideConfig, String reason) {
        ...
        // Note that the third parameter is true
        handleDestroyActivity(r.token, false, configChanges, true, reason);
        ...
        handleLaunchActivity(r, pendingActions, customIntent);

handleDestroyActivity is called internally, and the third parameter getNonConfigInstance = true
The handleLaunchActivity method will be described later

    @Override
    public void handleDestroyActivity(IBinder token, boolean finishing, int configChanges,
            boolean getNonConfigInstance, String reason) {
        ActivityClientRecord r = performDestroyActivity(token, finishing,
                configChanges, getNonConfigInstance, reason);
        ....

Look at the performDestroyActivity method again
It is the core implementation of the Activity destroy call.

    /** Core implementation of activity destroy call. */
    ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
            int configChanges, boolean getNonConfigInstance, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        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");
            }
            if (getNonConfigInstance) {
                try {
                	// Here's the point
                    r.lastNonConfigurationInstances
                            = r.activity.retainNonConfigurationInstances();
                } catch (Exception e) {
                    if (!mInstrumentation.onException(r.activity, e)) {
                        throw new RuntimeException(
                                "Unable to retain activity "
                                + r.intent.getComponent().toShortString()
                                + ": " + e.toString(), e);
                    }
                }
            }

Focus on performDestroyActivity, r.lastNonConfigurationInstances = r.Activity retainNonConfigurationInstances(); Retainnonconfigurationinstances() of the Activity object was called and the return value was assigned to the lastNonConfigurationInstances property of the R object of ActivityClientRecord type.

Let's look at what activity #retain nonconfigurationinstances does:

    NonConfigurationInstances retainNonConfigurationInstances() {
    	// a key
        Object activity = onRetainNonConfigurationInstance();
        HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
        FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();

        // We're already stopped but we've been asked to retain.
        // Our fragments are taken care of but we need to mark the loaders for retention.
        // In order to do this correctly we need to restart the loaders first before
        // handing them off to the next activity.
        mFragments.doLoaderStart();
        mFragments.doLoaderStop(true);
        ArrayMap<String, LoaderManager> loaders = mFragments.retainLoaderNonConfig();

        if (activity == null && children == null && fragments == null && loaders == null
                && mVoiceInteractor == null) {
            return null;
        }

        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.activity = activity;
        nci.children = children;
        nci.fragments = fragments;
        nci.loaders = loaders;
        if (mVoiceInteractor != null) {
            mVoiceInteractor.retainInstance();
            nci.voiceInteractor = mVoiceInteractor;
        }
        return nci;
    }

onRetainNonConfigurationInstance() is called in retainNonConfigurationInstances.
Here, you know how onRetainNonConfigurationInstance() is called.

Looking back at the last call to handleLaunchActivity in handleRelaunchActivityInner, you should know that handleLaunchActivity is an important step to start Activity by familiarity with the Activity boot process.

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

handleLaunchActivity

    @Override
    public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {
         final Activity a = performLaunchActivity(r, customIntent);
    }

performLaunchActivity
Start the core implementation of Activity

    /**  Core implementation of activity launch. */
    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
            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)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }
        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);

Create an activity instance and call the attach method of the activity. Note that a parameter of the attach method is passed into r.lastNonConfigurationInstances. Are you familiar with it? Just in performDestroyActivity, r.lastNonConfigurationInstances = r.activity retainNonConfigurationInstances(); Retainnonconfigurationinstances() of the activity object was called and the return value was assigned to the lastNonConfigurationInstances property of the R object of ActivityClientRecord type.
It's already strung.

    @UnsupportedAppUsage
    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);
        ...
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
        ...

Assign the previously saved lastNonConfigurationInstances object to the mLastNonConfigurationInstances object of the new Activity instance in the attach.

Let's review how to get the ViewModel

    val mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)

	// Construction method of ViewModelProvider
    public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
        this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
                ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
                : NewInstanceFactory.getInstance());
    }
	// 
    @NonNull
    @Override
    public ViewModelStore getViewModelStore() {
        if (getApplication() == null) {
            throw new IllegalStateException("Your activity is not yet attached to the "
                    + "Application instance. You can't request ViewModel before onCreate call.");
        }
        ensureViewModelStore();
        return mViewModelStore;
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void ensureViewModelStore() {
        if (mViewModelStore == null) {
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                // Restore the ViewModelStore from NonConfigurationInstances
                mViewModelStore = nc.viewModelStore;
            }
            if (mViewModelStore == null) {
                mViewModelStore = new ViewModelStore();
            }
        }
    }

When obtaining the ViewModelStore, the ensureViewModelStore() method is called, and the ensureViewModelStore() will call getLastNonConfigurationInstance() internally
Gets whether there is a cached ViewModelStore object. If so, it returns. If not, a new ViewModelStore instance is created.

getLastNonConfigurationInstance:

    @Nullable
    public Object getLastNonConfigurationInstance() {
        return mLastNonConfigurationInstances != null
                ? mLastNonConfigurationInstances.activity : null;
    }

The internal return of getLastNonConfigurationInstance is the mLastNonConfigurationInstances object just assigned by Activity#attach.

This problem of how to save and restore the ViewModel is solved!

Keywords: Android Interview ViewModel

Added by foreverhex on Fri, 28 Jan 2022 03:22:53 +0200