基于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_SMALL
、SCREENLAYOUT_SIZE_NORMAL
、SCREENLAYOUT_SIZE_LARGE
和SCREENLAYOUT_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 | 设备具有可用的硬键盘,但它处于隐藏状态 |
navigation
name | value | desc |
---|---|---|
NAVIGATION_UNDEFINED | 0 | 未知 |
NAVIGATION_NONAV | 1 | 除了使用触摸屏以外,设备没有其他导航设施 |
NAVIGATION_DPAD | 2 | 设备具有用于导航的方向键 |
NAVIGATION_TRACKBALL | 3 | 设备具有用于导航的轨迹球 |
NAVIGATION_WHEEL | 4 | 设备具有用于导航的方向盘 |
navigationHidden
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()的方式通知到应用进程中,供应用进程进行相对应的调整;