Android Configuration Changed

基于Android R版本分析

Configuration 应用开发背景

Configuration类专门描述手机设备上的配置信息,这些配置信息既包括用户特定的配置项,也包括系统的动态设备配置。

通过调用Activity的getResource().getConfiguration()方法获得Configuration对象,然后就可以使用下面常用的属性来获取系统的配置信息;

java 复制代码
// 颜色模式
public int colorMode;
// 像素密度
public int densityDpi;
// 字体缩放系数
public float fontScale;
// 硬键盘状态
public int hardKeyboardHidden;
// 键盘类型
public int keyboard;
// 键盘状态
public int keyboardHidden;
// 地区语言
@Deprecated public java.util.Locale locale;
// 移动国家代码
public int mcc;
// 移动网络代码
public int mnc;
// 导航条类型
public int navigation;
// 导航条隐藏
public int navigationHidden;
// 屏幕方向(旋转角度)
public int orientation;
// 屏幕高度像素
public int screenHeightDp;
// 屏幕布局
public int screenLayout;
// 屏幕宽度像素
public int screenWidthDp;
// 物理屏幕宽度像素
public int smallestScreenWidthDp;
// 触摸屏状态
public int touchscreen;
// 用户界面模式
public int uiMode;

当系统的一些配置属性发生了变化,就会导致系统当前的TopActivity会进行destory后进行重新create;

如果不想要重建Activity,那么我们就需要到AndroidManifest中为指定的Activity声明对应的configChange,这个 时候就会让Activity不重建,Activity就会执行对应回调onConfigurationChanged,应用进程需要根据onConfigurationChanged的回调信息,进行界面的重构;

ini 复制代码
<activity
          android:name=".XxxActivity"
           android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|uiMode|layoutDirection|colorMode|fontScale|density|screenSize|smallestScreenSize|screenLayout|orientation"
          android:resizeableActivity="true" />
less 复制代码
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
}

colorMode

WIDE_COLOR_GAMU:广色域模式,在使用该色域的时候,需要通过isWideColorGamut()方法来判断当前设备是否支持广色域;

| name | value | desc |
|---------------------------------------|---------------------------------------|------------------------------|--------|
| COLOR_MODE_WIDE_COLOR_GAMUT_MASK | 0x3 | 广色域位掩码,掩码在代码逻辑中进行位掩码实现所需要的功能 |
| COLOR_MODE_WIDE_COLOR_GAMUT_UNDEFINED | 0x0 | screen未知是否属于广色域模式 |
| COLOR_MODE_WIDE_COLOR_GAMUT_NO | 0x1 | 非广色域模式 |
| COLOR_MODE_WIDE_COLOR_GAMUT_YES | 0x2 | 广色域模式 |
| COLOR_MODE_HDR_MASK | 0xc | HDR位掩码 |
| COLOR_MODE_HDR_SHIFT | 2 | 位移动以获取屏幕动态范围 |
| COLOR_MODE_HDR_UNDEFINED | 0x0 | screen未知是否属于HDR |
| COLOR_MODE_HDR_NO | 0x1 << COLOR_MODE_HDR_SHIFT | screen不属于HDR |
| COLOR_MODE_HDR_YES | 0x2 << COLOR_MODE_HDR_SHIFT | screen属于HDR(动态范围) |
| COLOR_MODE_UNDEFINED | COLOR_MODE_WIDE_COLOR_GAMUT_UNDEFINED | COLOR_MODE_HDR_UNDEFINED | 颜色模式未定 |

screenLayout

screenLayout其实是承载着四个配置的:

  • 屏幕大小等级:有SCREENLAYOUT_SIZE_SMALLSCREENLAYOUT_SIZE_NORMALSCREENLAYOUT_SIZE_LARGESCREENLAYOUT_SIZE_XLARGE四种;
  • 是否宽屏:屏幕是否比普通屏幕更宽或更高;
  • 屏幕方向:屏幕是从左向右显示,还是从有向左显示;
  • 是否是圆角屏:屏幕是否有圆角

