Android Configuration Changed

1,728 阅读8分钟

基于Android R版本分析

Configuration 应用开发背景

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

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

// 颜色模式
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的回调信息,进行界面的重构;

<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" />
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
}

colorMode

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

namevaluedesc
COLOR_MODE_WIDE_COLOR_GAMUT_MASK0x3广色域位掩码,掩码在代码逻辑中进行位掩码实现所需要的功能
COLOR_MODE_WIDE_COLOR_GAMUT_UNDEFINED0x0screen未知是否属于广色域模式
COLOR_MODE_WIDE_COLOR_GAMUT_NO0x1非广色域模式
COLOR_MODE_WIDE_COLOR_GAMUT_YES0x2广色域模式
COLOR_MODE_HDR_MASK0xcHDR位掩码
COLOR_MODE_HDR_SHIFT2位移动以获取屏幕动态范围
COLOR_MODE_HDR_UNDEFINED0x0screen未知是否属于HDR
COLOR_MODE_HDR_NO0x1 << COLOR_MODE_HDR_SHIFTscreen不属于HDR
COLOR_MODE_HDR_YES0x2 << COLOR_MODE_HDR_SHIFTscreen属于HDR(动态范围)
COLOR_MODE_UNDEFINEDCOLOR_MODE_WIDE_COLOR_GAMUT_UNDEFINEDCOLOR_MODE_HDR_UNDEFINED颜色模式未定

screenLayout

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

  • 屏幕大小等级:有SCREENLAYOUT_SIZE_SMALLSCREENLAYOUT_SIZE_NORMALSCREENLAYOUT_SIZE_LARGESCREENLAYOUT_SIZE_XLARGE四种;
  • 是否宽屏:屏幕是否比普通屏幕更宽或更高;
  • 屏幕方向:屏幕是从左向右显示,还是从有向左显示;
  • 是否是圆角屏:屏幕是否有圆角
namevaluedesc
SCREENLAYOUT_SIZE_MASK0x0fSIZE_MASK(屏幕大小)
SCREENLAYOUT_SIZE_UNDEFINED0x00未知大小
SCREENLAYOUT_SIZE_SMALL0x01小(屏幕尺寸小于3英寸左右的布局)
SCREENLAYOUT_SIZE_NORMAL0x02正常(屏幕尺寸小于4.5英寸左右)
SCREENLAYOUT_SIZE_LARGE0x03大(4英寸-7英寸之间)
SCREENLAYOUT_SIZE_XLARGE0x04超大(7-10英寸之间)
SCREENLAYOUT_LONG_MASK0x30LONG_MASK(屏幕纵横比)
SCREENLAYOUT_LONG_UNDEFINED0x00未知宽屏
SCREENLAYOUT_LONG_NO0x10非宽屏
SCREENLAYOUT_LONG_YES0x20宽屏
SCREENLAYOUT_LAYOUTDIR_MASK0xC0LAYOUTDIR_MASK(屏幕方向)
SCREENLAYOUT_LAYOUTDIR_SHIFT6
SCREENLAYOUT_LAYOUTDIR_UNDEFINED0x00未知
SCREENLAYOUT_LAYOUTDIR_LTR0x01 << SCREENLAYOUT_LAYOUTDIR_SHIFT屏幕是从左向右显示
SCREENLAYOUT_LAYOUTDIR_RTL0x02 << SCREENLAYOUT_LAYOUTDIR_SHIFT屏幕是从右向左显示
SCREENLAYOUT_ROUND_MASK0x300ROUND_MASK(屏幕圆角屏)
SCREENLAYOUT_ROUND_SHIFT8
SCREENLAYOUT_ROUND_UNDEFINED0x00未知
SCREENLAYOUT_ROUND_NO0x1 << SCREENLAYOUT_ROUND_SHIFT非圆角屏
SCREENLAYOUT_ROUND_YES0x2 << SCREENLAYOUT_ROUND_SHIFT圆角屏
SCREENLAYOUT_UNDEFINEDSCREENLAYOUT_SIZE_UNDEFINEDSCREENLAYOUT_LONG_UNDEFINEDSCREENLAYOUT_LAYOUTDIR_UNDEFINEDSCREENLAYOUT_ROUND_UNDEFINED未知
SCREENLAYOUT_COMPAT_NEEDED0x10000000

