1 引言
在Android开发中,屏幕旋转是个很重要的知识点,但网上多是列举应用端的实现方法,少有深一步探索原因的。不久前工作中遇到不少旋转屏幕时的动画和时序问题,深为困扰,但在网上又找不到细致讲解原理的文章。
本文试图从AOSP源码角度,讲解屏幕旋转和配置改变时系统和App都分别发生了什么,并据此给出应用开发时比较好的实践做法,包括如何强制设置旋转方向、如何应对生命周期变化和资源更新,对冗长的源码分析无感的同学可以直接看后面章节。
(基于Android7.1.1源码,推荐源码阅读网站:http://androidxref.com/)
2 Rotation 与 Orientation区别
rotation 与 orientation 是相关但不同的概念。
orientation标识此时界面以什么方向显示,而且从词源上讲,这个方向可以是三维的;
而rotation是指平面内旋转的方向。
竖屏 | 横屏 |
---|---|
WMS.mRotation == 0 | WMS.mRotation == 1 |
screenOrientation == 1 | screenOrientation == 0 |
Configuration.orientation == 1 | Configuration.orientation == 2 |
2.1 rotation
rotation旋转方向是指界面(不是手机)相对于默认情况顺时针旋转的角度,平板一般默认横屏,而小屏幕设备默认竖屏。
其值,定义在Surface.java中:
ROTATION_0 = 0, ROTATION_90 = 1, ROTATION_180 = 2, ROTATION_270 = 3
2.2 orientation
orientation 分为两种,一个是在ActivityInfo.java中,另一个在Configuration.java。
前者具体来说是ActivityInfo.screenOrientation,这个值用于记录App强制设定的方向或旋转模式。具体代码如下:
/**
* The preferred screen orientation this activity would like to run in.
* From the {@link android.R.attr#screenOrientation} attribute, one of
* {@link #SCREEN_ORIENTATION_UNSPECIFIED}, -1
* {@link #SCREEN_ORIENTATION_LANDSCAPE}, 0
* {@link #SCREEN_ORIENTATION_PORTRAIT}, 1
* {@link #SCREEN_ORIENTATION_USER}, 2
* {@link #SCREEN_ORIENTATION_BEHIND}, 3
* {@link #SCREEN_ORIENTATION_SENSOR},
* {@link #SCREEN_ORIENTATION_NOSENSOR},
* {@link #SCREEN_ORIENTATION_SENSOR_LANDSCAPE},
* {@link #SCREEN_ORIENTATION_SENSOR_PORTRAIT},
* {@link #SCREEN_ORIENTATION_REVERSE_LANDSCAPE},
* {@link #SCREEN_ORIENTATION_REVERSE_PORTRAIT},
* {@link #SCREEN_ORIENTATION_FULL_SENSOR},
* {@link #SCREEN_ORIENTATION_USER_LANDSCAPE},
* {@link #SCREEN_ORIENTATION_USER_PORTRAIT},
* {@link #SCREEN_ORIENTATION_FULL_USER},
* {@link #SCREEN_ORIENTATION_LOCKED}, 14
*/
public int screenOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
而Configuration.java中的orientation值可以认为只有两个,横屏或竖屏:
public static final int ORIENTATION_UNDEFINED = 0;
public static final int ORIENTATION_PORTRAIT = 1;
public static final int ORIENTATION_LANDSCAPE = 2;
public static final int ORIENTATION_SQUARE = 3;
当app具体渲染时候,不必在意具体旋转方向,区分横屏竖屏即可。
3 自动旋转开关
系统全局是否响应sensor的旋转做转屏是通过Settings中的“自动旋转”开关控制的:
开与关分别调用wms#thawRotation()和wms#freezeRotation。
这两个动作,主要是为了改PhoneWindowManager中两个值,mUserRotationMode和mUserRotation,前者控制是否自由旋屏,后者是旋转锁死时候的系统旋转方向。
mUserRotationMode会有两种值,声明在WindowManagerPolicy中:
/** When not otherwise specified by the activity's screenOrientation, rotation should be
* determined by the system (that is, using sensors). */
public final int USER_ROTATION_FREE = 0;
/** When not otherwise specified by the activity's screenOrientation, rotation is set by
* the user. */
public final int USER_ROTATION_LOCKED = 1;
在thawRotation开启自动旋转时,设置mUserRotationMode
值为0。
在freezeRotation时,除了设置mUserRotationMode
为1,还会为mUserRotation
赋值,一般是0,即竖屏方向,这样如果应用没有自行设置orientation,那么方向判断时就会使用这里的mUserRotation
,从而尽可能保持竖屏。
设置这两个值的具体过程还涉及到Setting、PhoneWindowManager.SettingObserver,有兴趣的可以看此处blog的详细代码和流程分析:WMS注册传感器回调
mUserRotationMode
和mUserRotation
将在判断orientation的时候起到关键作用,在后续具体流程还会讲到。
4 旋转,从Sensor到onConfigurationChanged
上图是一般情况下在应用内屏幕从竖屏旋转到竖屏时的调用流程图,这里我们分为三部分讲,包括WMS一侧rotation更新逻辑,AMS一侧的configuration控制逻辑,以及AMS将configuration分发到app和WMS的逻辑。
4.1 Sensor -> PWM -> WMS
特别值得看的有两个地方:
一个是WMS#updateRotationUnchecked()
,还有一个是WMS#rotationForOrientationLw()
。
updateRotationUnchecked方法可以看作是更新旋转的枢纽,它联系了PWM、AMS和WMS,代码如下:
updateRotationUnchecked()
public void updateRotationUnchecked(boolean alwaysSendConfiguration, boolean forceRelayout) {
......
boolean changed;
synchronized(mWindowMap) {
changed = updateRotationUncheckedLocked(false);
if (!changed || forceRelayout) {
getDefaultDisplayContentLocked().layoutNeeded = true;
mWindowPlacerLocked.performSurfacePlacement();
}
}
if (changed || alwaysSendConfiguration) {
sendNewConfiguration();
}
Binder.restoreCallingIdentity(origId);
}
首先调用updateRotationUncheckedLocked()进行rotation的更新,而如果rotation有更新,则sendNewConfiguration()通知AMS更新configuration。
而在updateRotationUncheckedLocked()中,也就是流程图中的5处,会通过下面这句计算得到新的rotation:
int rotation = mPolicy.rotationForOrientationLw(mLastOrientation, mRotation);
mLastOrientation和mRotation全局记录在WMS中,其中前者是应用显示方向,后者是生效的旋转方向。
这个方法是重点,因为我们需要知道对于不同应用不同环境,为什么有时候会旋转有时候不旋转,而这个方法的结果直接决定了系统是否发生旋转。方法具体如下:
public int rotationForOrientationLw(int orientation, int lastRotation) {
if (false) {
Slog.v(TAG, "rotationForOrientationLw(orient="
+ orientation + ", last=" + lastRotation
+ "); user=" + mUserRotation + " "
+ ((mUserRotationMode == WindowManagerPolicy.USER_ROTATION_LOCKED)
? "USER_ROTATION_LOCKED" : "")
);
}
if (mForceDefaultOrientation) {
return Surface.ROTATION_0;
}
synchronized (mLock) {
int sensorRotation = mOrientationListener.getProposedRotation(); // may be -1
if (sensorRotation < 0) {
sensorRotation = lastRotation;
}
final int preferredRotation;
if (mLidState == LID_OPEN && mLidOpenRotation >= 0) {
// Ignore sensor when lid switch is open and rotation is forced.
preferredRotation = mLidOpenRotation;
}
......
else if (mDemoRotationLock) {
// Ignore sensor when demo rotation lock is enabled.
// Note that the dock orientation and HDMI rotation lock override this.
preferredRotation = mDemoRotation;
} else if (orientation == ActivityInfo.SCREEN_ORIENTATION_LOCKED) {
// Application just wants to remain locked in the last rotation.
preferredRotation = lastRotation;
} else if (!mSupportAutoRotation) {
// If we don't support auto-rotation then bail out here and ignore
// the sensor and any rotation lock settings.
preferredRotation = -1;
} else if ((mUserRotationMode == WindowManagerPolicy.USER_ROTATION_FREE
&& (orientation == ActivityInfo.SCREEN_ORIENTATION_USER
|| orientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|| orientation == ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|| orientation == ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|| orientation == ActivityInfo.SCREEN_ORIENTATION_FULL_USER))
|| orientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR
|| orientation == ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
|| orientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|| orientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
// Otherwise, use sensor only if requested by the application or enabled
// by default for USER or UNSPECIFIED modes. Does not apply to NOSENSOR.
if (mAllowAllRotations < 0) {
// Can't read this during init() because the context doesn't
// have display metrics at that time so we cannot determine
// tablet vs. phone then.
mAllowAllRotations = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_allowAllRotations) ? 1 : 0;
}
if (sensorRotation != Surface.ROTATION_180
|| mAllowAllRotations == 1
|| orientation == ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
|| orientation == ActivityInfo.SCREEN_ORIENTATION_FULL_USER) {
preferredRotation = sensorRotation;
} else {
preferredRotation = lastRotation;
}
} else if (mUserRotationMode == WindowManagerPolicy.USER_ROTATION_LOCKED
&& orientation != ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) {
// Apply rotation lock. Does not apply to NOSENSOR.
// The idea is that the user rotation expresses a weak preference for the direction
// of gravity and as NOSENSOR is never affected by gravity, then neither should
// NOSENSOR be affected by rotation lock (although it will be affected by docks).
preferredRotation = mUserRotation;
} else {
// No overriding preference.
// We will do exactly what the application asked us to do.
preferredRotation = -1;
}
switch (orientation) {
case ActivityInfo.SCREEN_ORIENTATION_PORTRAIT:
// Return portrait unless overridden.
if (isAnyPortrait(preferredRotation)) {
return preferredRotation;
}
return mPortraitRotation;
case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE:
// Return landscape unless overridden.
if (isLandscapeOrSeascape(preferredRotation)) {
return preferredRotation;
}
return mLandscapeRotation;
......
default:
// For USER, UNSPECIFIED, NOSENSOR, SENSOR and FULL_SENSOR,
// just return the preferred orientation we already calculated.
if (preferredRotation >= 0) {
return preferredRotation;
}
return Surface.ROTATION_0;
}
}
}
这个函数前面的if-else主要是结合当前系统显示模式、旋转模式以及sensor监测到的旋转方向给出一个preferredRotation,后面的switch-case则是根据当前的orientation决定是否可以使用preferredRotation。
下面我们从两个具体情况看这段代码做了什么,应用不设置横竖屏的情况以及应用设置强制竖屏的情况
4.1.1 应用不设置横竖屏的情况
对于一个没有设置screenOrientation的应用来说,一次竖屏旋转到横屏,wms是如何判定orientation的?
int rotation = mPolicy.rotationForOrientationLw(mLastOrientation, mRotation);
在上面一行代码中,mLastOrientation
此时为应用的screenOrientation值 -1(UNSPECIFIED),mRotatiton
是上次竖屏的值0。
然后进入orientationForRotation()
int sensorRotation = mOrientationListener.getProposedRotation(); // may be -1
要转到竖屏,所以此时sensorRotation为1。在后续判断中,首先根据各种系统此时的显示模式以及应用的orientationMode计算preferredRotation。在当前情况下,会走到90行
preferredRotation = sensorRotation;
并且由于当前orientation是SCREEN_ORIENTATION_UNSPECIFIED(应用未设置)的,最后会在130行将preferredRotation(==1)作为结果返回。
随后WMS将mRotation
值更新为1,并继续根据该值更新displayInfo等信息,
具体更新displayInfo等相关内容的流程这里不再详解,大致意思就是一层层重新计算display的长宽,最终给到设备底层,具体代码流程分析可以看此处。
除了dissplayInfo的更新,这里还创建了旋转动画,与动画相关的具体在下文第7节讲解。
这些WMS系统显示层相关东西搞定后,回到WMS#updateRotationUnchecked
,此时changed为true,即configuration有变化,调用AMS#updateConfiguration更新configuration。
下面看看应用如果设置了强制竖屏时,rotationForOrientationLw返回的结果如何。
4.1.2 应用设置强制竖屏的情况
应用强制设置竖屏的方式将在第5节中具体讲,
可以确定的一点是,在成功设置好竖屏后,且没关闭系统自动旋转开关的情况下,几个关键的变量如下:
WMS.mLastOrientation == 1, WMS.mRotation == 0,
PWM.mUserRotationMode == 0 (ROTATION_FREE), PWM.mUserRotation == 0。
然后我们将手机从竖屏旋转到横屏,在rotationForOrientationLw()
中进行判断时,虽然UserRotationMode
是FREE的,但mLastOrientation
却并不属于67~76行的条件中的一个,preferredRotation不能使用sensorRotation,且在后面switch(orientation)是,对于ORIENTATION_PORTRAIT的情况,实际上preferredRotation也是不会生效的,无论如何都会返回0,也就是说应用设定的orientation优先级总是最高的。
WMS拿到0的结果后,由于与已有的mRotation
值相同,会中断后续逻辑,对旋转不做任何反应,由于没有向AMS更新configuration,所以也不会有onConfigurationChanged过程,其它app也不会知道发生了旋转。
4.2 AMS
回到WMS#updateRotationUnchecked(),在确定rotation更新后changed==true,执行方法sendNewConfiguration() -> AMS#updateConfiguration 通知AMS更新configuration。
AMS#updateConfiguration()
public void updateConfiguration(Configuration values) {
enforceCallingPermission(android.Manifest.permission.CHANGE_CONFIGURATION,
"updateConfiguration()");
synchronized(this) {
if (values == null && mWindowManager != null) {
// sentinel: fetch the current configuration from the window manager
values = mWindowManager.computeNewConfiguration();
}
if (mWindowManager != null) {
mProcessList.applyDisplaySize(mWindowManager);
}
final long origId = Binder.clearCallingIdentity();
if (values != null) {
Settings.System.clearConfiguration(values);
}
updateConfigurationLocked(values, null, false);
Binder.restoreCallingIdentity(origId);
}
}
入参values这时候为空,首先从WMS重新获取一遍,生成一个新的Configuration,然后进入方法updateConfigurationLocked()
ActivityManagerService#updateConfigurationLocked
/**
* Do either or both things: (1) change the current configuration, and (2)
* make sure the given activity is running with the (now) current
* configuration. Returns true if the activity has been left running, or
* false if <var>starting</var> is being destroyed to match the new
* configuration.
*
* @param userId is only used when persistent parameter is set to true to persist configuration
* for that particular user
*/
private boolean updateConfigurationLocked(Configuration values, ActivityRecord starting,
boolean initLocale, boolean persistent, int userId, boolean deferResume) {
int changes = 0;
if (mWindowManager != null) {
mWindowManager.deferSurfaceLayout();
}
if (values != null) {
Configuration newConfig = new Configuration(mConfiguration);
changes = newConfig.updateFrom(values); //copy更新,同时以flags的方式记录区别
if (changes != 0) {
......
mConfigurationSeq++;
if (mConfigurationSeq <= 0) {
mConfigurationSeq = 1;
}
newConfig.seq = mConfigurationSeq; //序列号,可用来略过旧的config
mConfiguration = newConfig; //保存新configuration
......
final Configuration configCopy = new Configuration(mConfiguration);
......
for (int i=mLruProcesses.size()-1; i>=0; i--) {
ProcessRecord app = mLruProcesses.get(i);
try {
if (app.thread != null) {
if (DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION, "Sending to proc "
+ app.processName + " new config " + mConfiguration);
app.thread.scheduleConfigurationChanged(configCopy);
}
} catch (Exception e) {
}
}
Intent intent = new Intent(Intent.ACTION_CONFIGURATION_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
| Intent.FLAG_RECEIVER_REPLACE_PENDING
| Intent.FLAG_RECEIVER_FOREGROUND);
broadcastIntentLocked(null, null, intent, null, null, 0, null, null,
null, AppOpsManager.OP_NONE, null, false, false,
MY_PID, Process.SYSTEM_UID, UserHandle.USER_ALL);
if ((changes&ActivityInfo.CONFIG_LOCALE) != 0) {
intent = new Intent(Intent.ACTION_LOCALE_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
if (initLocale || !mProcessesReady) {
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
}
broadcastIntentLocked(null, null, intent,
null, null, 0, null, null, null, AppOpsManager.OP_NONE,
null, false, false, MY_PID, Process.SYSTEM_UID, UserHandle.USER_ALL);
}
......
}
// Update the configuration with WM first and check if any of the stacks need to be
// resized due to the configuration change. If so, resize the stacks now and do any
// relaunches if necessary. This way we don't need to relaunch again below in
// ensureActivityConfigurationLocked().
if (mWindowManager != null) {
final int[] resizedStacks = mWindowManager.setNewConfiguration(mConfiguration);
if (resizedStacks != null) {
for (int stackId : resizedStacks) {
final Rect newBounds = mWindowManager.getBoundsForNewConfiguration(stackId);
mStackSupervisor.resizeStackLocked(
stackId, newBounds, null, null, false, false, deferResume);
}
}
}
}
boolean kept = true;
final ActivityStack mainStack = mStackSupervisor.getFocusedStack();
// mainStack is null during startup.
if (mainStack != null) {
if (changes != 0 && starting == null) {
// If the configuration changed, and the caller is not already
// in the process of starting an activity, then find the top
// activity to check if its configuration needs to change.
starting = mainStack.topRunningActivityLocked();
}
if (starting != null) {
kept = mainStack.ensureActivityConfigurationLocked(starting, changes, false);
// And we need to make sure at this point that all other activities
// are made visible with the correct configuration.
mStackSupervisor.ensureActivitiesVisibleLocked(starting, changes,
!PRESERVE_WINDOWS);
}
}
if (mWindowManager != null) {
mWindowManager.continueSurfaceLayout();
}
return kept;
}
代码略长,上面已经略去很多这里不会讲的内容。
整个方法先前后通过mWindowManager.deferSurfaceLayout()
和mWindowManager.continueSurfaceLayout()
包起来,毕竟config变动布局等影响很大,许多状态变化会触发到surfacelayout,通过这种方式避免重复刷新。
首先将新config保存到mConfiguration中,然后是关键的3343行:66行,updateConfigurationChanged()还会发出ACTION_CONFIGURATION_CHANGED的广播,似乎是留给其它service接收该广播做其他事情的。
遍历mLruProcesses,调用appThread.scheduleConfigurationChanged,对应的ActivityThread随后进入onConfigurationChanged相关流程,app侧的处理下面会再细讲。跳转
除此之外,44
再之后,图中流程15,调用WMS#setNewConfiguration回到WMS:
@Override
public int[] setNewConfiguration(Configuration config) {
if (!checkCallingPermission(android.Manifest.permission.MANAGE_APP_TOKENS,
"setNewConfiguration()")) {
throw new SecurityException("Requires MANAGE_APP_TOKENS permission");
}
synchronized(mWindowMap) {
if (mWaitingForConfig) {
mWaitingForConfig = false;
mLastFinishedFreezeSource = "new-config";
}
boolean configChanged = mCurConfiguration.diff(config) != 0;
if (!configChanged) {
return null;
}
prepareFreezingAllTaskBounds();
mCurConfiguration = new Configuration(config);
return onConfigurationChanged();
}
}
这个方法主要是向WMS更新mCurConfiguration。是的,WMS此时才更新到新的Configuration,之前WMS的逻辑主要是在display和phone层面上的动作,最关键的修改是mRotation,而这里则扩展到了Configuration领域的动作,主题、字体、bounds的改变都是configuration的改变,AMS在逻辑层面更有话语权,而此时WMS响应新的配置项,并调用WMS#onConfigurationChanged()。
不过在更早的android版本上,这里并没有这么多的操作,会直接就进行surfaceLayout(现在延迟到continueSurfaceLayout了),这里增加的内容主要为了提前响应非全屏应用的bounds的改变,尤其有不少为分屏stack量身定制的逻辑,这里就不再细讲了。
在刷新界面之前,还有很重要的一部分逻辑,91行:
kept = mainStack.ensureActivityConfigurationLocked(starting, changes, false);
// And we need to make sure at this point that all other activities
// are made visible with the correct configuration.
mStackSupervisor.ensureActivitiesVisibleLocked(starting, changes, !PRESERVE_WINDOWS);
然后进入ActivityStack#ensureActivityConfigurationLocked
/**
* Make sure the given activity matches the current configuration. Returns false if the activity
* had to be destroyed. Returns true if the configuration is the same, or the activity will
* remain running as-is for whatever reason. Ensures the HistoryRecord is updated with the
* correct configuration and all other bookkeeping is handled.
*/
boolean ensureActivityConfigurationLocked(
ActivityRecord r, int globalChanges, boolean preserveWindow) {
......
if ((changes&(~r.info.getRealConfigChanged())) != 0 || r.forceNewConfig) {
// Aha, the activity isn't handling the change, so DIE DIE DIE.
r.configChangeFlags |= changes;
r.startFreezingScreenLocked(r.app, globalChanges);
r.forceNewConfig = false;
preserveWindow &= isResizeOnlyChange(changes);
if (r.app == null || r.app.thread == null) {
......
} else if (r.state == ActivityState.RESUMED) {
// Try to optimize this case: the configuration is changing and we need to restart
// the top, resumed activity. Instead of doing the normal handshaking, just say
// "restart!".
relaunchActivityLocked(r, r.configChangeFlags, true, preserveWindow);
} else {
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION,
"Config is relaunching non-resumed " + r);
relaunchActivityLocked(r, r.configChangeFlags, false, preserveWindow);
}
// All done... tell the caller we weren't able to keep this activity around.
return false;
}
// Default case: the activity can handle this new configuration, so hand it over.
// NOTE: We only forward the task override configuration as the system level configuration
// changes is always sent to all processes when they happen so it can just use whatever
// system level configuration it last got.
r.scheduleConfigurationChanged(taskConfig, true);
r.stopFreezingScreenLocked(false);
return true;
}
注释已经将此方法解释得很好:确保activity的Configuration更新,如果应用没有针对该类改变做特殊处理,那么不得不重启应用,走relaunch流程;相反,如果应用能够在handle当前的变化,那么走scheduleConfigurationChanged->onConfigurationChanged流程。这里的两个分支判断细节在后面会用具体例子讲解,这里先继续讲relauch流程部分。
relaunch流程很长,简单的说就是把应用重启了一遍,我们跳过各种binder调用,直接到app一侧并择取其中关键易懂的:
android.app.ActivityThread#handleRelaunchActivity()
private void handleRelaunchActivity(ActivityClientRecord tmp) {
......
// Need to ensure state is saved.
if (!r.paused) {
performPauseActivity(r.token, false, r.isPreHoneycomb(), "handleRelaunchActivity");
}
if (r.state == null && !r.stopped && !r.isPreHoneycomb()) {
callCallActivityOnSaveInstanceState(r);
}
handleDestroyActivity(r.token, false, configChanges, true);
......
handleLaunchActivity(r, currentIntent, "handleRelaunchActivity");
......
}
可以看到,代码里顺次进行了pause、destroy、launch,前面两个就是一般意义上的onPause、onDestroy生命周期。
而launch流程,它则是与一般的start流程<span class=”hint–top hint–error hint–medium hint–rounded hint–bounce” aria-label=”LocalActivityManager#moveToState(INITIALIZING)->startActivityNow() 和 handleLaunch() 都会走到performLaunchActivity() -> onStart() -> onCreate()
“>1一样的,relaunch就是我们常说的旋转屏幕时Activity会销毁重建的原因。
然后我们还看到第8行调用了onSaveInstanceState,所以应对屏幕旋转时的重新实例化的改变,通常需要在onSaveInstanceState()中保留必要的现场数据,应对屏幕旋转的其它技术细节见第6节。
回到AMS,除了对top应用的处理,还有对其它activities的处理。ensureActivitiesVisibleLocked将会遍历task,确定其它的activity的visible和configuration状态都正常,如果还有需要显示的activity(例如前台是透明应用的情况),也要走一遍ensureActivityConfigurationLocked
流程。
4.3 AMS -> AppThread.onConfigurationChanged
首先给出结论,只要系统AMS里的configuration确认发生了变化,所有app都会接收到新的configuration,并根据其配置可能执行onConfigurationChanged方法。
在上一节AMS#updateConfigurationLocked方法中,我们讲到其会遍历mLruProcesses,对每个appThread调用scheduleConfigurationChanged接口,这里我们讲一讲app在config变化后都做了什么以及如何继续步入onConfigurationChanged方法,流程图如下
首先在2.updatePendingConfiguration里把新值保存在mPendingConfiguration里,丢出msg,AMS继续遍历,app侧在handleConfigurationChanged继续处理。
紧接着,执行4.applyConfigurationToResourcesLocked,顾名思义,讲config应用到resource上,可以想见,这里将会遍历resource,按照config的变化刷新资源,具体是交给ResourcesImpl.updateConfiguration处理,值得细看,但我对resource不太熟悉,就不细讲了。
接下来collectComponentCallbacks(),收集应用的组件,再遍历组件执行performConfigurationChanged(cb),这也是我们比较关心的地方。
这个方法里主要是判断一下当前config的改变是否应该触发onConfigurationChanged,后面也有例子细讲。
再接下来正式进入各个组件的onConfigurationChanged方法,在service、contentprovider中都是空方法,activity有些不同:
public void onConfigurationChanged(Configuration newConfig) {
if (DEBUG_LIFECYCLE) Slog.v(TAG, "onConfigurationChanged " + this + ": " + newConfig);
mCalled = true;
mFragments.dispatchConfigurationChanged(newConfig);
if (mWindow != null) {
// Pass the configuration changed event to the window
mWindow.onConfigurationChanged(newConfig);
}
if (mActionBar != null) {
// Do this last; the action bar will need to access
// view changes from above.
mActionBar.onConfigurationChanged(newConfig);
}
}
最后再总结下Activity在发生旋转时重要的生命周期:
- 如果acitivityInfo.getRealConfigChanged() 不能cover住当前config发生的改变:
转屏 -> onPause -> onSaveInstanceState -> onStop -> onDestroy -> onCreate -> onStart -> onRestoreInstanceState -> onResume - 相反,如果config的变化应用自行可以处理:
转屏 -> 只会执行onConfigurationChanged
在网上有类说法如下:
不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次
设置Activity的android:configChanges=”orientation|keyboardHidden”时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法
请注意,这种说法完全不对,再怎么说,生命周期怎么可能重复执行两次呢,那是要有多么的浪费资源,框架不可能设计得那么差,这个时候就得想想是不是自己的代码写错了。
另外orientation 也与 keyboardHidden是两类不同的标签,二者没有关系,在API13以后orientation需要与screenSize配合使用,这是因为横竖屏切换不仅变方向同时也有屏幕大小的变化。详情见官方文档。
5 应用强制横屏竖屏
应用强制设置屏幕方向,就我所知有两种方式,一种是在manifest里设定,一种是在代码里通过setOrientation设定,接下来分别讲解具体方法以及原理。
5.1 静态 screenOrientation属性
假设需要强制竖屏,修改ActivityManifest.xml对应的activity元素里的内容如下即可:
<activity android:name=".MainActivity"
android:screenOrientation="portrait">
还有更多属性值用法见官方文档。
这里网上有说法认为使用了screenOrientation也应该同时使用configChanges,这样可以避免 应用在后台时由于其它应用的旋转切换回本应用时会onCreate<span class=”hint–top hint–error hint–medium hint–rounded hint–bounce” aria-label=”LocalActivityManager#moveToState(INITIALIZING)->startActivityNow() 和 handleLaunch() 都会走到performLaunchActivity() -> onStart() -> onCreate()
“>1。
这种问题意味着多走了relaunch流程,我实测了下未出现此种问题。
在resume一个应用时,确实会再次做ensureActivityConfig,ActivityStack#resumeTopActivityInnerLocked()中有如下一段代码:
// Have the window manager re-evaluate the orientation of
// the screen based on the new activity order.
boolean notUpdated = true;
if (mStackSupervisor.isFocusedStack(this)) {
Configuration config = mWindowManager.updateOrientationFromAppTokens(
mService.mConfiguration,
next.mayFreezeScreenLocked(next.app) ? next.appToken : null);
if (config != null) {
next.frozenBeforeDestroy = true;
}
notUpdated = !mService.updateConfigurationLocked(config, next, false);
}
第11行updateConfigurationLocked是之前讲到的AMS更新config的方法,方法内会执行ensureActivityConfig()
但我们可以看到,在updateConfigurationLocked之前,第5行重新从wms计算了一遍config,而这个config在下面ensureAtyConfig判断时,会发现前后Aty已有的config与此值一样,所以正常不应该再走relaunch流程。
不过有鉴于异常情况总会意想不到地出现,这里还是推荐增加相应configChanges属性值。
5.2 动态 setRequestedOrientation
上一小节我们讲了静态修改的方法,接下来是用代码动态生效的方法。
代码动态修改orientation为横屏,例子如下:
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
直接在activity中执行该方法即可,它会调用到AMS#setRequestedOrientation(),并最终执行WMS#setAppOrientation来修改AppToken.requestOrientation的值,用于保存应用设定的屏幕方向。
其实,前面静态设置屏幕方向的screenOrientation值,最终也是传递到了AppToken.requestOrientation<span class=”hint–top hint–error hint–medium hint–rounded hint–bounce” aria-label=”LocalActivityManager#moveToState(INITIALIZING)->startActivityNow() 和 handleLaunch() 都会走到performLaunchActivity() -> onStart() -> onCreate()
“>1才生效,二者殊途同归。
那么这个值究竟又是如何影响屏幕旋转方向的呢?
5.3 应用强制屏幕方向生效原理
前面 4.1.1 和 4.1.2 具体讲解了WMS如何判断决定屏幕方向,主要通过PWM.rotationForOrientationLw方法,而这个方法里,screenOrientation几乎是起决定性作用的,这个参数的值则是WMS.mLastOrientation。全局搜索这个WMS.mLastOrientation的引用,发现只有一个写操作的地方:WMS#updateOrientationFromAppTokensLocked(boolean)。
WindowManagerService#updateOrientationFromAppTokensLocked(boolean)
/*
* Determine the new desired orientation of the display, returning
* a non-null new Configuration if it has changed from the current
* orientation. IF TRUE IS RETURNED SOMEONE MUST CALL
* setNewConfiguration() TO TELL THE WINDOW MANAGER IT CAN UNFREEZE THE
* SCREEN. This will typically be done for you if you call
* sendNewConfiguration().
*
* The orientation is computed from non-application windows first. If none of
* the non-application windows specify orientation, the orientation is computed from
* application tokens.
* @see android.view.IWindowManager#updateOrientationFromAppTokens(
* android.os.IBinder)
*/
boolean updateOrientationFromAppTokensLocked(boolean inTransaction) {
long ident = Binder.clearCallingIdentity();
try {
int req = getOrientationLocked();
if (req != mLastOrientation) {
mLastOrientation = req;
//send a message to Policy indicating orientation change to take
//action like disabling/enabling sensors etc.,
mPolicy.setCurrentOrientationLw(req);
if (updateRotationUncheckedLocked(inTransaction)) {
// changed
return true;
}
}
return false;
} finally {
Binder.restoreCallingIdentity(ident);
}
}
在此方法中,通过getOrientationLocked()计算并更新mLastOrientation的值。具体计算过程大致就是注释里第9行开始说的,先根据系统状态看是否有确定的orientation(比如锁屏时一般就只能是portrait),如果没有,则取前台应用appToken里的requestedOrientation拿来用。
WMS#updateOrientationFromAppTokensLocked通常在应用resume时被调用,它的调用流程见下一节流程图。
所以我们可以总结出应用强制设置orientation的数据流图大概如下:
5.4 横屏应用载入流程
这个就直接窃用网上博客图了,各个方法前面基本也都有讲到,这里就略过了。
6 更好的应对旋转布局变化以及持久化
6.1 在onConfigurationChanged里处理configuration的变化,避免重新实例化
自行处理配置变更,官方文档在此,简单来说需要做两件事情:
- 在manifest的
标签里增加configChanges属性,例如,如果你准备在onConfigurationChanged处理旋转屏幕的情况,要这样写:
<activity android:name=".MyActivity"
android:configChanges="orientation|screenSize"
android:label="@string/app_name">
- 重写各个组件的onConfigurationChanged方法
接下来从代码流程上讲下这种处理方式的原理:
在上面AMS#ensureActivityConfigurationLocked里我们提到,应用要么走relaunch重新实例化的流程,要么scheduleConfigurationChanged走onConfigurationChanged流程。而这里我们希望是后者。
ActivityStack#ensureActivityConfigurationLocked
boolean ensureActivityConfigurationLocked(
ActivityRecord r, int globalChanges, boolean preserveWindow) {
......
if ((changes&(~r.info.getRealConfigChanged())) != 0 || r.forceNewConfig) {
// Aha, the activity isn't handling the change, so DIE DIE DIE.
r.configChangeFlags |= changes;
r.startFreezingScreenLocked(r.app, globalChanges);
r.forceNewConfig = false;
preserveWindow &= isResizeOnlyChange(changes);
if (r.app == null || r.app.thread == null) {
......
} else if (r.state == ActivityState.RESUMED) {
// Try to optimize this case: the configuration is changing and we need to restart
// the top, resumed activity. Instead of doing the normal handshaking, just say
// "restart!".
relaunchActivityLocked(r, r.configChangeFlags, true, preserveWindow);
} else {
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION,
"Config is relaunching non-resumed " + r);
relaunchActivityLocked(r, r.configChangeFlags, false, preserveWindow);
}
// All done... tell the caller we weren't able to keep this activity around.
return false;
}
// Default case: the activity can handle this new configuration, so hand it over.
// NOTE: We only forward the task override configuration as the system level configuration
// changes is always sent to all processes when they happen so it can just use whatever
// system level configuration it last got.
r.scheduleConfigurationChanged(taskConfig, true);
r.stopFreezingScreenLocked(false);
return true;
}
上面代码第5行是这个方法重要的分支点。
首先我们假设此时的activity设置了configChanges=”orientation|screenSize”。
如果此时config只发生了旋屏变化,此时changes&(~r.info.getRealConfigChanged())
为0,整个判断条件为false(标记位的细节就不解释了),所以会跳过该代码块,走下面28行开始的schedule流程,也就是onConfigurationChanged流程;
相对的,如果此时的config发生了其它变化,或者不只是旋屏变化,此时判断条件会为真,意味着app无法在onCOnfigurationChanged自行处理config的所有变化,需要重走实例化流程,走relaunch流程并返回。
这段代码基本解释了为什么有的时候应用不得不重新实例化,有的时候则是走onConfigurationChanged。
还有一点要提一下,relaunch和onConfigurationChanged流程是不会并存的。
虽然ensureActivityConfig之前都会发出scheduleConfigurationChanged流程,但在app一侧,也会进行上面类似的判断,对与activityinfo.getRealConfigChanged()掩码不能handle当前config变化的情况,会跳过onConfigurationChanged,果断等待后面的relaunch。
android.app.ActivityThread#performConfigurationChanged
private void performConfigurationChanged(ComponentCallbacks2 cb,
IBinder activityToken,
Configuration newConfig,
Configuration amOverrideConfig,
boolean reportToActivity) {
......
boolean shouldChangeConfig = false;
if ((activity == null) || (activity.mCurrentConfig == null)) {
shouldChangeConfig = true;
} else {
// If the new config is the same as the config this Activity is already
// running with and the override config also didn't change, then don't
// bother calling onConfigurationChanged.
int diff = activity.mCurrentConfig.diff(newConfig);
if (diff != 0 || !mResourcesManager.isSameResourcesOverrideConfig(activityToken,
amOverrideConfig)) {
// Always send the task-level config changes. For system-level configuration, if
// this activity doesn't handle any of the config changes, then don't bother
// calling onConfigurationChanged as we're going to destroy it.
if (!mUpdatingSystemConfig
|| (~activity.mActivityInfo.getRealConfigChanged() & diff) == 0
|| !reportToActivity) {
shouldChangeConfig = true;
}
}
}
if (shouldChangeConfig) {
...
if (reportToActivity) {
cb.onConfigurationChanged(configToReport);
}
...
}
如上,在21行会同样把ActivityInfo.getRealConfigChanged与diff做判断,如果shouldChangeConfig不为真,后面并不会调用onConfigurationChanged。
也就是说,我们任何写在onConfiguraionChanged中的一套东西,不能忘了在实例化时也得有一套(如果在旋转屏幕的同时还发生了其它的配置变化,并不会先在onConfigChange里处理屏幕旋转)。
6.2 需要实例化的情况,做好现场的保存与恢复
由于应用侧代码写得较少,我自己这里也总结不出什么好的具体实践的建议,这里暂时就贴下我看到的比较好的总结以及官方文档,以后有机会再更新。
在配置变更期间保留对象
Handling Orientation Changes on Android
保存与恢复
7 DividerBar 分屏分割条如何在旋转时附加特殊动画
(未完待续)
注:
- 1.LocalActivityManager#moveToState(INITIALIZING)->startActivityNow() 和 handleLaunch() 都会走到performLaunchActivity() -> onStart() -> onCreate()
8: http://or5nesfx1.bkt.clouddn.com/prief_force_orientation_flow.png ↩