| name | value | desc |
|----------------------------------|----------------------------------------|-----------------------------|----------------------------------|------------------------------|----|
| SCREENLAYOUT_SIZE_MASK | 0x0f | SIZE_MASK(屏幕大小) |
| SCREENLAYOUT_SIZE_UNDEFINED | 0x00 | 未知大小 |
| SCREENLAYOUT_SIZE_SMALL | 0x01 | 小(屏幕尺寸小于3英寸左右的布局) |
| SCREENLAYOUT_SIZE_NORMAL | 0x02 | 正常(屏幕尺寸小于4.5英寸左右) |
| SCREENLAYOUT_SIZE_LARGE | 0x03 | 大(4英寸-7英寸之间) |
| SCREENLAYOUT_SIZE_XLARGE | 0x04 | 超大(7-10英寸之间) |
| SCREENLAYOUT_LONG_MASK | 0x30 | LONG_MASK(屏幕纵横比) |
| SCREENLAYOUT_LONG_UNDEFINED | 0x00 | 未知宽屏 |
| SCREENLAYOUT_LONG_NO | 0x10 | 非宽屏 |
| SCREENLAYOUT_LONG_YES | 0x20 | 宽屏 |
| SCREENLAYOUT_LAYOUTDIR_MASK | 0xC0 | LAYOUTDIR_MASK(屏幕方向) |
| SCREENLAYOUT_LAYOUTDIR_SHIFT | 6 | |
| SCREENLAYOUT_LAYOUTDIR_UNDEFINED | 0x00 | 未知 |
| SCREENLAYOUT_LAYOUTDIR_LTR | 0x01 << SCREENLAYOUT_LAYOUTDIR_SHIFT | 屏幕是从左向右显示 |
| SCREENLAYOUT_LAYOUTDIR_RTL | 0x02 << SCREENLAYOUT_LAYOUTDIR_SHIFT | 屏幕是从右向左显示 |
| SCREENLAYOUT_ROUND_MASK | 0x300 | ROUND_MASK(屏幕圆角屏) |
| SCREENLAYOUT_ROUND_SHIFT | 8 | |
| SCREENLAYOUT_ROUND_UNDEFINED | 0x00 | 未知 |
| SCREENLAYOUT_ROUND_NO | 0x1 << SCREENLAYOUT_ROUND_SHIFT | 非圆角屏 |
| SCREENLAYOUT_ROUND_YES | 0x2 << SCREENLAYOUT_ROUND_SHIFT | 圆角屏 |
| SCREENLAYOUT_UNDEFINED | SCREENLAYOUT_SIZE_UNDEFINED | SCREENLAYOUT_LONG_UNDEFINED | SCREENLAYOUT_LAYOUTDIR_UNDEFINED | SCREENLAYOUT_ROUND_UNDEFINED | 未知 |
| SCREENLAYOUT_COMPAT_NEEDED | 0x10000000 | |

touchscreen

name value desc
TOUCHSCREEN_UNDEFINED 0 未知
TOUCHSCREEN_NOTOUCH 1 非触摸
TOUCHSCREEN_STYLUS 2 手写笔模式
TOUCHSCREEN_FINGER 3 手指,支持触摸

keyboard

name value desc
KEYBOARD_UNDEFINED 0 未知
KEYBOARD_NOKEYS 1 设备没有用于文本输入的硬按键
KEYBOARD_QWERTY 2 设备具有标准硬键盘(全键)
KEYBOARD_12KEY 3 设备具有 12 键硬键盘

keyboardHidden

name value desc
KEYBOARDHIDDEN_UNDEFINED 0 未知
KEYBOARDHIDDEN_NO 1 设备具有可用的键盘
KEYBOARDHIDDEN_YES 2 设备具有可用的键盘,但它处于隐藏状态,且设备没有启用软键盘
KEYBOARDHIDDEN_SOFT 3 设备已经启用软键盘

hardKeyboardHidden

name value desc
HARDKEYBOARDHIDDEN_UNDEFINED 0 未知
HARDKEYBOARDHIDDEN_NO 1 设备具有可用的硬键盘
HARDKEYBOARDHIDDEN_YES 2 设备具有可用的硬键盘,但它处于隐藏状态
name value desc
NAVIGATION_UNDEFINED 0 未知
NAVIGATION_NONAV 1 除了使用触摸屏以外,设备没有其他导航设施
NAVIGATION_DPAD 2 设备具有用于导航的方向键
NAVIGATION_TRACKBALL 3 设备具有用于导航的轨迹球
NAVIGATION_WHEEL 4 设备具有用于导航的方向盘
name value desc
NAVIGATIONHIDDEN_UNDEFINED 0 未知
NAVIGATIONHIDDEN_NO 1 导航键可供用户使用
NAVIGATIONHIDDEN_YES 2 导航键不可用

Orientation

name value desc
ORIENTATION_UNDEFINED 0 未知
ORIENTATION_PORTRAIT 1 竖屏方向,屏幕宽度小于高度
ORIENTATION_LANDSCAPE 2 横屏方向,屏幕宽度大于高度
ORIENTATION_SQUARE 3 正方形屏幕,认为屏幕宽度等于高度

