Configuration Changed Event transfer process

When a device configuration changes, AMS calls its updateConfiguration() method to notify AMS to handle the Configuration Changed event. Activitymanage Service#updateConfiguration()

public boolean updateConfiguration(Configuration values) {
    / /... Omit a piece of code
    synchronized(this) {
        / /... Omit a piece of code
        try {
            if(values ! =null) {
                Settings.System.clearConfiguration(values);
            }
            updateConfigurationLocked(values, null.false.false /* persistent */,
                    UserHandle.USER_NULL, false /* deferResume */,
                    mTmpUpdateConfigurationResult);
            returnmTmpUpdateConfigurationResult.changes ! =0;
        } finally{ Binder.restoreCallingIdentity(origId); }}}Copy the code

Can see ActivityManagerService# updateConfiguration () calls the internal ActivityManagerService# updateConfigurationLocked (), the method is totally did two things:

  1. callActivityManagerService#updateGlobalConfigurationLocked()Update the current configuration information
  2. callActivityManagerService#ensureConfigAndVisibilityAfterUpdate()Ensure that the given activity uses the current configuration. If the returntrueState that the activity is not restarted. Otherwise, make the activity destroyed to match the current configuration.

ActivityManagerService# ensureConfigAndVisibilityAfterUpdate () of the source code is as follows:

    private boolean ensureConfigAndVisibilityAfterUpdate(ActivityRecord starting, int changes) {
        boolean kept = true;
        // Get the activity that currently has focus
        final ActivityStack mainStack = mStackSupervisor.getFocusedStack();
        // mainStack is null during startup.
        if(mainStack ! =null) {
            if(changes ! =0 && starting == null) {
                // If the configuration changed, and the caller is not already
                // in the process of starting an activity, then find the top
                // activity to check if its configuration needs to change.
                starting = mainStack.topRunningActivityLocked();
            }

            if(starting ! =null) {
                // Key code
                kept = starting.ensureActivityConfiguration(changes,
                        false /* preserveWindow */);
                // And we need to make sure at this point that all other activities
                // are made visible with the correct configuration.
                mStackSupervisor.ensureActivitiesVisibleLocked(starting, changes,
                        !PRESERVE_WINDOWS);
            }
        }

        return kept;
    }
Copy the code

Here you can see, really do the Activity configuration update is ActivityRecord# ensureActivityConfiguration () method, this method is called its overloading method, its source code is as follows:

boolean ensureActivityConfiguration(int globalChanges, boolean preserveWindow,
            boolean ignoreStopState) {
        final ActivityStack stack = getStack();
        // If you call updateConfiguration() again soon, ignore this change and leave it to the next process to save time
        if (stack.mConfigWillChange) {
            if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION,
                    "Skipping config check (will change): " + this);
            return true;
        }

        // We don't worry about activities that are finishing.
        // If the current activity already finishes, ignore it
        if (finishing) {
            if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION,
                    "Configuration doesn't matter in finishing " + this);
            stopFreezingScreenLocked(false);
            return true;
        }
        / /... Omit a piece of code
        if (mState == INITIALIZING) {
            // No need to relaunch or schedule new config for activity that hasn't been launched
            // yet. We do, however, return after applying the config to activity record, so that
            // it will use it for launch transaction.
            if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION,
                    "Skipping config check for initializing activity: " + this);
            return true;
        }

        if (shouldRelaunchLocked(changes, mTmpConfig) || forceNewConfig) {
            // Aha, the activity isn't handling the change, so DIE DIE DIE.
            configChangeFlags |= changes;
            startFreezingScreenLocked(app, globalChanges);
            forceNewConfig = false;
            preserveWindow &= isResizeOnlyChange(changes);
            if (app == null || app.thread == null) {
                / /... Omit log code
                // If the app is not in a hosted state, only the current activity is destroyed
                stack.destroyActivityLocked(this.true."config");
            } else if (mState == PAUSING) {
                / /... Omit log code
                // If the activity is currently in the PAUSING state, mark it to restart and wait until the PAUSING state is relaunched
                deferRelaunchUntilPaused = true;
                preserveWindowOnDeferredRelaunch = preserveWindow;
                return true;
            } else if (mState == RESUMED) {
                / /... Omit a piece of code
                If your activity is in the RESUMED state, you need to restart it to resume to the RESUMED state
                relaunchActivityLocked(true /* andResume */, preserveWindow);
            } else {
                / /... Omit log code
                relaunchActivityLocked(false /* andResume */, preserveWindow);
            }
            // The activity processes the configuration changed itself and does not need to restart
            return false;
        }
        // The Activity can handle configuration changes by itself
        if (displayChanged) {
            scheduleActivityMovedToDisplay(newDisplayId, newMergedOverrideConfig);
        } else {
            scheduleConfigurationChanged(newMergedOverrideConfig);
        }
        return true;
    }
