View.onConfigurationChanged函数分析
目录
1.概述
最近总是遇到应用模块因为不了解View.onConfigurationChanged函数,错误地使用该方法处理横竖屏切换的情况,导致模块出现异常。本篇就来详细分析下View.onConfigurationChanged这个函数。
首先看下View.java文件中的定义,该函数是一个空函数,由应用模块自己来实现。
该函数的作用,注释也写得很清楚了: 当应用程序正在使用的资源的当前配置发生更改时调用。可以使用它来决定是否重新加载与方向和其他配置特征进行相关的资源。只有当你不依赖普通的Activity机制时(例如状态栏就不存在Activity,都是由view堆积起来的),才需要使用这个函数。
/**
* Called when the current configuration of the resources being used
* by the application have changed. You can use this to decide when
* to reload resources that can changed based on orientation and other
* configuration characteristics. You only need to use this if you are
* not relying on the normal {@link android.app.Activity} mechanism of
* recreating the activity instance upon a configuration change.
*
* @param newConfig The new resource configuration.
*/
protected void onConfigurationChanged(Configuration newConfig) {
}
2.源码梳理
这里主要区分两种情况:
1.No Activity,仅仅是由View堆积的场景
2.Normal Activity场景
这里先总结下,后面根据总结内容来梳理的话会更加清晰一些:
系统横竖屏切换时,View.onConfigurationChanged是有可能不执行的,如果该view所在的窗口没有参与布局的情况下,一般就不会走到View.onConfigurationChanged中,但是再次显示时,会对比应用上一次保存的config信息,如果不一致就会走入View.onConfigurationChanged,否则就不会走入。
用一句话概括就是: View.onConfigurationChanged函数只能确保最终显示时的参数newConfig是正确的,但是并不能确保每次横竖屏切换时都会走入该函数。
Google原生机制就是如此,需要应用去主动适配,正确地使用该方法。
2.1 No Activity(纯View堆积的界面)
2.1.1 概述
下面这种用法就是纯View的场景(稍微对源码了解一些的人,应该都清楚,Activity的窗口也是这么创建的,Activity只是进行了一层封装而已):
Context applicationContext = mContext.getApplicationContext()
WindowManager mWindowManager = (WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams lp = ......;
View mView = new View(applicationContext); // 一般来说No Activity场景,使用的都是ApplicationContext
mWindowManager.addView(mView, lp);
2.1.2 源码梳理
通过查看源码可知,其入口点为ViewRootImpl.performConfigurationChange(如果对ViewRootImpl也不太了解的话,建议先看深入理解Android3 第6章 深入理解控件系统–写得很好也很详细), 该函数有3处场景调用:
(1). ViewRootImpl.performTraversals
private void performTraversals() {
......
// 判断是否符合执行布局窗口的条件:第一次执行performTraversals,窗口尺寸更改,可见性更改,
// cutout更改(目前市面上有很多摄像头在屏幕中央或者屏幕左右侧的非全面屏,这类凹口屏都有一块
// 区域用来让应用模块避免内容被摄像头遮盖,这个区域就是cutout),
// view layoutParams更改或者mForceNextWindowRelayout为true
if (mFirst || windowShouldResize || viewVisibilityChanged || cutoutChanged
|| params != null || mForceNextWindowRelayout) {
// 请求系统执行布局窗口
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
if (DEBUG_LAYOUT) Log.v(mTag, "relayout: frame=" + frame.toShortString()
+ " cutout=" + mPendingDisplayCutout.get().toString()
+ " surface=" + mSurface);
// If the pending {@link MergedConfiguration} handed back from
// {@link #relayoutWindow} does not match the one last reported,
// WindowManagerService has reported back a frame from a configuration not yet
// handled by the client. In this case, we need to accept the configuration so we
// do not lay out and draw with the wrong configuration.
// mPendingMergedConfiguration是上一步请求系统布局返回的config信息--mWindowSession.relayout
// mLastReportedMergedConfiguration是private变量--Last configuration reported from WM
// 若是新返回的pendingConfig与上一次wm传来的config不一致,则调用performConfigurationChange
if (!mPendingMergedConfiguration.equals(mLastReportedMergedConfiguration)) {
if (DEBUG_CONFIGURATION) Log.v(mTag, "Visible with new config: "
+ mPendingMergedConfiguration.getMergedConfiguration());
performConfigurationChange(mPendingMergedConfiguration, !mFirst,
INVALID_DISPLAY /* same display */);
updatedConfiguration = true;
}
}
(2). ViewRootImpl.ViewRootHandler.handleMessage–MSG_RESIZED_REPORT消息
case MSG_RESIZED_REPORT:
if (mAdded) {
SomeArgs args = (SomeArgs) msg.obj;
final int displayId = args.argi3;
MergedConfiguration mergedConfiguration = (MergedConfiguration) args.arg4;
final boolean displayChanged = mDisplay.getDisplayId() != displayId;
boolean configChanged = false;
// mergedConfiguration: 系统端通过IWindow.resized传给应用端的参数信息
// 同样地,若是新传来的mergedConfiguration与上一次wm传来的config不一致,则调用performConfigurationChange
if (!mLastReportedMergedConfiguration.equals(mergedConfiguration)) {
// If configuration changed - notify about that and, maybe,
// about move to display.
performConfigurationChange(mergedConfiguration, false /* force */,
displayChanged
? displayId : INVALID_DISPLAY /* same display */);
configChanged = true;
}
......
}
break;
(3). ViewRootImpl.ViewRootHandler.handleMessage–MSG_UPDATE_CONFIGURATION消息
case MSG_UPDATE_CONFIGURATION: {
Configuration config = (Configuration) msg.obj;
if (config.isOtherSeqNewer(
mLastReportedMergedConfiguration.getMergedConfiguration())) {
// If we already have a newer merged config applied - use its global part.
config = mLastReportedMergedConfiguration.getGlobalConfiguration();
}
// Use the newer global config and last reported override config.
mPendingMergedConfiguration.setConfiguration(config,
mLastReportedMergedConfiguration.getOverrideConfiguration());
performConfigurationChange(mPendingMergedConfiguration, false /* force */,
INVALID_DISPLAY /* same display */);
} break;
通过代码来看,这个函数是从ActivityThread.handleUpdatePackageCompatibilityInfo中走进来的,但是该函数目前我并没有怎么涉及到,目前并没有看到有这种问题场景,因此暂时略。
上面的(1),(2)一般都只有在窗口可见并参与布局的时候才调用,因此若是窗口并不是一直参与布局的话,上面两个场景可能都不会被调用到,而mLastReportedMergedConfiguration可能一直未更新–该值仅在ViewRootImpl.performConfigurationChange中赋值。(例如状态栏面板窗口就属于该场景,用户下拉状态栏,显示状态栏面板,上滑收起,则隐藏状态栏面板)
/** Last configuration reported from WM or via {@link #MSG_UPDATE_CONFIGURATION}. */
private final MergedConfiguration mLastReportedMergedConfiguration = new MergedConfiguration();
上面场景已经分析完毕了,下面开始来看ViewRootImpl.performConfigurationChange
Step 1. ViewRootImpl.performConfigurationChange
/**
* Notifies all callbacks that configuration and/or display has changed and updates internal
* state. 通知所有callbacks configuration and/or display已更改并更新内部状态。
* @param mergedConfiguration New global and override config in {@link MergedConfiguration}
* container.
* @param force Flag indicating if we should force apply the config.
* @param newDisplayId Id of new display if moved, {@link Display#INVALID_DISPLAY} if not
* changed.
*/
private void performConfigurationChange(MergedConfiguration mergedConfiguration, boolean force,
int newDisplayId) {
if (mergedConfiguration == null) {
// 若mergedCofig为null,则直接抛出异常
throw new IllegalArgumentException("No merged config provided.");
}
// 分别取出mergedConfig的global config和override config并赋值
Configuration globalConfig = mergedConfiguration.getGlobalConfiguration();
final Configuration overrideConfig = mergedConfiguration.getOverrideConfiguration();
if (DEBUG_CONFIGURATION) Log.v(mTag,
"Applying new config to window " + mWindowAttributes.getTitle()
+ ", globalConfig: " + globalConfig
+ ", overrideConfig: " + overrideConfig);
final CompatibilityInfo ci = mDisplay.getDisplayAdjustments(