touchscreen

namevaluedesc
TOUCHSCREEN_UNDEFINED0未知
TOUCHSCREEN_NOTOUCH1非触摸
TOUCHSCREEN_STYLUS2手写笔模式
TOUCHSCREEN_FINGER3手指,支持触摸

keyboard

namevaluedesc
KEYBOARD_UNDEFINED0未知
KEYBOARD_NOKEYS1设备没有用于文本输入的硬按键
KEYBOARD_QWERTY2设备具有标准硬键盘(全键)
KEYBOARD_12KEY3设备具有 12 键硬键盘

keyboardHidden

namevaluedesc
KEYBOARDHIDDEN_UNDEFINED0未知
KEYBOARDHIDDEN_NO1设备具有可用的键盘
KEYBOARDHIDDEN_YES2设备具有可用的键盘,但它处于隐藏状态,且设备没有启用软键盘
KEYBOARDHIDDEN_SOFT3设备已经启用软键盘

hardKeyboardHidden

namevaluedesc
HARDKEYBOARDHIDDEN_UNDEFINED0未知
HARDKEYBOARDHIDDEN_NO1设备具有可用的硬键盘
HARDKEYBOARDHIDDEN_YES2设备具有可用的硬键盘,但它处于隐藏状态

navigation

namevaluedesc
NAVIGATION_UNDEFINED0未知
NAVIGATION_NONAV1除了使用触摸屏以外,设备没有其他导航设施
NAVIGATION_DPAD2设备具有用于导航的方向键
NAVIGATION_TRACKBALL3设备具有用于导航的轨迹球
NAVIGATION_WHEEL4设备具有用于导航的方向盘

navigationHidden

namevaluedesc
NAVIGATIONHIDDEN_UNDEFINED0未知
NAVIGATIONHIDDEN_NO1导航键可供用户使用
NAVIGATIONHIDDEN_YES2导航键不可用

Orientation

namevaluedesc
ORIENTATION_UNDEFINED0未知
ORIENTATION_PORTRAIT1竖屏方向,屏幕宽度小于高度
ORIENTATION_LANDSCAPE2横屏方向,屏幕宽度大于高度
ORIENTATION_SQUARE3正方形屏幕,认为屏幕宽度等于高度

uiMode

namevaluedesc
UI_MODE_TYPE_MASK0x0f定义了设备的整个UI模式,它支持如下取值
UI_MODE_TYPE_UNDEFINED0x00未知
UI_MODE_TYPE_NORMAL0x01通常模式
UI_MODE_TYPE_DESK0x02带底座模式
UI_MODE_TYPE_CAR0x03车载模式
UI_MODE_TYPE_TELEVISION0x04电视模式
UI_MODE_TYPE_APPLIANCE0x05设备模式(无显示器)
UI_MODE_TYPE_WATCH0x06手表模式
UI_MODE_TYPE_VR_HEADSET0x07
UI_MODE_NIGHT_MASK0x30定义了屏幕是否在一个特殊模式中
UI_MODE_NIGHT_UNDEFINED0x00未知
UI_MODE_NIGHT_NO0x10白天模式
UI_MODE_NIGHT_YES0x20夜间模式

densityDpi

namevaluedesc
DENSITY_DPI_UNDEFINED0
DENSITY_DPI_ANY0xfffe
DENSITY_DPI_NONE0xffff

windowConfiguration

windowConfigurationdesc
mBoundsbounds信息,包括insets
mAppBoundsApp窗口信息,不包括insets
mWindowingMode窗口模式
mDisplayWindowModeDisplay的窗口模式
mActivityTypeActivity类型
mAlwaysOnTop是否要一致处于顶部显示的标志

ActivityThread

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

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中:

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

moveStackToDisplay.png

这一块逻辑分为两大块:

  • 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

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

ensureActivityConfiguration.png

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

  • 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

scheduleActivityMovedToDisplay.png

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

参考

Android清单文件详解

Android资源应用与适配标准