Android 14 昼夜色切换多屏时候非主屏的Activity无法收到onConfigurationChanged
记录一下遇见的这个问题和查看源码的过程
首先先说遇见的问题
Android 14 昼夜色切换多屏时候 非主屏的Activity 会经常收不到 onConfigurationChanged的回调
分析原因源码中ActivityThread::performActivityConfigurationChanged 里面
private Configuration performActivityConfigurationChanged(ActivityClientRecord r,
Configuration newConfig, Configuration amOverrideConfig, int displayId,
boolean alwaysReportChange) {
final Activity activity = r.activity;
final IBinder activityToken = activity.getActivityToken();
// WindowConfiguration differences aren't considered as public, check it separately.
// multi-window / pip mode changes, if any, should be sent before the configuration
// change callback, see also PinnedStackTests#testConfigurationChangeOrderDuringTransition
handleWindowingModeChangeIfNeeded(r, newConfig);
final boolean movedToDifferentDisplay = isDifferentDisplay(activity.getDisplayId(),
displayId);
final Configuration currentResConfig = activity.getResources().getConfiguration();
final int diff = currentResConfig.diffPublicOnly(newConfig);
final boolean hasPublicResConfigChange = diff != 0;
// TODO(b/173090263): Use diff instead after the improvement of AssetManager and
// ResourcesImpl constructions.
final boolean shouldUpdateResources = hasPublicResConfigChange
|| shouldUpdateResources(activityToken, currentResConfig, newConfig,
amOverrideConfig, movedToDifferentDisplay, hasPublicResConfigChange);
final boolean shouldReportChange = shouldReportChange(
activity.mCurrentConfig, newConfig, r.mSizeConfigurations,
activity.mActivityInfo.getRealConfigChanged(), alwaysReportChange);
// Nothing significant, don't proceed with updating and reporting.
if (!shouldUpdateResources && !shouldReportChange) {
return null;
}
。。。。省略
shouldReportChange 这个值出问题时候是个false 也就是说系统认为没有变化
activity.mConfigChangeFlags = 0;
if (shouldReportChange) {
activity.mCalled = false;
activity.mCurrentConfig = new Configuration(newConfig);
activity.onConfigurationChanged(configToReport);
if (!activity.mCalled) {
throw new SuperNotCalledException("Activity " + activity.getLocalClassName() +
" did not call through to super.onConfigurationChanged()");
}
}
}
shouldReportChange 这个值出问题时候是个false 也就是说系统认为没有变化
可是为什么 shouldReportChange 是false?而不是true?昼夜色变化一定会发生改变的,我们看下这个shouldReportChange 方法
@VisibleForTesting
public static boolean shouldReportChange(@Nullable Configuration currentConfig,
@NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets sizeBuckets,
int handledConfigChanges, boolean alwaysReportChange) {
final int publicDiff = currentConfig.diffPublicOnly(newConfig);
// Don't report the change if there's no public diff between current and new config.
if (publicDiff == 0) {
return false;
// Report the change regardless if the changes across size-config-buckets.
if (alwaysReportChange) {
return true;
}
final int diffWithBucket = SizeConfigurationBuckets.filterDiff(publicDiff, currentConfig,
newConfig, sizeBuckets);
// Compare to the diff which filter the change without crossing size buckets with
// {@code handledConfigChanges}. The small changes should not block Activity to receive
// its handlConfigChanges}. The small changes should not block Activity to receive
// its handled config updates. Also, if Activity handles all small changes, we should
// dispatch the updated config to it.
final int diff = diffWithBucket != 0 ? diffWithBucket : publicDiff;
// If this activity doesn't handle any of the config changes, then don't bother
// calling onConfigurationChanged. Otherwise, report to the activity for the
// changes.
return (~handledConfigChanges & diff) == 0;
}
final int publicDiff = currentConfig.diffPublicOnly(newConfig);
这个方法比较了新老的 config是否有区别的,而且大家可以感兴趣看一下这个currentConfig.diffPublicOnly方法,这个方法check了uimode也就是新的uimode和老的uimode是否一个值
但是不可能,因为新的uimode如果没有变化就不应该回调,但是回调了就是因为有变化,于是我们接着看这个地方newConfig 是从何而来
我们先看一下到这的调用栈
ActivityThread::handleActivityConfigurationChanged
ActivityThread::performConfigurationChangedForActivity (这步获取newconfig)
ActivityThread::performConfigurationChangedForActivity
ActivityThread::performActivityConfigurationChanged
activity::onConfigurationChanged
在ActivityThread::performConfigurationChangedForActivity 获取新的config
看看performConfigurationChangedForActivity 代码
void handleActivityConfigurationChanged(ActivityClientRecord r,
@NonNull Configuration overrideConfig, int displayId, boolean alwaysReportChange) {
。。省略
final Configuration reportedConfig = performConfigurationChangedForActivity(r,
mConfigurationController.getCompatConfiguration(),
movedToDifferentDisplay ? displayId : r.activity.getDisplayId(),
alwaysReportChange);
// Notify the ViewRootImpl instance about configuration changes. It may have initiated this
// update to make sure that resources are updated before updating itself.
if (viewRoot != null) {
if (movedToDifferentDisplay) {
viewRoot.onMovedToDisplay(displayId, reportedConfig);
}
viewRoot.updateConfiguration(displayId);
}
mSomeActivitiesChanged = true;
}
mConfigurationController.getCompatConfiguration() 就是我们后面拿到的newConfig
也就是说这个config其实不是系统直接发来的其实是APP进程内获取的,那么为什么APP进程为什么获取的错的?mConfigurationController.getCompatConfiguration()是什么时候更新的?为什么会偶尔拿错呢?到底是因为没回调还是时序错误呢?
我先说结论,这是个时序问题
这个地方Android14一共有3个回调,我给出的顺序也是正常应该回调的顺序
1. ConfigurationChangeItem::handleConfigurationChanged 更新 APP进程内cofig 就是这个方法更新完后去getCompatConfiguration 取的数据
@Override
public void handleConfigurationChanged(Configuration config, int deviceId) {
mConfigurationController.handleConfigurationChanged(config);
}
2. ActivityConfigurationChangeItem::handleActivityConfigurationChanged
@Override
public void handleActivityConfigurationChanged(ActivityClientRecord r,
@NonNull Configuration overrideConfig, int displayId) {
handleActivityConfigurationChanged(r, overrideConfig, displayId,
// This is the only place that uses alwaysReportChange=true. The entry point should
// be from ActivityConfigurationChangeItem or MoveToDisplayItem, so the server side
// has confirmed the activity should handle the configuration instead of relaunch.
// If Activity#onConfigurationChanged is called unexpectedly, then we can know it is
// something wrong from server side.
true /* alwaysReportChange */);
}
3. ViewRootImpl::onConfigurationChanged 是解除冻屏后进行resize传来的,这个一会说
private void init() {
parent = null;
embeddedID = null;
paused = false;
stopped = false;
hideForNow = false;
activityConfigCallback = new ViewRootImpl.ActivityConfigCallback() {
@Override
public void onConfigurationChanged(Configuration overrideConfig,
int newDisplayId) {
if (activity == null) {
throw new IllegalSttateException(
"Received config update for non-existing activity");
}
//我是在这个地方做了判断,没有修改就不回调了,有更好的办法可以讲下
activity.mMainThread.handleActivityConfigurationChanged(
ActivityClientRecord.this, overrideConfig, newDisplayId,
false /* alwaysReportChange */);
}
这三个回调按照这个顺序是不会错的,但是,在非主屏的情况下,可能会先收到ViewRootImpl::onConfigurationChanged然后剩下两个回调 这个时候就会出现我们说的当 ViewRootImpl::onConfigurationChanged回调时候APP进程内的config还没更信导致昼夜色取到的值是一样的
那问题来了
ActivityConfigurationChangeItem::handleActivityConfigurationChanged 回调时候为什么uimode不一样了还不进行回调?
void handleActivityConfigurationChanged(ActivityClientRecord r,
@NonNull Configuration overrideConfig, int displayId, boolean alwaysReportChange) {
synchronized (mPendingOverrideConfigs) {
final Configuration pendingOverrideConfig = mPendingOverrideConfigs.get(r.token);
if (overrideConfig.isOtherSeqNewer(pendingOverrideConfig)) {
if (DEBUG_CONFIGURATION) {
Slog.v(TAG, "Activity has newer configuration pending so drop this"
+ " transaction. overrideConfig=" + overrideConfig
+ " pendingOverrideConfig=" + pendingOverrideConfig);
}
return;
}
mPendingOverrideConfigs.remove(r.token);
}
if (displayId == INVALID_DISPLAY) {
// If INVALID_DISPLAY is passed assume that the activity should keep its current
// display.
displayId = r.activity.getDisplayId();
}
final boolean movedToDifferentDisplay = isDifferentDisplay(
r.activity.getDisplayId(), displayId);
if (r.overrideConfig != null && !r.overrideConfig.isOtherSeqNewer(overrideConfig)
&& !movedToDifferentDisplay) {
if (DEBUG_CONFIGURATION) {
Slog.v(TAG, "Activity already handled newer configuration so drop this"
+ " transaction. overrideConfig=" + overrideConfig
+ r.overrideConfig);
}
return;
}
// Perform updates.
r.overrideConfig = overrideConfig;
这地方更新了config的版本号
r.overrideConfig != null && !r.overrideConfig.isOtherSeqNewer(overrideConfig)
&& !movedToDifferentDisplay 这个isOtherSeqNewer 的判断阻拦了我们后续的回调
isOtherSeqNewer判断的是什么?是seq也就是版本号,因为ViewRootImpl::onConfigurationChanged 已经把我们config版本号给更新了,导致后续的回调都被屏蔽了
可能有人注意到 ConfigurationChangeItem::handleConfigurationChanged 里面 走到 handleConfigurationChanged 会更新APP的数据还分发过数据但是为什么收不到,我们来看代码
void handleConfigurationChanged(@Nullable Configuration config,
@Nullable CompatibilityInfo compat) {
int configDiff;
boolean equivalent;
。。。省略直接看分发
这步就是后续能get到更新的值 进行修改的地方
config = applyCompatConfiguration();
这个地方好像能进行分发,但是为什么收不到?
final ArrayList<ComponentCallbacks2> callbacks =
mActivityThread.collectComponentCallbacks(false /* includeUiContexts */);
freeTextLayoutCachesIfNeeded(configDiff);
if (callbacks != null) {
final int size = callbacks.size();
for (int i = 0; i < size; i++) {
ComponentCallbacks2 cb = callbacks.get(i);
if (!equivalent) {
performConfigurationChanged(cb, config);
}
}
}
ActivityThread::collectComponentCallbacks 这个方法获取到的并不是所有的Activity
@Override
public ArrayList<ComponentCallbacks2> collectComponentCallbacks(boolean includeUiContexts) {
ArrayList<ComponentCallbacks2> callbacks
= new ArrayList<ComponentCallbacks2>();
ArrayList<ComponentCallbacks2> callbacks
= new ArrayList<ComponentCallbacks2>();
synchronized (mResourcesManager) {
final int NAPP = mAllApplications.size();
for (int i=0; i<NAPP; i++) {
callbacks.add(mAllApplications.get(i));
}
这地方includeUiContexts 是个false 所以Activity根本加不进来
if (includeUiContexts) {
for (int i = mActivities.size() - 1; i >= 0; i--) {
final Activity a = mActivities.valueAt(i).activity;
if (a != null && !a.mFinished) {
callbacks.add(a);
}
}
}
final int NSVC = mServices.size();
for (int i=0; i<NSVC; i++) {
final Service service = mServices.valueAt(i);
// If {@code includeUiContext} is set to false, WindowProviderService should not be
// collected because WindowProviderService is a UI Context.
if (includeUiContext} is set to false, WindowProviderService should not be
// collected because WindowProviderService is a UI Context.
if (includeUiContexts || !(service instanceof WindowProviderService)) {
callbacks.add(service);
}
}
}
synchronized (mProviderMap) {
final int NPRV = mLocalProviders.size();
for (int i=0; i<NPRV; i++) {
callbacks.add(mLocalProviders.valueAt(i).mLocalProvider);
}
}
return callbacks;
}
所以根本分发不到Activity,但是Application能接到,就是有点快
屏蔽了所以副屏的APP都无法回调了,那问题来了,到底是系统层的回调错了还是为什么会导致回调时序错误?
还是先说结论系统层分发来的时候时序是没问题的上代码看一下
首先先看昼夜色切换触发的方法
首先是UIModeManager::setNightModeActivated
@RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
public boolean setNightModeActivated(boolean active) {
if (mService != null) {
try {
return mService.setNightModeActivated(active);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return false;
}
mService其实就是 UiModeManagerService
然后走到 UiModeManagerService
@Override
public boolean setNightModeActivated(boolean active) {
return setNightModeActivatedForModeInternal(mNightModeCustomType, active);
}
private boolean setNightModeActivatedForModeInternal(int modeCustomType, boolean active) {
if (getContext().checkCallingOrSelfPermission(
android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
!= PackageManager.PERMISSION_GRANTED) {
Slog.e(TAG, "Night mode locked, requires MODIFY_DAY_NIGHT_MODE permission");
return false;
}
final int user = Binder.getCallingUserHandle().getIdentifier();
if (user != mCurrentUser && getContext().checkCallingOrSelfPermission(
android.Manifest.permission.INTERACT_ACROSS_USERS)
!= PackageManager.PERMISSION_GRANTED) {
Slog.e(TAG, "Target user is not current user,"
+ " INTERACT_ACROSS_USERS permission is required");
return false;
} // Store the last requested bedtime night mode state so that we don't need to notify
// anyone if the user decides to switch to the night mode to bedtime.
if (modeCustomType == MODE_NIGHT_CUSTOM_TYPE_BEDTIME) {
mLastBedtimeRequestedNightMode = active;
}
if (modeCustomType != mNightModeCustomType) {
return false;
}
synchronized (mLock) {
final long ident = Binder.clearCallingIdentity();
try {
if (mNightMode == MODE_NIGHT_AUTO || mNightMode == MODE_NIGHT_CUSTOM) {
unregisterScreenOffEventLocked();
mOverrideNightModeOff = !active;
mOverrideNightModeOn = active;
mOverrideNightModeUser = mCurrentUser;
persistNightModeOverrides(mCurrrentUser);
} else if (mNightMode == UiModeManager.MODE_NIGHT_NO
&& active) {
mNightMode = UiModeManager.MODE_NIGHT_YES;
} else if (mNightMode == UiModeManager.MODE_NIGHT_YES
&& !active) {
mNightMode = UiModeManager.MODE_NIGHT_NO;
}
updateConfigurationLocked();
applyConfigurationExternallyLocked();
persistNightMode(mCurrentUser);
return true;
} finally {
Binder.restoreCallingIdentity(ident);
}
}
}
注意一下 applyConfigurationExternallyLocked();
方法
重点关注 applyConfigurationExternallyLocked();方法
private void applyConfigurationExternallyLocked() {
if (mSetUiMode != mConfiguration.uiMode) {
mSetUiMode = mConfiguration.uiMode;
// load splash screen instead of screenshot
mWindowManager.clearSnapshotCache();
try {
ActivityTaskManager.getService().updateConfiguration(mConfiguration);
} catch (RemoteException e) {
Slog.w(TAG, "Failure communicating with activity manager", e);
}
}
}
很明显,在这走到了ATMS的方法里
@Override
public boolean updateConfiguration(Configuration values) {
。。。
updateConfigurationLocked(values, null, false, false /* persistent */,
UserHandle.USER_NULL, false /* deferResume */,
mTmpUpdateConfigurationResult);
。。。
}
boolean updateConfigurationLocked(Configuration values, ActivityRecord starting,
boolean initLocale, boolean persistent, int userId, boolean deferResume,
ActivityTaskManagerService.UpdateConfigurationResult result) {
int changes = 0;
boolean kept = true;
首先先冻屏
deferWindowLayout();
try {
if (values != null) {
1然后更新所有的config 给所有的config新的赋值
changes = updateGlobalConfigurationLocked(values, initLocale, persistent, userId);
}
if (!deferResume) {
2更新所有的Activity把Activity数据更新 真正的分发
kept = ensureConfigAndVisibilityAfterUpdate(starting, changes);
}
} finally {
3最后解除冻屏
continueWindowLayout();
}
if (result != null) {
result.changes = changes;
result.activityRelaunched = !kept;
}
return kept;
}
updateConfigurationLocked 这个方法很重要,可以看到这三个方法不存在顺序出错的可能性
我们可以看到上面的三个APP层回调也是这三个方法分别回调
因为我们已经知道系统中三个回调顺序不可能出错,但是冻屏解除的resize居然先于上面两个方法到来,个人猜测是因为解除冻屏的AIDL回调跟 updateGlobalConfigurationLocked 和 ensureConfigAndVisibilityAfterUpdate两个方法回调的AIDL不是一个导致的
updateGlobalConfigurationLocked 和 ensureConfigAndVisibilityAfterUpdate两个方法回调都到了Application里,但是 continueWindowLayout走的是ViewRootImpl
还有一个问题,正常AIDL也是同步的,博主看了下系统到APP的代码,AIDL都带了ONEWAY标签,所以没法控制时序
所以看到这里,结论就是原生的调用链其实顺序是没问题的,问题是到达APP的顺序出错,所以此题有两个解法
1. 修改版本号
2. 检查是否改变,无改变不回调(需要验证)
那我们来看看版本号在哪改变的让我们看方法1
int updateGlobalConfigurationLocked(@NonNull Configuration values, boolean initLocale,
boolean persistent, int userId) {
mTempConfig.setTo(getGlobalConfiguration());
final int changes = mTempConfig.updateFrom(values);
if (changes == 0) {
return 0;
}
。。。
mTempConfig.seq = increaseConfigurationSeqLocked();
。。。
}
int increaseConfigurationSeqLocked() {
mConfigurationSeq = Math.max(++mConfigurationSeq, 1);
return mConfigurationSeq;
}
所以是这个地方进行了改变,这个版本号接下来回调也使用