How does ViewModel retain data after configuration changes

Time:2022-5-25

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

Today, let’s explore how to retain data after configuration changes.

First, let’s look at how to createViewModelexample:

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
    }
}

establishViewModelexample

// 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() }

ViewModelProviderSource code

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;
}

ViewModelStoreOwnerSource code

public interface ViewModelStoreOwner {
    @NonNull
    ViewModelStore getViewModelStore();
}

Look at what the above code does:

  1. First, create an instance of viewmodelprovider,ViewModelProviderThe first argument to the constructor isViewModelStoreOwner, then why can it be passed inActivityExamples, becauseComponentActivityRealizedViewModelStoreOwnerThis interface;

  2. adoptget(@NonNull Class<T> modelClass)Method getsViewModelExamples of;

  3. adoptowner.getViewModelStore()GetViewModelStore, that isComponentActivityofgetViewModelStore()method.

Then let’s take a lookViewModelProvider#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);
        return viewModel;
    }
}

As you can see from the code above,ViewModelStoreHold one inHashMap, ifViewModelStoreCached inViewModelInstance, return directly. Otherwise, create a new instance and save it toViewModelStoreYes.

Next, let’s take a lookComponentActivity#getViewModelStore()method:

@NonNull
public ViewModelStore getViewModelStore() {
    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() {
    if (this.mViewModelStore == null) {
        ComponentActivity.NonConfigurationInstances nc = (ComponentActivity.NonConfigurationInstances)this.getLastNonConfigurationInstance();
        if (nc != null) {
            this.mViewModelStore = nc.viewModelStore;
        }

        if (this.mViewModelStore == null) {
            this.mViewModelStore = new ViewModelStore();
        }
    }

}

As can be seen from the above code:

  1. First fromActivityofgetLastNonConfigurationInstance()Get oneNonConfigurationInstancesexamplenc

  2. If thisncIf it is not empty, it willncViewmodelstore assigned toComponentActivityofmViewModelStoreField;

  3. IfmViewModelStoreOr fornull, create a new onemViewModelStoreObject;

ObviouslyActivityofgetLastNonConfigurationInstance()Is cacheViewModelStoreThe core of.

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

This method returnsActivityofonRetainNonConfigurationInstance()Hold the object, butActivityThis method returnsnull

According to the source code comments,Once the configuration changes and destroys the activity, the system will create a new activity instance for the new configuration. This method will be called by the Android system

We can return any object in this method, including the activity instance itself, which can be called later in the new activity instancegetLastNonfigurationInstance()To retrieve it.

Continue to track the source code and find it inActivityofretainNonConfigurationInstances()Called inonRetainNonConfigurationInstance()method.

NonConfigurationInstances retainNonConfigurationInstances() {
    // 1
    Object 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 = new NonConfigurationInstances();
    nci.activity = activity;
    nci.children = children;
    nci.fragments = fragments;
    nci.loaders = loaders;
    ......
    return nci;
}

As can be seen from the above code:

  1. First callonRetainNonConfigurationInstance()Get an object, which can be any object we want to save when the configuration is changed;

  2. As long as these objects are allnull, no data will be returned;

  3. Create a static inner classNonConfigurationInstancesExamples ofnciAnd then assign it tonciofactivityField;

So who didActivityofonRetainNonConfigurationInstance()Well, by tracking the code, it is found thatComponentActivityOverride this method:

public final Object onRetainNonConfigurationInstance() {
    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;
        return nci;
    }
}

To sum up:

  1. If we want to save any object when the system configuration changes and restore this object when the system creates a new activity instance for the new configuration, we only need to rewrite the activityonRetainNonConfigurationInstance()Method is enough.
  2. According to Article 1, when a configuration change destroys an activity,onRetainNonConfigurationInstance()SavedViewModelStore, andViewModelPass againViewModelStoreTo access, so whenActivityWhen you rebuild, you can get the previous informationViewModel

How is the system retained and restoredNonConfigurationInstancesof

As we know from the previous chapter,ActivityofonRetainNonConfigurationInstance()It is called by the system. When did the system call it?

According to experience,ActivityThreadbe responsible forActivityScheduling and execution, then we’ll goActivityThreadSearch inActivityofretainNonConfigurationInstances()method.

Tip: Android framework source code generally has the following relationship links:

schedule(arrangement) – >handler(treatment) – >perform(execution) – >on(in progress)

ActivityThreadofperformDestroyActivity()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;
}

WhenActivityCalled when destroyed or rebuilt due to configuration changesActivityThreadofhandleRelaunchActivityInner()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);
}

As can be seen from the above code:

  1. Execute firsthandleDestroyActivity, and willgetNonConfigInstanceSet astrue, so you canActivityofonRetainNonConfigurationInstance()Retained data, saving toActivityClientRecordMedium;
  2. Then executehandleLaunchActivity(), rebuildActivityAnd restore the data.

ActivityThreadofperformLaunchActivity()

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;
}


As can be seen from the above code:

  1. establishActivityExamples of;
  2. attachData will be destroyed due to configuration changesActivityRetained toActivityClientRecordHeld inlastNonConfigurationInstancesAssign to newActivity
  3. Then you can callActivitygetLastNonConfigurationInstance() 检索到 onRetainNonConfigurationInstance() 方法保留的对象。

Activityattach()方法

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() {
    return mLastNonConfigurationInstances != null
            ? mLastNonConfigurationInstances.activity : null;
}

How does ViewModel retain data after configuration changes

image.png

上图是Framework源码留存和恢复NonConfigurationInstances的大致调用流程。

ViewModel 和 onSaveInstanceState 情况一样吗

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

onSaveInstanceState 方法在 Activity 可能被杀死之前被调用,以便当它在将来某个时间返回时可以恢复它的状态。例如,如果Activity B 在 Activity A 的前面被启动,在某个时间点 Activity A 被杀死回收资源,Activity A 将有机会通过这个方法保存其用户界面的当前状态,这样当用户返回到 Activity A 时,用户界面的状态可以通过 onCreate()onRestoreInstanceState() 来恢复。

如果该方法被调用,该方法将发生在以 android.os.Build.VERSION_CODESP 平台版本的应用程序的 onStop 之后。对于以较早平台版本为目标的应用程序,该方法将出现在 onStop 之前,并且不能保证它将出现在 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>
    }
}

Activity 通常会在下面三种情况下被销毁:

  1. 从当前界面永久离开:用户导航至其他界面或直接关闭 Activity (通过点击返回按钮或执行的操作调用了 finish 方法)。对应 Activity 实例被永久关闭;

  2. Activity 配置 (configuration) 被改变: 例如,旋转屏幕等操作,会使 Activity 需要立即重建;

  3. 应用在后台时,其进程被系统杀死:这种情况发生在设备剩余运行内存不足,系统又亟须释放一些内存的时候。当进程在后台被杀死后,用户又返回该应用时,Activity 也需要被重建。

在后两种情况中,我们通常都希望重建 ActivityViewModel 会帮我们处理第二种情况,因为在这种情况下 ViewModel 没有被销毁;而在第三种情况下, ViewModel 被销毁了。所以一旦出现了第三种情况,便需要在 ActivityonSaveInstanceState 相关回调中保存和恢复 ViewModel 中的数据。