uiMode

name value desc
UI_MODE_TYPE_MASK 0x0f 定义了设备的整个UI模式,它支持如下取值
UI_MODE_TYPE_UNDEFINED 0x00 未知
UI_MODE_TYPE_NORMAL 0x01 通常模式
UI_MODE_TYPE_DESK 0x02 带底座模式
UI_MODE_TYPE_CAR 0x03 车载模式
UI_MODE_TYPE_TELEVISION 0x04 电视模式
UI_MODE_TYPE_APPLIANCE 0x05 设备模式(无显示器)
UI_MODE_TYPE_WATCH 0x06 手表模式
UI_MODE_TYPE_VR_HEADSET 0x07
UI_MODE_NIGHT_MASK 0x30 定义了屏幕是否在一个特殊模式中
UI_MODE_NIGHT_UNDEFINED 0x00 未知
UI_MODE_NIGHT_NO 0x10 白天模式
UI_MODE_NIGHT_YES 0x20 夜间模式

densityDpi

name value desc
DENSITY_DPI_UNDEFINED 0
DENSITY_DPI_ANY 0xfffe
DENSITY_DPI_NONE 0xffff

windowConfiguration

windowConfiguration desc
mBounds bounds信息,包括insets
mAppBounds App窗口信息,不包括insets
mWindowingMode 窗口模式
mDisplayWindowMode Display的窗口模式
mActivityType Activity类型
mAlwaysOnTop 是否要一致处于顶部显示的标志

ActivityThread

我们知道了在应用进程中如何配置config属性以及如何及时获取config change信息。我们需要了解onConfigurationChanged过程,我们以move stack场景进行分析;

php 复制代码
08-04 09:29:19.594  1881  1881 W System.err: java.lang.Exception: Stack trace
08-04 09:29:19.594  1881  1881 W System.err:    at java.lang.Thread.dumpStack(Thread.java:1529)
08-04 09:29:19.594  1881  1881 W System.err:    at com.example.android.pictureinpicture.MainActivity.onConfigurationChanged(MainActivity.java:419)
08-04 09:29:19.594  1881  1881 W System.err:    at android.app.ActivityThread.performActivityConfigurationChanged(ActivityThread.java:5707)
08-04 09:29:19.594  1881  1881 W System.err:    at android.app.ActivityThread.performConfigurationChangedForActivity(ActivityThread.java:5574)
08-04 09:29:19.594  1881  1881 W System.err:    at android.app.ActivityThread.handleActivityConfigurationChanged(ActivityThread.java:6035)
08-04 09:29:19.595  1881  1881 W System.err:    at android.app.servertransaction.MoveToDisplayItem.execute(MoveToDisplayItem.java:50)
08-04 09:29:19.595  1881  1881 W System.err:    at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
08-04 09:29:19.595  1881  1881 W System.err:    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
08-04 09:29:19.595  1881  1881 W System.err:    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2068)
08-04 09:29:19.595  1881  1881 W System.err:    at android.os.Handler.dispatchMessage(Handler.java:106)
08-04 09:29:19.595  1881  1881 W System.err:    at android.os.Looper.loop(Looper.java:223)
08-04 09:29:19.595  1881  1881 W System.err:    at android.app.ActivityThread.main(ActivityThread.java:7666)
08-04 09:29:19.595  1881  1881 W System.err:    at java.lang.reflect.Method.invoke(Native Method)
08-04 09:29:19.595  1881  1881 W System.err:    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
08-04 09:29:19.595  1881  1881 W System.err:    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

我们可以知道,Handler的消息是由MoveToDisplayItem进行处理的,那我们需要知道MoveToDisplayItem是被哪一块逻辑触发,通过分析代码可知,该触发逻辑定义在ActivityRecord中:

arduino 复制代码
private void scheduleActivityMovedToDisplay(int displayId, Configuration config) {
    if (!attachedToProcess()) {
        if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.w(TAG,
                "Can't report activity moved to display - client not running, activityRecord="
                        + this + ", displayId=" + displayId);
        return;
    }
    try {
        if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG,
                "Reporting activity moved to display" + ", activityRecord=" + this
                        + ", displayId=" + displayId + ", config=" + config);
​
        mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
                MoveToDisplayItem.obtain(displayId, config));
    } catch (RemoteException e) {
        // If process died, whatever.
    }
}

scheduleActivityMovedToDisplay()方法则是在ensureActivityConfiguration()方法中被调用;