Copy the code

You can see that the key code for deciding whether reLaunch is needed is ActivityRecord#shouldRelaunchLocked(changes, mTmpConfig). Another point of concern is the forceNewConfig variable, which, if changed to true, forces the activity to restart, ignoring the configChanges configuration of the activity. Its value is true only when ActivityStack#restartPackage() is called. ActivityRecord#shouldRelaunchLocked

private boolean shouldRelaunchLocked(int changes, Configuration changesConfig) {
        // Get the configChanges property configured in the manifest
        int configChanged = info.getRealConfigChanged();
        boolean onlyVrUiModeChanged = onlyVrUiModeChanged(changes, changesConfig);

        // Override for apps targeting pre-O sdks
        // If a device is in VR mode, and we're transitioning into VR ui mode, add ignore ui mode
        // to the config change.
        // For O and later, apps will be required to add configChanges="uimode" to their manifest.
        if(appInfo.targetSdkVersion < O && requestedVrComponent ! =null
                && onlyVrUiModeChanged) {
            configChanged |= CONFIG_UI_MODE;
        }
        // Key code
        return(changes&(~configChanged)) ! =0;
    }
Copy the code

Yes, exactly (changes&(~configChanged))! = 0 determines whether to reLaunch by comparing whether the change event is within the scope of the Activity itself to handle, and configChanged is the Android :configChanges property we configured in manifest.xml.

ReLunch call flow

Moving on, we can see that the ActivityRecord#relaunchActivityLocked() method actually performs the reLuanch logic:

void relaunchActivityLocked(boolean andResume, boolean preserveWindow) {
        / /... Omit a piece of code
        try {
            / /... Omit log code
            final ClientTransactionItem callbackItem = ActivityRelaunchItem.obtain(pendingResults,
                    pendingNewIntents, configChangeFlags,
                    new MergedConfiguration(service.getGlobalConfiguration(),
                            getMergedOverrideConfiguration()),
                    preserveWindow);
            final ActivityLifecycleItem lifecycleItem;
            if (andResume) {
                lifecycleItem = ResumeActivityItem.obtain(service.isNextTransitionForward());
            } else {
                lifecycleItem = PauseActivityItem.obtain();
            }
            final ClientTransaction transaction = ClientTransaction.obtain(app.thread, appToken);
            // Key code 1
            transaction.addCallback(callbackItem);
            transaction.setLifecycleStateRequest(lifecycleItem);
            // Key code 2
            service.getLifecycleManager().scheduleTransaction(transaction);
        } catch (RemoteException e) {
            if (DEBUG_SWITCH || DEBUG_STATES) Slog.i(TAG_SWITCH, "Relaunch failed", e);
        }
        / /... Omit a piece of code
    }
Copy the code

First let’s look at key code 2. Service. GetLifecycleManager () returns a ClientLifecycleManager instance, Its scheduleTransaction(ClientTransaction) method will eventually call ClientTransaction#schedule():

    public void schedule(a) throws RemoteException {
        // mClient is an ApplicationThead variable
        mClient.scheduleTransaction(this);
    }
