Android屏幕旋转源码探索及应用实践

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  
 */  
@ScreenOrientation  
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;  
@Deprecated 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注册传感器回调

mUserRotationModemUserRotation将在判断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中,其中前者是应用显示方向,后者是生效的旋转方向。
这个方法是重点,因为我们需要知道对于不同应用不同环境,为什么有时候会旋转有时候不旋转,而这个方法的结果直接决定了系统是否发生旋转。方法具体如下:

@Override  
    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行:
遍历mLruProcesses,调用appThread.scheduleConfigurationChanged,对应的ActivityThread随后进入onConfigurationChanged相关流程,app侧的处理下面会再细讲。跳转
除此之外,44
66行,updateConfigurationChanged()还会发出ACTION_CONFIGURATION_CHANGED的广播,似乎是留给其它service接收该广播做其他事情的。
再之后,图中流程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方法,流程图如下
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 横屏应用载入流程

这个就直接窃用网上博客图了,各个方法前面基本也都有讲到,这里就略过了。
landscape_start_flow

6 更好的应对旋转布局变化以及持久化

6.1 在onConfigurationChanged里处理configuration的变化,避免重新实例化

自行处理配置变更,官方文档在此,简单来说需要做两件事情:

  1. 在manifest的标签里增加configChanges属性,例如,如果你准备在onConfigurationChanged处理旋转屏幕的情况,要这样写:
<activity android:name=".MyActivity"  
          android:configChanges="orientation|screenSize"  
          android:label="@string/app_name">  
  1. 重写各个组件的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. 1.LocalActivityManager#moveToState(INITIALIZING)->startActivityNow() 和 handleLaunch() 都会走到performLaunchActivity() -> onStart() -> onCreate()

    8: http://or5nesfx1.bkt.clouddn.com/prief_force_orientation_flow.png