config变化的一些知识
一. 何时走relaunch,何时onConfigurationChanged
当系统的一些配置发生变化时,比如横竖屏变化,深色模式变化时,
系统会判断,app能否应对处理这次变化,如果不能则relaunch应用的activity,如果可以则回调app对应的onConfigurationChanged方法,让app自行处理
ensureActivityConfiguration
主要限制了finishing和destroyed状态不必处理
boolean ensureActivityConfiguration(boolean ignoreVisibility) {
final Task rootTask = getRootTask();
if (rootTask.mConfigWillChange) {
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Skipping config check "
+ "(will change): %s", this);
return true;
}
// We don't worry about activities that are finishing.
if (finishing) {
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration doesn't matter "
+ "in finishing %s", this);
return true;
}
if (isState(DESTROYED)) {
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Skipping config check "
+ "in destroyed state %s", this);
return true;
}
//如果没有忽略可见性,那么stoping,stopped,不会应该可见的,都不会走入逻辑
if (!ignoreVisibility && (mState == STOPPING || mState == STOPPED || !shouldBeVisible())) {
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Skipping config check "
+ "invisible: %s", this);
return true;
}
if (isConfigurationDispatchPaused()) {
return true;
}
return updateReportedConfigurationAndSend();
}
updateReportedConfigurationAndSend
boolean updateReportedConfigurationAndSend() {
if (isConfigurationDispatchPaused()) {
Slog.wtf(TAG, "trying to update reported(client) config while dispatch is paused");
}
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Ensuring correct "
+ "configuration: %s", this);
final int newDisplayId = getDisplayId();
final boolean displayChanged = mLastReportedDisplayId != newDisplayId;
if (displayChanged) {
mLastReportedDisplayId = newDisplayId;
}
//兼容模式相关,主要是在应用指定了宽高比和方向
// Calling from here rather than from onConfigurationChanged because it's possible that
// onConfigurationChanged was called before mVisibleRequested became true and
// mAppCompatDisplayInsets may not be called again when mVisibleRequested changes. And we
// don't want to save mAppCompatDisplayInsets in onConfigurationChanged without visibility
// check to avoid remembering obsolete configuration which can lead to unnecessary
// size-compat mode.
if (mVisibleRequested) {
// Calling from here rather than resolveOverrideConfiguration to ensure that this is
// called after full config is updated in ConfigurationContainer#onConfigurationChanged.
mAppCompatController.getAppCompatSizeCompatModePolicy().updateAppCompatDisplayInsets();
}
//本次config比较上次无变化则return
// Short circuit: if the two full configurations are equal (the common case), then there is
// nothing to do. We test the full configuration instead of the global and merged override
// configurations because there are cases (like moving a task to the root pinned task) where
// the combine configurations are equal, but would otherwise differ in the override config
mTmpConfig.setTo(mLastReportedConfiguration.getMergedConfiguration());
final ActivityWindowInfo newActivityWindowInfo = getActivityWindowInfo();
final boolean isActivityWindowInfoChanged =
!mLastReportedActivityWindowInfo.equals(newActivityWindowInfo);
if (!displayChanged && !isActivityWindowInfoChanged
&& getConfiguration().equals(mTmpConfig)) {
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration & display "
+ "unchanged in %s", this);
return true;
}
//记录changes为两次变化的差异,getConfigurationChanges方法要注意,
// Okay we now are going to make this activity have the new config.
// But then we need to figure out how it needs to deal with that.
// Find changes between last reported merged configuration and the current one. This is used
// to decide whether to relaunch an activity or just report a configuration change.
final int changes = getConfigurationChanges(mTmpConfig);
// Update last reported values.
final Configuration newMergedOverrideConfig = getMergedOverrideConfiguration();
setLastReportedConfiguration(getProcessGlobalConfiguration(), newMergedOverrideConfig);
setLastReportedActivityWindowInfo(newActivityWindowInfo);
//处于初始化阶段直接return,不必处理变更,直接读新的
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.
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Skipping config check for "
+ "initializing activity: %s", this);
return true;
}
//如果计算之后没有变化,也发送一下
if (changes == 0) {
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration no differences in %s",
this);
// There are no significant differences, so we won't relaunch but should still deliver
// the new configuration to the client process.
if (displayChanged) {
scheduleActivityMovedToDisplay(newDisplayId, newMergedOverrideConfig,
newActivityWindowInfo);
} else {
scheduleConfigurationChanged(newMergedOverrideConfig, newActivityWindowInfo);
}
notifyActivityRefresherAboutConfigurationChange(
mLastReportedConfiguration.getMergedConfiguration(), mTmpConfig);
return true;
}
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration changes for %s, "
+ "allChanges=%s", this, Configuration.configurationDiffToString(changes));
//不在运行中的app,不走下面逻辑
// If the activity isn't currently running, just leave the new configuration and it will
// pick that up next time it starts.
if (!attachedToProcess()) {
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration doesn't matter not running %s", this);
return true;
}
// Figure out how to handle the changes between the configurations.
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Checking to restart %s: changed=0x%s, "
+ "handles=0x%s, mLastReportedConfiguration=%s", info.name,
Integer.toHexString(changes), Integer.toHexString(info.getRealConfigChanged()),
mLastReportedConfiguration);
### ///关键点,如果需要定制化一些独特的系统config,可以在shouldRelaunchLocked()方法中配置,
if (shouldRelaunchLocked(changes, mTmpConfig)) {
// Aha, the activity isn't handling the change, so DIE DIE DIE.
if (mVisible && mAtmService.mTmpUpdateConfigurationResult.mIsUpdating
&& !mTransitionController.isShellTransitionsEnabled()) {
startFreezingScreenLocked(app, mAtmService.mTmpUpdateConfigurationResult.changes);
}
final boolean displayMayChange = mTmpConfig.windowConfiguration.getDisplayRotation()
!= getWindowConfiguration().getDisplayRotation()
|| !mTmpConfig.windowConfiguration.getMaxBounds().equals(
getWindowConfiguration().getMaxBounds());
final boolean isAppResizeOnly = !displayMayChange
&& (changes & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE
| CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT)) == 0;
// Do not preserve window if it is freezing screen because the original window won't be
// able to update drawn state that causes freeze timeout.
// TODO(b/258618073): Always preserve if possible.
final boolean preserveWindow = isAppResizeOnly && !mFreezingScreen;
final boolean hasResizeChange = hasResizeChange(changes & ~info.getRealConfigChanged());
if (hasResizeChange) {
final boolean isDragResizing = task.isDragResizing();
mRelaunchReason = isDragResizing ? RELAUNCH_REASON_FREE_RESIZE
: RELAUNCH_REASON_WINDOWING_MODE_RESIZE;
} else {
mRelaunchReason = RELAUNCH_REASON_NONE;
}
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Config is relaunching %s", this);
if (!mVisibleRequested) {
ProtoLog.v(WM_DEBUG_STATES, "Config is relaunching invisible "
+ "activity %s called by %s", this, Debug.getCallers(4));
}
relaunchActivityLocked(preserveWindow, changes);
// All done... tell the caller we weren't able to keep this activity around.
return false;
}
###///发生了config差异变化,但是应用没走上面的relaunch,自身能处理,则走onConfigurationChanged逻辑
// Default case: the activity can handle this new configuration, so hand it over.
// NOTE: We only forward the override configuration as the system level configuration
// changes is always sent to all processes when they happen so it can just use whatever
// system level configuration it last got.
if (displayChanged) {
scheduleActivityMovedToDisplay(newDisplayId, newMergedOverrideConfig,
newActivityWindowInfo);
} else {
scheduleConfigurationChanged(newMergedOverrideConfig, newActivityWindowInfo);
}
notifyActivityRefresherAboutConfigurationChange(
mLastReportedConfiguration.getMergedConfiguration(), mTmpConfig);
return true;
}
getConfigurationChanges
请注意其中的SizeConfigurationBuckets.filterDiff
private int getConfigurationChanges(Configuration lastReportedConfig) {
// Determine what has changed. May be nothing, if this is a config that has come back from
// the app after going idle. In that case we just want to leave the official config object
// now in the activity and do nothing else.
int changes = lastReportedConfig.diff(getConfiguration());
changes = SizeConfigurationBuckets.filterDiff(
changes, lastReportedConfig, getConfiguration(), mSizeConfigurations);
// We don't want window configuration to cause relaunches.
if ((changes & CONFIG_WINDOW_CONFIGURATION) != 0) {
changes &= ~CONFIG_WINDOW_CONFIGURATION;
}
return changes;
}
//如果变化较小,在可接受范围内,则不必加上config_screen等尺寸变化的config,避免relaunch
//此方法常出现在较为方正的设备的旋转场景,为其忽略变化
public static int filterDiff(int diff, @NonNull Configuration oldConfig,
@NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets) {
if (buckets == null) {
return diff;
}
final boolean nonSizeLayoutFieldsUnchanged =
areNonSizeLayoutFieldsUnchanged(oldConfig.screenLayout, newConfig.screenLayout);
if ((diff & CONFIG_SCREEN_SIZE) != 0) {
final boolean crosses = buckets.crossesHorizontalSizeThreshold(oldConfig.screenWidthDp,
newConfig.screenWidthDp)
|| buckets.crossesVerticalSizeThreshold(oldConfig.screenHeightDp,
newConfig.screenHeightDp);
if (!crosses) {
diff &= ~CONFIG_SCREEN_SIZE;
}
}
if ((diff & CONFIG_SMALLEST_SCREEN_SIZE) != 0) {
final int oldSmallest = oldConfig.smallestScreenWidthDp;
final int newSmallest = newConfig.smallestScreenWidthDp;
if (!buckets.crossesSmallestSizeThreshold(oldSmallest, newSmallest)) {
diff &= ~CONFIG_SMALLEST_SCREEN_SIZE;
}
}
if ((diff & CONFIG_SCREEN_LAYOUT) != 0 && nonSizeLayoutFieldsUnchanged) {
if (!buckets.crossesScreenLayoutSizeThreshold(oldConfig, newConfig)
&& !buckets.crossesScreenLayoutLongThreshold(oldConfig.screenLayout,
newConfig.screenLayout)) {
diff &= ~CONFIG_SCREEN_LAYOUT;
}
}
return diff;
}
shouldRelaunchLocked
继续仔细分析shouldRelaunchLocked这段代码,这段代码成立,则relaunch,不成立,则走onConfigurationChanged逻辑
### ///关键点,如果需要定制化一些独特的系统config,可以在shouldRelaunchLocked()方法中配置,
if (shouldRelaunchLocked(changes, mTmpConfig)) {
// Aha, the activity isn't handling the change, so DIE DIE DIE.
if (mVisible && mAtmService.mTmpUpdateConfigurationResult.mIsUpdating
&& !mTransitionController.isShellTransitionsEnabled()) {
startFreezingScreenLocked(app, mAtmService.mTmpUpdateConfigurationResult.changes);
}
final boolean displayMayChange = mTmpConfig.windowConfiguration.getDisplayRotation()
!= getWindowConfiguration().getDisplayRotation()
|| !mTmpConfig.windowConfiguration.getMaxBounds().equals(
getWindowConfiguration().getMaxBounds());
final boolean isAppResizeOnly = !displayMayChange
&& (changes & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE
| CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT)) == 0;
// Do not preserve window if it is freezing screen because the original window won't be
// able to update drawn state that causes freeze timeout.
// TODO(b/258618073): Always preserve if possible.
final boolean preserveWindow = isAppResizeOnly && !mFreezingScreen;
final boolean hasResizeChange = hasResizeChange(changes & ~info.getRealConfigChanged());
if (hasResizeChange) {
final boolean isDragResizing = task.isDragResizing();
mRelaunchReason = isDragResizing ? RELAUNCH_REASON_FREE_RESIZE
: RELAUNCH_REASON_WINDOWING_MODE_RESIZE;
} else {
mRelaunchReason = RELAUNCH_REASON_NONE;
}
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Config is relaunching %s", this);
if (!mVisibleRequested) {
ProtoLog.v(WM_DEBUG_STATES, "Config is relaunching invisible "
+ "activity %s called by %s", this, Debug.getCallers(4));
}
relaunchActivityLocked(preserveWindow, changes);
// All done... tell the caller we weren't able to keep this activity around.
return false;
}
重点方法shouldRelaunchLocked,
- 此方法主要读取app自身声明可适配的所有config和当前changes的差异
- 各家定制rom可在此方法里,定制需要屏蔽哪些config,在那些场景不做relaunch
private boolean shouldRelaunchLocked(int changes, Configuration changesConfig) {
###///configChanged代表应用在manifest中定义的configChanges
int configChanged = info.getRealConfigChanged();
if (android.content.res.Flags.handleAllConfigChanges()) {
if ((configChanged & CONFIG_RESOURCES_UNUSED) != 0) {
// Don't relaunch any activities that claim they do not use resources at all.
// If they still do, the onConfigurationChanged() callback will get called to
// let them know anyway.
return false;
}
}
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 (info.applicationInfo.targetSdkVersion < O
&& requestedVrComponent != null
&& onlyVrUiModeChanged) {
configChanged |= CONFIG_UI_MODE;
}
// TODO(b/274944389): remove workaround after long-term solution is implemented
// Don't restart due to desk mode change if the app does not have desk resources.
if (mWmService.mSkipActivityRelaunchWhenDocking && onlyDeskInUiModeChanged(changesConfig)
&& !hasDeskResources()) {
configChanged |= CONFIG_UI_MODE;
}
return (changes & (~configChanged)) != 0;
}
总结
- 如果没有忽略可见性,即没有声明为不可见app也处理config,那么stopping,stopped,finishing,destroyed,和不应该可见的,都不会进入该逻辑中
- 逻辑判断是否relaunch依赖于manifest中声明的configChanges
- shouldRelaunchLocked是决定走relaunch还是onConfigurationChanged的关键判断
- 横竖屏旋转并不一定会下发screen_size变化,类似的有CONFIG_SCREEN_SIZE,CONFIG_SMALLEST_SCREEN_SIZE,CONFIG_SCREEN_LAYOUT都有可能在差异变化较小,或者未跨越阈值范围时,被忽略
二. app直接读系统config属性
我们常用到settings.system.xxxxx来获取当前的系统属性
当一个config变化时,系统会总是计算当前变化中是否字体font和语言locals变化差异
然后最终将他们持久化到系统属性中
/** @hide */
public static boolean putConfigurationForUser(ContentResolver cr, Configuration config,
int userHandle) {
return Settings.System.putFloatForUser(cr, FONT_SCALE, config.fontScale, userHandle) &&
Settings.System.putStringForUser(
cr, SYSTEM_LOCALES, config.getLocales().toLanguageTags(), userHandle,
DEFAULT_OVERRIDEABLE_BY_RESTORE);
}
该方法在updateGlobalConfigurationLocked方法每次更新config时,被调用,仅更新语言和字体
三. config变化后,后台app直接进程重启的场景
当config变化时如果走到下面的else if条件,会直接把app的进程重启了,请注意
如果某个应用指定了不可resizable和指定了方向,即处于兼容模式
当他此刻处于后台时,并且此次变化
- 涉及到了尺寸变化且不是方向变化时
- 或者设计density变化时,
将会重启进程以覆盖配置
public void onConfigurationChanged(Configuration newParentConfig) {
... ...
if (mVisibleRequested) {
// It may toggle the UI for user to restart the size compatibility mode activity.
display.handleActivitySizeCompatModeIfNeeded(this);
} else if (getAppCompatDisplayInsets() != null && !visibleIgnoringKeyguard
&& (app == null || !app.hasVisibleActivities())) {
// visibleIgnoringKeyguard is checked to avoid clearing mAppCompatDisplayInsets during
// displays change. Displays are turned off during the change so mVisibleRequested
// can be false.
// The override changes can only be obtained from display, because we don't have the
// difference of full configuration in each hierarchy.
final int displayChanges = display.getCurrentOverrideConfigurationChanges();
final int orientationChanges = CONFIG_WINDOW_CONFIGURATION
| CONFIG_SCREEN_SIZE | CONFIG_ORIENTATION;
final boolean hasNonOrienSizeChanged = hasResizeChange(displayChanges)
// Filter out the case of simple orientation change.
&& (displayChanges & orientationChanges) != orientationChanges;
// For background activity that uses size compatibility mode, if the size or density of
// the display is changed, then reset the override configuration and kill the activity's
// process if its process state is not important to user.
if (hasNonOrienSizeChanged || (displayChanges & ActivityInfo.CONFIG_DENSITY) != 0) {
restartProcessIfVisible();
}
}
}