Copy the code

The final message is forwarded to ActivityThread via ApplicationThead and ActivityThread#sendMessage(ActivityThread.h.execute_TRANSACTION, Transaction is transferred to H.

case EXECUTE_TRANSACTION:
    final ClientTransaction transaction = (ClientTransaction) msg.obj;
    mTransactionExecutor.execute(transaction);
    if (isSystem()) {
       transaction.recycle();
    }
    break;
Copy the code

H executes the RANSaction via TransactionExecutor, internally iterates through the callbacks passed to the transaction via addCallback(), Callbacks the execute (ClientTransactionHandler IBinder, PendingTransactionActions) can be invoked, The ClientTransactionHandler object that the callback receives is of type exactly ActivityThread. Let’s go back to key code 1. Here we add a Callback of type ActivityRelaunchItem. It inherits from ClientTransactionItem, In it execute() calls ClientTransactionHandler#handleRelaunchActivity(mActivityClientRecord, pendingActions) to execute the real reluanch logic:

public void execute(ClientTransactionHandler client, IBinder token,PendingTransactionActions pendingActions) {
    if (mActivityClientRecord == null) {
        if (DEBUG_ORDER) Slog.d(TAG, "Activity relaunch cancelled");
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart");
    client.handleRelaunchActivity(mActivityClientRecord, pendingActions);
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
Copy the code

From the analysis of key code 2, we know that ActivityThread#handleRelaunchActivity() is actually called:

    public void handleRelaunchActivity(ActivityClientRecord tmp,PendingTransactionActions pendingActions) {
        / /... Omit a piece of code
        if(changedConfig ! =null) {
            mCurDefaultDisplayDpi = changedConfig.densityDpi;
            updateDefaultDensity();
            handleConfigurationChanged(changedConfig, null);
        }
        ActivityClientRecord r = mActivities.get(tmp.token);
        / /... Omit a piece of code
        handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
                pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");
         / /... Omit a piece of code
    }
Copy the code

In handleRelaunchActivityInner (), call first ActivityThread# handleDestroyActivity () to destroy the current activity, Call ActivityThread#handleLaunchActivity() to restart the activity. The Activity, as we all know, there is a callback method onRetainNonConfigurationInstance (), when the device information change, will save the method returns the Object, After can be in the Activity of restart by getLastNonConfigurationInstance () to get the Object. OnRetainNonConfigurationInstance () is not only in the case of reLaunchActivity callback, but the Activity destoryed, In ActivityThread. PerformDestroyActivity () call Activity. RetainNonConfigurationInstances (). This method returns the NonConfigurationInstances, its activity attribute is called the activity. The onRetainNonConfigurationInstance (). And the getLastNonConfigurationInstance () can get to the value, because will be the same in reLaunchActivity ActivityRecord as a parameter, passed to a new Activity. The method is in ComponentActivity, have been rewritten as the final method, a subclass if you want to save the data, can pass onRetainCustomNonConfigurationInstance replacement, but the official is recommended to use the ViewModel component to replace it, The ViewModel is saved in this way because it recovers after the device rotates.

Activity#onConfigurationChanged(Configuration)Call time

The activity only calls onConfigurationChanged(Configuration) when the changed Configuration is in the activity’s self-handling Configuration list. Where is this called? The answer is in ActivityRecord ensureActivityConfiguration () method.

    // The Activity can handle configuration changes by itself
    if (displayChanged) {
        scheduleActivityMovedToDisplay(newDisplayId, newMergedOverrideConfig);
    } else {
        scheduleConfigurationChanged(newMergedOverrideConfig);
    }
Copy the code

The two branches, the final will be called ClientTransactionHandler. HandleActivityConfigurationChanged () method, the method is implemented by ActivityThread:

    public void handleActivityConfigurationChanged(IBinder activityToken,Configuration overrideConfig, int displayId) {
        / /... Omit a piece of code
        final booleanmovedToDifferentDisplay = displayId ! = INVALID_DISPLAY && displayId ! = r.activity.getDisplay().getDisplayId();/ /... Omit a piece of code
        if (movedToDifferentDisplay) {
            / /... Omit a piece of code
            final Configuration reportedConfig = performConfigurationChangedForActivity(r,
                    mCompatConfiguration, displayId, true /* movedToDifferentDisplay */);
            / /... Omit a piece of code
        } else {
            if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle activity config changed: "
                    + r.activityInfo.name + ", config=" + overrideConfig);
            performConfigurationChangedForActivity(r, mCompatConfiguration);
        }
        / /... Omit a piece of code
    }
Copy the code

PerformActivityConfigurationChanged performConfigurationChangedForActivity () will call () method, the method is as follows:

    private Configuration performActivityConfigurationChanged(Activity activity,Configuration newConfig, Configuration amOverrideConfig, int displayId,boolean movedToDifferentDisplay) {
        / /... Omit a piece of code
        boolean shouldChangeConfig = false;
        if (activity.mCurrentConfig == null) {
            shouldChangeConfig = true;
        } else {
            // If the new config is the same as the config this Activity is already running with and
            // the override config also didn't change, then don't bother calling
            // onConfigurationChanged.
            final int diff = activity.mCurrentConfig.diffPublicOnly(newConfig);

            if(diff ! =0| |! mResourcesManager.isSameResourcesOverrideConfig(activityToken, amOverrideConfig)) {// Always send the task-level config changes. For system-level configuration, if
                // this activity doesn't handle any of the config changes, then don't bother
                // calling onConfigurationChanged as we're going to destroy it.
                // If the shared configuration changes
                // mUpdatingSystemConfig is false so shouldChangeConfig=true
                if(! mUpdatingSystemConfig || (~activity.mActivityInfo.getRealConfigChanged() & diff) ==0| |! REPORT_TO_ACTIVITY) { shouldChangeConfig =true; }}}if(! shouldChangeConfig && ! movedToDifferentDisplay) {// Nothing significant, don't proceed with updating and reporting.
            return null;
        }
        / /... Omit a piece of code
        if (shouldChangeConfig) {
            activity.mCalled = false;
            activity.onConfigurationChanged(configToReport);
            if(! activity.mCalled) {throw new SuperNotCalledException("Activity " + activity.getLocalClassName() +
                                " did not call through to super.onConfigurationChanged()"); }}return configToReport;
    }
Copy the code

In the end we found the activity. OnConfigurationChanged (configToReport) call position. At this point, the device change is that the activity’s lifecycle call flow is analyzed.

Fragment#setRetainInstanceImplementation principle of

As we all know, by calling Fragment#setRetainInstance(true), we can preserve the Fragment instead of destroying the rebuild when it is rebuilt due to a configuration change. So how does this effect work? Fragment#setRetainInstance() :

    public void setRetainInstance(boolean retain) {
        mRetainInstance = retain;
        if(mFragmentManager ! =null) {
            if (retain) {
                mFragmentManager.addRetainedFragment(this);
            } else {
                mFragmentManager.removeRetainedFragment(this); }}else {
            mRetainInstanceChangedWhileDetached = true; }}Copy the code

Continue to track, found that fragments instance is added to the FragmentManagerViewModel. MRetainedFragments, this is a HashSet variables of type, thought the same only allowed to be added again. So when are these fragments saved? Think before the ViewModel preservation, yes is Activity# retainNonConfigurationInstances () :

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

        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;
        // Assign to the encapsulated variable
        nci.fragments = fragments;
        nci.loaders = loaders;
        if(mVoiceInteractor ! =null) {
            mVoiceInteractor.retainInstance();
            nci.voiceInteractor = mVoiceInteractor;
        }
        return nci;
    }
