当前位置: 首页 > article >正文

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;
    }

所以是这个地方进行了改变,这个版本号接下来回调也使用


http://www.kler.cn/a/584035.html

相关文章:

  • 双指针算法介绍+算法练习(2025)
  • Anaconda 以及 Jupyter Notebook的详细安装教程
  • 独立IP服务器的好处都有哪些?
  • Android头像布局
  • Node.js 模块化概念详细介绍
  • 【微知】tmux如何在一个会话的1个窗口中水平分割或者垂直分割窗口?(垂直 Ctrl + b, %; 切换Ctrl + b, 方向键; ctrl d关闭)
  • 当AI回答问题时,它的“大脑”里在炒什么菜?
  • PrivHunterAI越权漏洞检测工具详细使用教程
  • 从零开始学习机器人---如何高效学习机械原理
  • pycharm + anaconda + yolo11(ultralytics) 的视频流实时检测,保存推流简单实现
  • 程序化广告行业(11/89):洗牌期与成熟期的变革及行业生态解析
  • 深度学习-145-Text2SQL之基于官方提示词模板进行交互
  • 三分钟掌握音视频信息查询 | 在 Rust 中优雅地集成 FFmpeg
  • 平时作业
  • C语言一维数组
  • 使用 ESP32 和 Python 进行手势识别
  • 【redis】list类型:基本命令(下)
  • C++复试笔记(三)
  • uniapp 实现的步进指示器组件
  • C++设计模式-原型模式:从基本介绍,内部原理、应用场景、使用方法,常见问题和解决方案进行深度解析