我们从move stack的根源开始分析;

move stack

这一块逻辑分为两大块:

  • stack reparent:变更正在move的stack的parent节点;

  • stack resume:更新所有DisplayContent Task堆栈的状态;

    • 更新、获取所有DisplayContent的本应持有焦点的ActivityStack;
    • 恢复Task堆栈的next ActivityStack的状态,可以简单的理解为ActivityStack对应的TopActivity的生命周期;
    • 更新TopActivity的visible属性值;

其中最核心的逻辑对应了resumeFocusedStacksTopActivities()方法;

在该方法中会针对TopActivity的状态进行更新,因为主屏TopActivity move到副屏,即该move的Activity还是为TopActivity,所以上述逻辑会覆盖到当前move的Activity。而在该逻辑中,会确认Activity的config信息状态;

ensureActivityConfiguration

scss 复制代码
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.
    final Configuration currentConfig = getConfiguration();
    int changes = lastReportedConfig.diff(currentConfig);
    // We don't want to use size changes if they don't cross boundaries that are important to
    // the app.
    if ((changes & CONFIG_SCREEN_SIZE) != 0) {
        final boolean crosses = crossesHorizontalSizeThreshold(lastReportedConfig.screenWidthDp,
                                                               currentConfig.screenWidthDp)
            || crossesVerticalSizeThreshold(lastReportedConfig.screenHeightDp,
                                            currentConfig.screenHeightDp);
        if (!crosses) {
            changes &= ~CONFIG_SCREEN_SIZE;
        }
    }
    if ((changes & CONFIG_SMALLEST_SCREEN_SIZE) != 0) {
        final int oldSmallest = lastReportedConfig.smallestScreenWidthDp;
        final int newSmallest = currentConfig.smallestScreenWidthDp;
        if (!crossesSmallestSizeThreshold(oldSmallest, newSmallest)) {
            changes &= ~CONFIG_SMALLEST_SCREEN_SIZE;
        }
    }
    // We don't want window configuration to cause relaunches.
    if ((changes & CONFIG_WINDOW_CONFIGURATION) != 0) {
        changes &= ~CONFIG_WINDOW_CONFIGURATION;
    }
​
    return changes;
}

这个逻辑相对比较简单,判断了两个条件:

  • displayChanged:判断Activity的changed是否涉及DisplayId的变化;
  • shouldRelaunchLocked:判断Activity的changed集合信息是否和AndroidManifest中的config changes属性配置是否匹配;

针对move stack在车机上的环境,上述的两个条件value:

  • displayChanged = true:代表了Activity的change涉及到了displayId的变化;
  • shouldRelaunchLocked = true / false :这个是不固定的,需要看move的Activity是否进行的configChangs的属性配置,且对应的配置项是否齐全(我们默认配置是齐全的);

在上述的场景下,会调用到scheduleActivityMovedToDisplay()方法;

scheduleActivityMovedToDisplay

这个过程就比较简单了,基本上属于层层的透传调用,会将Configuration的变化情况通过onConfigurationChanged()的方式通知到应用进程中,供应用进程进行相对应的调整;

参考

Android清单文件详解

Android资源应用与适配标准

相关推荐
全栈若城25 分钟前
HarmonyOS 6 实战:Component3D 与 SURFACE 渲染模式深度解析
3d·架构·harmonyos6
全栈若城27 分钟前
HarmonyOS 6 实战:使用 ArkGraphics3D 加载 GLB 模型与 Scene 初始化全流程
3d·华为·架构·harmonyos·harmonyos6
小羊子说6 小时前
Android系统中 socketpair 的源码解读与应用分析小结
android·java
FL4m3Y4n7 小时前
MySQL索引原理与SQL优化
android·sql·mysql
瀚高PG实验室7 小时前
同架构大数据量HGDB到HGDB数据迁移
架构·瀚高数据库
唐骁虎7 小时前
Claude Code 全景架构指南——三大核心支柱及四大关键扩展组件
ai·架构·ai编程·claude code
启山智软7 小时前
【启山智软智能商城系统技术架构剖析】
java·前端·架构
学不完的7 小时前
ZrLog 高可用架构监控部署指南(Prometheus + Grafana)
linux·运维·架构·负载均衡·grafana·prometheus·ab测试
我命由我123457 小时前
Android Gradle - Gradle 自定义插件(Build Script 自定义插件、buildSrc 自定义插件、独立项目自定义插件)
android·java·java-ee·kotlin·android studio·android-studio·android runtime