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().getCompatibilityInfo();
if (!ci.equals(CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO)) {
globalConfig = new Configuration(globalConfig);
ci.applyToConfiguration(mNoncompatDensity, globalConfig);
}
synchronized (sConfigCallbacks) {
// ActivityThread.attach函数中注册了回调--ViewRootImpl.addConfigCallback(configChangedCallback);
// 重点函数Step 2 这个函数涉及的比较多,先简单介绍下
for (int i=sConfigCallbacks.size()-1; i>=0; i--) {
sConfigCallbacks.get(i).onConfigurationChanged(globalConfig);
}
}
// 更新mLastReportedMergedConfiguration
mLastReportedMergedConfiguration.setConfiguration(globalConfig, overrideConfig);
mForceNextConfigUpdate = force;
// DecorView.onAttachedToWindow中注册的callback,这个也是针对Activity场景,稍后分析
if (mActivityConfigCallback != null) {
// An activity callback is set - notify it about override configuration update.
// This basically initiates a round trip to ActivityThread and back, which will ensure
// that corresponding activity and resources are updated before updating inner state of
// ViewRootImpl. Eventually it will call #updateConfiguration().
mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId);
} else { // No Activity场景的话, 应该走这个流程 Step 3
// There is no activity callback - update the configuration right away.
updateConfiguration(newDisplayId);
}
mForceNextConfigUpdate = false;
}
Step 2. ConfigChangedCallback.onConfigurationChanged
首先看下该类的定义–位于ViewRootImpl.java
/**
* Callback for notifying about global configuration changes.
*/
public interface ConfigChangedCallback {
/** Notifies about global config change. */
void onConfigurationChanged(Configuration globalConfig);
}
通过查询源码可得,在ActivityThread.attach函数中实现了该类,并注册到了ViewRootImpl中。
ActivityThread.attatch
@UnsupportedAppUsage
private void attach(boolean system, long startSeq) {
......
ViewRootImpl.ConfigChangedCallback configChangedCallback
= (Configuration globalConfig) -> {
synchronized (mResourcesManager) {
// TODO (b/135719017): Temporary log for debugging IME service.
if (Build.IS_DEBUGGABLE && mHasImeComponent) {
Log.d(TAG, "ViewRootImpl.ConfigChangedCallback for IME, "
+ "config=" + globalConfig);
}
// We need to apply this change to the resources immediately, because upon returning
// the view hierarchy will be informed about it.
if (mResourcesManager.applyConfigurationToResourcesLocked(globalConfig,
null /* compat */,
mInitialApplication.getResources().getDisplayAdjustments())) {
updateLocaleListFromAppContext(mInitialApplication.getApplicationContext(),
mResourcesManager.getConfiguration().getLocales());
// This actually changed the resources! Tell everyone about it.
if (mPendingConfiguration == null
|| mPendingConfiguration.isOtherSeqNewer(globalConfig)) {
mPendingConfiguration = globalConfig;
sendMessage(H.CONFIGURATION_CHANGED, globalConfig);
}
}
}
};
ViewRootImpl.addConfigCallback(configChangedCallback);
}
代码虽少,但是如果能把上面逻辑都梳理清楚的话,应用端config机制基本上就掌握地差不多了。
简单介绍下:若参数globalConfig seq > oldConfig, 则更新所有Resources资源,回调Activity,Application的onConfigurationChanged函数等等。这里面涉及到的细节太多了,下一章节详细来分析。
Step 3. ViewRootImpl.updateConfiguration
/**
* Update display and views if last applied merged configuration changed.
* @param newDisplayId Id of new display if moved, {@link Display#INVALID_DISPLAY} otherwise.
*/
public void updateConfiguration(int newDisplayId) {
if (mView == null) { // 若根view为null,则直接return
return;
}
// At this point the resources have been updated to
// have the most recent config, whatever that is. Use
// the one in them which may be newer.
// View构造函数中使用的是ApplicationContext,所以这里拿的是Applicaition对应的ContextImpl的Resources对象
final Resources localResources = mView.getResources();
final Configuration config = localResources.getConfiguration(); // 获取Resources的config
// Handle move to display.
if (newDisplayId != INVALID_DISPLAY) { // 一般是多屏Display移动窗口的情况
onMovedToDisplay(newDisplayId, config);
}
// Handle configuration change. 强制更新(!mFirst)或者config change
// 一般横竖屏旋转的情况下,config肯定会change,即这里一定会走进来
if (mForceNextConfigUpdate || mLastConfigurationFromResources.diff(config) != 0) {
// Update the display with new DisplayAdjustments.
// Step 4--Step 8 设置ApplicationContext的ContextImpl的mDisplay对象
updateInternalDisplay(mDisplay.getDisplayId(), localResources);
// return (screenLayout&SCREENLAYOUT_LAYOUTDIR_MASK) == SCREENLAYOUT_LAYOUTDIR_RTL
// ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR;
final int lastLayoutDirection = mLastConfigurationFromResources.getLayoutDirection();
final int currentLayoutDirection = config.getLayoutDirection();
// 更新mLastConfigurationFromResources
mLastConfigurationFromResources.setTo(config);
// 更新布局方向??? 这个地方暂时没怎么涉及到,后面碰到再补充吧
if (lastLayoutDirection != currentLayoutDirection
&& mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
mView.setLayoutDirection(currentLayoutDirection);
}
// Step 9--Step 10 终于到这一步啦,分发config给树形结构上的所有view--config:applicaitionContext的config
mView.dispatchConfigurationChanged(config);
// We could have gotten this {@link Configuration} update after we called
// {@link #performTraversals} with an older {@link Configuration}. As a result, our
// window frame may be stale. We must ensure the next pass of {@link #performTraversals}
// catches this.
mForceNextWindowRelayout = true;
requestLayout(); // 请求布局
}
updateForceDarkMode();
}
Step 4. ViewRootImpl.updateInternalDisplay
/**
* Updates {@link #mDisplay} to the display object corresponding to {@param displayId}.
* Uses DEFAULT_DISPLAY if there isn't a display object in the system corresponding
* to {@param displayId}.
*/
private void updateInternalDisplay(int displayId, Resources resources) {
// 根据resources,DisplayInfo,displayId信息创建一个Display对象 Step 5--Step 6
final Display preferredDisplay =
ResourcesManager.getInstance().getAdjustedDisplay(displayId, resources);
if (preferredDisplay == null) {
// Fallback to use default display.
Slog.w(TAG, "Cannot get desired display with Id: " + displayId);
mDisplay = ResourcesManager.getInstance()
.getAdjustedDisplay(DEFAULT_DISPLAY, resources);
} else {
mDisplay = preferredDisplay; // 更新mDisplay
}
// Step 7--Step 8 再一次创建对应的Display对象,然后赋值给ApplicationContext的ContextImpl的mDisplay对象
mContext.updateDisplay(mDisplay.getDisplayId());
}
Step 5. ResourcesManager.getAdjustedDisplay
/**
* Returns an adjusted {@link Display} object based on the inputs or null if display isn't
* available.
*
* @param displayId display Id.
* @param resources The {@link Resources} backing the display adjustments.
*/
public Display getAdjustedDisplay(final int displayId, Resources resources) {
synchronized (this) {
// Manager communication with the display manager service on behalf of an application process.
// 该对象主要用来代表一个应用进程与DislayManagerService进行交互
final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
if (dm == null) {
// may be null early in system startup
return null;
}
// 获取新的DisplayInfo,并结合传入的参数创建Display对象 Step 6
return dm.getCompatibleDisplay(displayId, resources);
}
}
/**
* Gets an instance of the display manager global singleton.
*
* @return The display manager instance, may be null early in system startup
* before the display manager has been fully initialized.
*/
@UnsupportedAppUsage
public static DisplayManagerGlobal getInstance() {
synchronized (DisplayManagerGlobal.class) {
if (sInstance == null) {
IBinder b = ServiceManager.getService(Context.DISPLAY_SERVICE);
if (b != null) {
sInstance = new DisplayManagerGlobal(IDisplayManager.Stub.asInterface(b));
}
}
return sInstance;
}
}
Step 6. DisplayManagerGlobal.getCompatibleDisplay
/**
* Gets information about a logical display.
*
* The display metrics may be adjusted to provide compatibility
* for legacy applications or limited screen areas.
*
* @param displayId The logical display id.
* @param resources Resources providing compatibility info.
* @return The display object, or null if there is no display with the given id.
*/
public Display getCompatibleDisplay(int displayId, Resources resources) {
// 相比较与Q每次都跨进程到DMS获取DisplayInfo,R使用了PropertyInvalidatedCache机制,若是缓存失效,才到DMS中获取Displayinfo
DisplayInfo displayInfo = getDisplayInfo(displayId);
if (displayInfo == null) {
return null;
}
// 创建Display对象,初始化一些参数
return new Display(this, displayId, displayInfo, resources);
}
DisplayManagerGlobal.getDisplayInfo
/**
* Get information about a particular logical display.
*
* @param displayId The logical display id.
* @return Information about the specified display, or null if it does not exist.
* This object belongs to an internal cache and should be treated as if it were immutable.
*/
@UnsupportedAppUsage
public DisplayInfo getDisplayInfo(int displayId) {
synchronized (mLock) {
return getDisplayInfoLocked(displayId);
}
}
DisplayManagerGlobal.getDisplayInfoLocked
/**
* Gets information about a particular logical display
* See {@link getDisplayInfo}, but assumes that {@link mLock} is held
*/
private @Nullable DisplayInfo getDisplayInfoLocked(int displayId) {
DisplayInfo info = null;
if (mDisplayCache != null) { // PropertyInvalidatedCache机制也挺有趣的
info = mDisplayCache.query(displayId);
} else {
try {
info = mDm.getDisplayInfo(displayId);
} catch (RemoteException ex) {
ex.rethrowFromSystemServer();
}
}
if (info == null) {
return null;
}
registerCallbackIfNeededLocked();
if (DEBUG) {
Log.d(TAG, "getDisplayInfo: displayId=" + displayId + ", info=" + info);
}
return info;
}
Display.Display
private Display(DisplayManagerGlobal global, int displayId,
/*@NotNull*/ DisplayInfo displayInfo, DisplayAdjustments daj, Resources res) {
mGlobal = global;
mDisplayId = displayId;
mDisplayInfo = displayInfo;
mResources = res;
mDisplayAdjustments = mResources != null
? new DisplayAdjustments(mResources.getConfiguration())
: daj != null ? new DisplayAdjustments(daj) : new DisplayAdjustments();
mIsValid = true;
// Cache properties that cannot change as long as the display is valid.
mLayerStack = displayInfo.layerStack;
mFlags = displayInfo.flags;
mType = displayInfo.type;
mAddress = displayInfo.address;
mOwnerUid = displayInfo.ownerUid;
mOwnerPackageName = displayInfo.ownerPackageName;
}
Step 7. Context.updateDisplay
/**
* @hide
*/
@Override
public void updateDisplay(int displayId) {
mBase.updateDisplay(displayId);
}
Step 8. ContextImpl.updateDisplay
@Override
public void updateDisplay(int displayId) {
// 这一步之前已经分析过,这里补充一点:在创建ApplicationContext时,其ContextImpl的mDisplay并未赋值,若是走到这一
// 步,ApplicationContext对应的ContextImpl.mDisplay就设值了
mDisplay = mResourcesManager.getAdjustedDisplay(displayId, mResources);
mIsAssociatedWithDisplay = true;
}
Step 9. ViewGroup.dispatchConfigurationChanged
@Override
public void dispatchConfigurationChanged(Configuration newConfig) {
super.dispatchConfigurationChanged(newConfig);
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
children[i].dispatchConfigurationChanged(newConfig);
}
}
ViewGroup复写了该函数,从上面代码来看,这个newConfig会被分发给树形结构上的所有view,并且最终会回调每个view的onConfigurationChanged函数。
Step 10. View.dispatchConfigurationChanged
/**
* Dispatch a notification about a resource configuration change down
* the view hierarchy.
* ViewGroups should override to route to their children.
*
* @param newConfig The new resource configuration.
*
* @see #onConfigurationChanged(android.content.res.Configuration)
*/
public void dispatchConfigurationChanged(Configuration newConfig) {
onConfigurationChanged(newConfig);
}
2.2 Normal Activity
2.1.1 概述
Normal Activity的场景稍微复杂一些,ViewRootImpl与根view密切相关,Activity的根view是DecorView,所以需要看下DecorView的创建细节。
流程基本上没有区别,就不贴流程图了。
2.1.2 创建DecorView
Step 1. PhoneWindow.generateDecor
protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) { // true
// return (mPackageInfo != null) ? mPackageInfo.getApplication() : mMainThread.getApplication();
// 这个返回的是Application对象
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
// Step 2. 创建DecorContext对象
context = new DecorContext(applicationContext, this);
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes()); // 构建DecorView对象
}
Step 2. DecorContext
/**
* Context for decor views which can be seeded with display context and not depend on the activity,
* but still provide some of the facilities that Activity has,
* e.g. themes, activity-based resources, etc.
*
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public class DecorContext extends ContextThemeWrapper {
@VisibleForTesting
public DecorContext(Context baseContext, PhoneWindow phoneWindow) {
super(null /* base */, null);
setPhoneWindow(phoneWindow);
// phone.getContext()
// baseContext指向ApplicationContext--ContextWrapper
// phoneWindow.getContext()指向对应的Activity
final Context displayContext = baseContext.createDisplayContext(
// TODO(b/149790106): Non-activity context can be passed.
phoneWindow.getContext().getDisplayNoVerify());
// 赋值DecorContext的mBase对象
attachBaseContext(displayContext);
}
}
Step 3. ContextWrapper.createDisplayContext
@Override
public Context createDisplayContext(Display display) {
return mBase.createDisplayContext(display);
}
Step 4. ContextImpl.createDisplayContext
@Override
public Context createDisplayContext(Display display) {
if (display == null) { // ativity相关的Display
throw new IllegalArgumentException("display must not be null");
}
// 首先创建一个ContextImpl对象, 注意:ApplicationContext中mToken为null
ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mAttributionTag,
mSplitName, mToken, mUser, mFlags, mClassLoader, null);
final int displayId = display.getDisplayId();
// 利用现有ContextImpl对象创建一个Resources对象,然后赋值给新建的ContextImpl对象
// 所以Resources资源关联的还是ApplicaitonContext数据
context.setResources(createResources(mToken, mPackageInfo, mSplitName, displayId,
null, getDisplayAdjustments(displayId).getCompatibilityInfo(),
mResources.getLoaders()));
context.mDisplay = display; // mDisplay关联的则是Acivity数据
context.mIsAssociatedWithDisplay = true;
// Note that even if a display context is derived from an UI context, it should not be
// treated as UI context because it does not handle configuration changes from the server
// side. If the context does need to handle configuration changes, please use
// Context#createWindowContext(int, Bundle).
context.mIsUiContext = false;
return context;
}
Step 5. ContextImpl.createResources
private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo,
List<ResourcesLoader> resourcesLoader) {
final String[] splitResDirs;
final ClassLoader classLoader;
try {
splitResDirs = pi.getSplitPaths(splitName);
classLoader = pi.getSplitClassLoader(splitName);
} catch (NameNotFoundException e) {
throw new RuntimeException(e);
}
return ResourcesManager.getInstance().getResources(activityToken,
pi.getResDir(),
splitResDirs,
pi.getOverlayDirs(),
pi.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfig,
compatInfo,
classLoader,
resourcesLoader);
}
2.1.3 源码梳理
Step 1. ViewRootImpl.performConfigurationChange
private void performConfigurationChange(MergedConfiguration mergedConfiguration, boolean force,
int newDisplayId) {
......
if (mActivityConfigCallback != null) { // 之前提到过,Activity场景走这个
// An activity callback is set - notify it about override configuration update.
// This basically initiates a round trip to ActivityThread and back, which will ensure
// that corresponding activity and resources are updated before updating inner state of
// ViewRootImpl. Eventually it will call #updateConfiguration().
mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId);
} else { // No Activity场景走这个
// There is no activity callback - update the configuration right away.
updateConfiguration(newDisplayId);
}
mForceNextConfigUpdate = false;
}
Step 2. ActivityConfigCallback.onConfigurationChanged
首先看下ActivityConfigCallback对象–又是定义在ViewRootImpl文件的接口,通过调用setActivityConfigCallback函数赋值。
/**
* Callback for notifying activities about override configuration changes.
*/
public interface ActivityConfigCallback {
/**
* Notifies about override config change and/or move to different display.
* @param overrideConfig New override config to apply to activity.
* @param newDisplayId New display id, {@link Display#INVALID_DISPLAY} if not changed.
*/
void onConfigurationChanged(Configuration overrideConfig, int newDisplayId);
}
/**
* Callback used to notify corresponding activity about override configuration change and make
* sure that all resources are set correctly before updating the ViewRootImpl's internal state.
*/
private ActivityConfigCallback mActivityConfigCallback;
/** Add activity config callback to be notified about override config changes. */
public void setActivityConfigCallback(ActivityConfigCallback callback) {
mActivityConfigCallback = callback;
}
通过查看源码可知,仅有一处调用该函数–PhoneWindow.onViewRootImplSet(该函数在DecorView.onAttachedToWindow中调用)
/** Notify when decor view is attached to window and {@link ViewRootImpl} is available. */
void onViewRootImplSet(ViewRootImpl viewRoot) {
viewRoot.setActivityConfigCallback(mActivityConfigCallback);
applyDecorFitsSystemWindows();
}
追踪代码,最后最终到赋值原点–ActivityClientRecord.init(位于ActivityThread.java)
/** Common initializer for all constructors. */
private void init() {
parent = null;
embeddedID = null;
paused = false;
stopped = false;
hideForNow = false;
nextIdle = null;
configCallback = (Configuration overrideConfig, int newDisplayId) -> {
if (activity == null) {
throw new IllegalStateException(
"Received config update for non-existing activity");
}
activity.mMainThread.handleActivityConfigurationChanged(token, overrideConfig,
newDisplayId);
};
}
Step 3. ActivityThread.handleActivityConfigurationChanged
/**
* Handle new activity configuration and/or move to a different display. This method is a noop
* if {@link #updatePendingActivityConfiguration(IBinder, Configuration)} has been called with
* a newer config than {@code overrideConfig}.
*
* @param activityToken Target activity token.
* @param overrideConfig Activity override config.
* @param displayId Id of the display where activity was moved to, -1 if there was no move and
* value didn't change.
*/
@Override
public void handleActivityConfigurationChanged(IBinder activityToken,
@NonNull Configuration overrideConfig, int displayId) {
ActivityClientRecord r = mActivities.get(activityToken);
// Check input params.
if (r == null || r.activity == null) {
if (DEBUG_CONFIGURATION) Slog.w(TAG, "Not found target activity to report to: " + r);
return;
}
final boolean movedToDifferentDisplay = displayId != INVALID_DISPLAY
&& displayId != r.activity.getDisplayId();
synchronized (r) {
// 比较seq
if (overrideConfig.isOtherSeqNewer(r.mPendingOverrideConfig)) {
if (DEBUG_CONFIGURATION) {
Slog.v(TAG, "Activity has newer configuration pending so drop this"
+ " transaction. overrideConfig=" + overrideConfig
+ " r.mPendingOverrideConfig=" + r.mPendingOverrideConfig);
}
return;
}
r.mPendingOverrideConfig = null;
}
if (r.overrideConfig != null && !r.overrideConfig.isOtherSeqNewer(overrideConfig)
&& !movedToDifferentDisplay) {
if (DEBUG_CONFIGURATION) {
Slog.v(TAG, "Activity already handled newer configuration so drop this"
+ " transaction. overrideConfig=" + overrideConfig + " r.overrideConfig="
+ r.overrideConfig);
}
return;
}
// Perform updates. 更新ActivityClientRecord的overrideConfig
r.overrideConfig = overrideConfig;
...... // 先略过一些无关的代码
// Notify the ViewRootImpl instance about configuration changes. It may have initiated this
// update to make sure that resources are updated before updating itself.
if (viewRoot != null) { // 这里又调用到ViewRootImpl.updateConfiguration
viewRoot.updateConfiguration(displayId);
}
mSomeActivitiesChanged = true;
}
Step 4. ViewRootImpl.updateConfiguration
/**
* Update display and views if last applied merged configuration changed.
* @param newDisplayId Id of new display if moved, {@link Display#INVALID_DISPLAY} otherwise.
*/
public void updateConfiguration(int newDisplayId) {
if (mView == null) {
return;
}
// At this point the resources have been updated to
// have the most recent config, whatever that is. Use
// the one in them which may be newer.
// 之前分析过这里还是ApplicaitonContext的resource资源
final Resources localResources = mView.getResources();
final Configuration config = localResources.getConfiguration();
// Handle move to display.
if (newDisplayId != INVALID_DISPLAY) {
onMovedToDisplay(newDisplayId, config);
}
// Handle configuration change.
if (mForceNextConfigUpdate || mLastConfigurationFromResources.diff(config) != 0) {
// Update the display with new DisplayAdjustments. 更新mDisplay
updateInternalDisplay(mDisplay.getDisplayId(), localResources);
final int lastLayoutDirection = mLastConfigurationFromResources.getLayoutDirection();
final int currentLayoutDirection = config.getLayoutDirection();
mLastConfigurationFromResources.setTo(config);
if (lastLayoutDirection != currentLayoutDirection
&& mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
mView.setLayoutDirection(currentLayoutDirection);
}
mView.dispatchConfigurationChanged(config);
// We could have gotten this {@link Configuration} update after we called
// {@link #performTraversals} with an older {@link Configuration}. As a result, our
// window frame may be stale. We must ensure the next pass of {@link #performTraversals}
// catches this.
mForceNextWindowRelayout = true;
requestLayout();
}
updateForceDarkMode();
}
3.结语
好了,分析完毕啦,这里其实还有很多重要的内容,限于篇幅原因,就暂时略过了, 后面会继续一一做详细解析的,本章结束。
本文详细分析了在Android 11中,View.onConfigurationChanged函数的工作原理,特别是在没有Activity的纯View界面和普通Activity场景下的不同表现。文章指出,横竖屏切换时,该函数可能不会执行,但在显示时会对比配置信息,确保最终显示的参数正确。文中还介绍了配置变更的处理流程,包括ViewRootImpl和Activity的回调机制。
4045

被折叠的 条评论
为什么被折叠?



