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资源应用与适配标准

相关推荐
韩楚风1 小时前
【linux 多进程并发】linux进程状态与生命周期各阶段转换,进程状态查看分析,助力高性能优化
linux·服务器·性能优化·架构·gnu
网络研究院1 小时前
Android 安卓内存安全漏洞数量大幅下降的原因
android·安全·编程·安卓·内存·漏洞·技术
凉亭下1 小时前
android navigation 用法详细使用
android
小比卡丘4 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭5 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android
_.Switch6 小时前
Python机器学习:自然语言处理、计算机视觉与强化学习
python·机器学习·计算机视觉·自然语言处理·架构·tensorflow·scikit-learn
落落落sss6 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.7 小时前
数据库语句优化
android·数据库·adb
GEEKVIP9 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
feng_xiaoshi10 小时前
【云原生】云原生架构的反模式
云原生·架构