config变化的一些知识

253 阅读7分钟

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,

  1. 此方法主要读取app自身声明可适配的所有config和当前changes的差异
  2. 各家定制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;
    }

总结

  1. 如果没有忽略可见性,即没有声明为不可见app也处理config,那么stopping,stopped,finishing,destroyed,和不应该可见的,都不会进入该逻辑中
  1. 逻辑判断是否relaunch依赖于manifest中声明的configChanges
  2. shouldRelaunchLocked是决定走relaunch还是onConfigurationChanged的关键判断
  3. 横竖屏旋转并不一定会下发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和指定了方向,即处于兼容模式

当他此刻处于后台时,并且此次变化

  1. 涉及到了尺寸变化且不是方向变化时
  2. 或者设计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();
            }
        }
    }