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:
- call
ActivityManagerService#updateGlobalConfigurationLocked()
Update the current configuration information - call
ActivityManagerService#ensureConfigAndVisibilityAfterUpdate()
Ensure that the given activity uses the current configuration. If the returntrue
State 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#setRetainInstance
Implementation 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.