Copy the code

We track mFragments. RetainNestedNonConfig () look down, found the calling process is as follows: FragmentController#retainNestedNonConfig() -> FragmentManagerImpl#retainNonConfig() ->FragmentManagerViewModel#getSnapshot(), the method is as follows:

    FragmentManagerNonConfig getSnapshot(a) {
        if (mRetainedFragments.isEmpty() && mChildNonConfigs.isEmpty()
                && mViewModelStores.isEmpty()) {
            return null;
        }
        HashMap<String, FragmentManagerNonConfig> childNonConfigs = new HashMap<>();
        for (Map.Entry<String, FragmentManagerViewModel> entry : mChildNonConfigs.entrySet()) {
            FragmentManagerNonConfig childNonConfig = entry.getValue().getSnapshot();
            if(childNonConfig ! =null) {
                childNonConfigs.put(entry.getKey(), childNonConfig);
            }
        }

        mHasSavedSnapshot = true;
        if (mRetainedFragments.isEmpty() && childNonConfigs.isEmpty()
                && mViewModelStores.isEmpty()) {
            return null;
        }
        return new FragmentManagerNonConfig(
                new ArrayList<>(mRetainedFragments),
                childNonConfigs,
                new HashMap<>(mViewModelStores));
    }
Copy the code

As you can see, if we set a preserved Fragment, the preserved Fragment will eventually be stored in the FragmentManagerNonConfig variable. To be Activity# retainNonConfigurationInstances save (), as a parameter to reluanch after the Activity, thus fragments preservation. Let’s look at how preserved fragments are restored. By reading Activity# onCreate () method we found that if the Activity mLastNonConfigurationInstances is not null, the device configuration changes to save the data, then will be back on fragments. Key code fragments of the state of the recovery is mFragments restoreAllState (), the mFragments here is FragmentController instance, The interior of this method finally calls FragmentManagerImpl#restoreSaveState(). Let’s look at the implementation of this method:

    void restoreSaveState(Parcelable state) {
        / /... Omit a piece of code
        // First re-attach any non-config instances we are retaining back
        // to their saved state, so we don't try to instantiate them again.
        // Iterate over retained Fragments
        for (Fragment f : mNonConfig.getRetainedFragments()) {
            if (DEBUG) Log.v(TAG, "restoreSaveState: re-attaching retained " + f);
            FragmentState fs = null;
            // Get the status of Fragments that were previously Active
            for (FragmentState fragmentState : fms.mActive) {
                if (fragmentState.mWho.equals(f.mWho)) {
                    fs = fragmentState;
                    break; }}// State change lifecycle is not obtained
            if (fs == null) {
                if (DEBUG) {
                    Log.v(TAG, "Discarding retained Fragment " + f
                            + " that was not found in the set of active Fragments " + fms.mActive);
                }
                // We need to ensure that onDestroy and any other clean up is done
                // so move the Fragment up to CREATED, then mark it as being removed, then
                // destroy it.
                moveToState(f, Fragment.CREATED, 0.0.false);
                f.mRemoving = true;
                moveToState(f, Fragment.INITIALIZING, 0.0.false);
                continue;
            }
            // Restore the Fragment state
            fs.mInstance = f;
            f.mSavedViewState = null;
            f.mBackStackNesting = 0;
            f.mInLayout = false;
            f.mAdded = false; f.mTargetWho = f.mTarget ! =null ? f.mTarget.mWho : null;
            f.mTarget = null;
            if(fs.mSavedFragmentState ! =null) { fs.mSavedFragmentState.setClassLoader(mHost.getContext().getClassLoader()); f.mSavedViewState = fs.mSavedFragmentState.getSparseParcelableArray( FragmentManagerImpl.VIEW_STATE_TAG); f.mSavedFragmentState = fs.mSavedFragmentState; }}/ /... Omit a piece of code
    }
Copy the code

The state of the remaining Fragments has been restored.