在前一篇文章中,我们从ViewRootImpl的setView方法一直分析到WMS中的addWindow方法,这个方法真是是非常的长,鉴于篇幅太长,而接下去的方法还有不少,且有些也比较重要,所以这篇文章继续从WMS的addWindow方法开始分析,
```java
public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
...............
boolean focusChanged = false;
// 如果窗口能接受按键事件
if (win.canReceiveKeys()) {
// 更新当前的焦点窗口
focusChanged = updateFocusedWindowLocked(UPDATE_FOCUS_WILL_ASSIGN_LAYERS,
false /* updateInputWindows */);
if (focusChanged) { // 如果焦点窗口有变化的话
// 这里理解为焦点窗口变化,自然输入法会消失,而焦点窗口在上面
// updateFocusedWindowLocked方法会自己切换
// 所以这个不用设置为true,一旦设置了rue后,下面会重新计算焦点窗口就重复了
imMayMove = false; // 不需要后面再处理输入法相关窗口了
}
}
..............
}
```
上一篇文章分析到WMS的addWindow方法,addWindow方法里面做了很多的事情,最主要的就是创建WindowState的对象,这个对象是从WMS的视角来说,描述一个窗口的封装对象,里面包含了很多这个窗口的属性和状态,当然各个窗口之间的关系也是比较微妙的,添加一个窗口后不仅仅就是增加一个对象,不同窗口对象之间会互相影响,所以一旦添加一个窗口后,会总整体上对所有的创建进行一定的更新和调整,比如之前文章中讲到的和输入法窗口相关的目标对象可能在添加新窗口后会发生变化,所以添加窗口后还有很复杂的逻辑需要处理,上面这段代码中的updateFocusedWindowLocked就是对添加新窗口后,当前的焦点对象进行更新,里面的逻辑也是比较复杂的,我们来看一下。
当这个添加的窗口是可以接受按键事件的话,会调用updateFocusedWindowLocked方法来更新下当前的焦点窗口,我们先看下canReceiveKeys这个方法:
```java
boolean canReceiveKeys() {
return isVisibleOrAdding()
&& (mViewVisibility == View.VISIBLE) && !mRemoveOnExit
&& ((mAttrs.flags & WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) == 0)
&& (mAppToken == null || mAppToken.windowsAreFocusable())
&& !canReceiveTouchInput();
}
```
我们知道所谓焦点窗口,简单说就是可以显示在前台给人操作的,这里方法里开始的isVisibleOrAdding方法我们前一篇文章已经看过了,主要就是表示这个窗口是非隐藏的或者该窗口还是添加过程中但是是可见的,具体方法可以去看下前一篇文章。之后这里的判断也是差不多的,这里主要需要这个窗口是可以获得焦点的,否则不可以作为一个焦点窗口,这里我们看下AppWindowToken的windowsAreFocusable方法:
```java
boolean windowsAreFocusable() {
return StackId.canReceiveKeys(getTask().mStack.mStackId) || mAlwaysFocusable;
}
public static boolean canReceiveKeys(int stackId) {
return stackId != PINNED_STACK_ID;
}
```
可以看到这里主要是判断如果当前TaskStack是PINNED_STACK_ID类型的,即画中画类型的窗口,就是不可聚焦的,我们想想一般画中画窗口都是类似浮窗那样显示在最前面的,自然不会成为一个焦点窗口,其他类型TaskStack都是可以作为焦点窗口的。
总之这里首先判断如果添加的是一个可以作为焦点窗口的话,那么可能会引起现有窗口的一些变化,所以会调用updateFocusedWindowLocked方法,这里方法应该说是主要的一个处理窗口变化时候的方法,里面包括计算窗口在Z轴层序以及测量等,我们看下这个方法:
```java
// 更新最新的焦点窗口,处理窗口顺序布局等,以及输入法对新焦点窗口的影响,移除不同uid的toast的窗口等
boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows) {
// 获取当前要更新的焦点窗口
WindowState newFocus = mRoot.computeFocusedWindow();
// 要更新的焦点窗口和当前的不是一个
if (mCurrentFocus != newFocus) {
mH.removeMessages(H.REPORT_FOCUS_CHANGE);
mH.sendEmptyMessage(H.REPORT_FOCUS_CHANGE);
// TODO(multidisplay): Focused windows on default display only.
// 获取默认显示屏幕
final DisplayContent displayContent = getDefaultDisplayContentLocked();
boolean imWindowChanged = false; // 输入法窗口是否有改变
if (mInputMethodWindow != null) { // 输入法窗口非空的话
// 获取之前的输入法窗口
final WindowState prevTarget = mInputMethodTarget;
// 获取当前的输入法窗口
final WindowState newTarget = displayContent.computeImeTarget(true /* updateImeTarget */);
// 如果之前和当前输入法不是同一个,说明输入法窗口z序有改变
imWindowChanged = prevTarget != newTarget;
// 更新屏幕里的窗口Z序
if (mode != UPDATE_FOCUS_WILL_ASSIGN_LAYERS
&& mode != UPDATE_FOCUS_WILL_PLACE_SURFACES) {
// 先把之前输入法窗口动画Z序的保存下
final int prevImeAnimLayer = mInputMethodWindow.mWinAnimator.mAnimLayer;
// 更新这个屏幕里面的windowState的z序
displayContent.assignWindowLayers(false /* setLayoutNeeded */);
// 是否输入法窗口Z序有变化
imWindowChanged |= prevImeAnimLayer != mInputMethodWindow.mWinAnimator.mAnimLayer;
}
}
// 如果输入法窗口有变化的话,再次获取下要更新的焦点窗口
if (imWindowChanged) {
mWindowsChanged = true;
displayContent.setLayoutNeeded();
// 获取更新的焦点窗口
newFocus = mRoot.computeFocusedWindow();
}
.................
}
return false;
}
```
这个方法从名字上看我们可以知道主要是更新下当前的焦点窗口,此外也会处理窗口在Z轴上的顺序等问题,我们分几段来看,上面是第一段。这里首先调用RootWindowContainer的computeFocusedWindow方法来获取当前的焦点窗口,我们跟进这个方法看下:
```java
// 从上往下,寻找焦点窗口
WindowState computeFocusedWindow() {
// 遍历所有屏幕
for (int i = mChildren.size() - 1; i >= 0; i--) {
final DisplayContent dc = mChildren.get(i);
// 寻找这个屏幕是否有焦点窗口
final WindowState win = dc.findFocusedWindow();
if (win != null) {
return win;
}
}
return null;
}
```
所谓焦点窗口,简单来说就是显示在屏幕最上面的一个可见窗口,所以这里会从上往下遍历所有屏幕中最上面的一个窗口,这里遍历过程中又会调用DisplayContent的findFocusedWindow方法,我们继续进入看下:
```java
// 寻找这个屏幕的焦点窗口
WindowState findFocusedWindow() {
mTmpWindow = null;
// 遍历所有windowState,寻找一个可以作为焦点的WindowState,一般来说比如添加删除后
// 返回栈最上面一个可以作为焦点的WindowState
forAllWindows(mFindFocusedWindow, true /* traverseTopToBottom */);
if (mTmpWindow == null) {
if (DEBUG_FOCUS_LIGHT) Slog.v(TAG_WM, "findFocusedWindow: No focusable windows.");
return null;
}
return mTmpWindow;
}
```
这些寻找窗口的方法逻辑经过这么多的分析,我们也很熟悉了,这里遍历这个屏幕中的所有窗口,每次遍历会调用mFindFocusedWindow来判断当前这个是不是焦点窗口,我们看下mFindFocusedWindow表达式:
```java
// ToBooleanFunction是一个接口,这个接口只有一个方法,w是这个方法的泛型参数,这里即为WindowState
// 返回一个焦点可以作为焦点的windowState,比如添加一个新Activity的时候,最上面的windowState应该
// 就是新添加的这个Activity了,正常情况下就会返回这个windowState
private final ToBooleanFunction<WindowState> mFindFocusedWindow = w -> {
// 当前这个屏幕前台的Activity
final AppWindowToken focusedApp = mService.mFocusedApp;
if (DEBUG_FOCUS) Slog.v(TAG_WM, "Looking for focus: " + w
+ ", flags=" + w.mAttrs.flags + ", canReceive=" + w.canReceiveKeys());
// 如果不能接受key事件,返回false
if (!w.canReceiveKeys()) {
return false;
}
// 获取这个窗口对应的Activity
final AppWindowToken wtoken = w.mAppToken;
// If this window's application has been removed, just skip it.
// 如果这个Activity非null,但是被移除了,或者将被放到栈底部,所以不能作为焦点窗口,return
if (wtoken != null && (wtoken.removed || wtoken.sendingToBottom)) {
if (DEBUG_FOCUS) Slog.v(TAG_WM, "Skipping " + wtoken + " because "
+ (wtoken.removed ? "removed" : "sendingToBottom"));
return false;
}
// 如果当前前台没有Activity,那么这个WindowState就作为焦点窗口返回
if (focusedApp == null) {
if (DEBUG_FOCUS_LIGHT) Slog.v(TAG_WM, "findFocusedWindow: focusedApp=null"
+ " using new focus @ " + w);
mTmpWindow = w;
return true;
}
// 如果当前Activity是不能获取焦点的,那么还是把遍历到的这个WindowState作为焦点窗口返回
if (!focusedApp.windowsAreFocusable()) {
// Current focused app windows aren't focusable...
if (DEBUG_FOCUS_LIGHT) Slog.v(TAG_WM, "findFocusedWindow: focusedApp windows not"
+ " focusable using new focus @ " + w);
mTmpWindow = w;
return true;
}
// Descend through all of the app tokens and find the first that either matches
// win.mAppToken (return win) or mFocusedApp (return null).
// 如果这个windowState属于一个Activity,并且不是启动窗口,
if (wtoken != null && w.mAttrs.type != TYPE_APPLICATION_STARTING) {
// 比较当前前台focusedApp和这个windowState所在Activty在Z轴的关系
// 如果当前前台focusedApp在上面,那么就返回null,否则就会返回遍历到的这个windowState
if (focusedApp.compareTo(wtoken) > 0) {
// App stack below focused app stack. No focus for you!!!
if (DEBUG_FOCUS_LIGHT) Slog.v(TAG_WM,
"findFocusedWindow: Reached focused app=" + focusedApp);
mTmpWindow = null;
return true;
}
}
if (DEBUG_FOCUS_LIGHT) Slog.v(TAG_WM, "findFocusedWindow: Found new focus @ " + w);
mTmpWindow = w;
return true;
};
```
这个方法看上不算很短,其实还比较简单。这里遍历到的这个WindowState是需要能接受按键事件的,只有能接收案件事件的窗口才能成为焦点窗口。其次这个遍历到的窗口不能是被移除的或者被放到任务栈底部的,是这种情况的话也不能作为焦点窗口。
在WMS中保存着当前的焦点窗口,如果当前焦点窗口不存在或者是不可聚焦的,那么就返回当前遍历到的这个窗口作为新的焦点窗口。这里判断窗口是否是可以聚焦的方法windowsAreFocusable前面我们也已经看过了,这不多说。
如果说当前WMS中保存的焦点窗口也是一个符合条件的焦点窗口,那么需要和当前遍历到的这个窗口进行比较,看哪个更加合适,调用的方法是两个窗口WindowToken的compareTo方法,这个方法的实现是在WindowToken的超类WindowContainer中,我们稍稍看一下是怎么比较的就可以:
```java
public int compareTo(WindowContainer other) {
if (this == other) {
return 0;
}
if (mParent != null && mParent == other.mParent) {
final WindowList<WindowContainer> list = mParent.mChildren;
return list.indexOf(this) > list.indexOf(other) ? 1 : -1;
}
..................
}
```
这个方法我们不细看,这里截取开始的一段就可以大致了解他的意思。这里可以看到这里比较两个WindowContainer的试试,就是比较他们在集合中的下标,下标大的返回1,否则返回0,而我们知道下标大的就是显示在屏幕上的,所以返回到前面比较2个焦点窗口的方法,就是哪个窗口在上面就选择哪个作为焦点窗口,我们回到前面WMS的updateFocusedWindowLocked方法。
从所有屏幕中找到当前的焦点窗口后,如果和当前保存在WMS中的焦点窗口不一样的话,说明焦点窗口有改变。然后如果当前有输入法窗口的话,会比较下之前保存在WMS中的输入法目标窗口和当前屏幕中重新查找的输入法目标窗口是否一致,如果不一致或者更新当前屏幕中Z轴层序后输入法窗口的z轴层序也不一致,那么会重新获取下当前的焦点窗口。这是updateFocusedWindowLocked第一部分代码所做的事情,我们看下第二部分代码:
```java
boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows) {
..............
// 保存下当前焦点窗口
final WindowState oldFocus = mCurrentFocus;
// 把需要更新的焦点窗口设置为当前窗口
mCurrentFocus = newFocus;
// 如果在失去焦点能力的窗口中有这个新的WindowState,移除他
mLosingFocus.remove(newFocus);
// 如果当前焦点不为null了
if (mCurrentFocus != null) {
// 清除下之前记录的焦点窗口为null时,添加和移除的窗口
mWinAddedSinceNullFocus.clear();
mWinRemovedSinceNullFocus.clear();
}
// 保存最新的焦点窗口newFocus到PhoneWindowManager中,并返回导航栏是否有变化
// 如果有变化返回FINISH_LAYOUT_REDO_LAYOUT,表示需要重新布局。否则0
int focusChanged = mPolicy.focusChangedLw(oldFocus, newFocus);
// 输入法窗口有变化,并且旧的焦点窗口不是输入法窗口
if (imWindowChanged && oldFocus != mInputMethodWindow) {
// Focus of the input method window changed. Perform layout if needed.
// 这里表示需要强制布局更新,所以下面调用performLayout
// 上面focusChanged决定了要不要重新布局,这里既然已经重新布局了,那么把focusChanged重新置回去
if (mode == UPDATE_FOCUS_PLACING_SURFACES) {
// 重新布局
displayContent.performLayout(true /* initial */, updateInputWindows);
focusChanged &= ~FINISH_LAYOUT_REDO_LAYOUT;
} else if (mode == UPDATE_FOCUS_WILL_PLACE_SURFACES) {
// 这里表示更新焦点在重新布局前
// Client will do the layout, but we need to assign layers
// for handleNewWindowLocked() below.
// 在重新布局前先调整下这个屏幕中的Z序
displayContent.assignWindowLayers(false /* setLayoutNeeded */);
}
}
// 进入这个if,说明需要重新布局
if ((focusChanged & FINISH_LAYOUT_REDO_LAYOUT) != 0) {
// The change in focus caused us to need to do a layout. Okay.
// 标记下重新布局是需要的
displayContent.setLayoutNeeded();
// 如果mode是表示更新焦点窗口是在重新布局的时候,那么调用下面的performLayout方法重新布局
if (mode == UPDATE_FOCUS_PLACING_SURFACES) {
displayContent.performLayout(true /* initial */, updateInputWindows);
}
}
// 如果mode不是在计算窗口层这个阶段
if (mode != UPDATE_FOCUS_WILL_ASSIGN_LAYERS) {
// If we defer assigning layers, then the caller is responsible for
// doing this part.
// 设置焦点窗口给InputMonitor
mInputMonitor.setInputFocusLw(mCurrentFocus, updateInputWindows);
}
..............
}
```
这部分代码,首先把WMS中保存的焦点窗口更新为最新的这个,之后调用WindowManagerPolicy的focusChangedLw方法,这个方法会计算下最新的焦点窗口是否导航栏显示有变化,我们看一眼这个方法就好:
```java
public int focusChangedLw(WindowState lastFocus, WindowState newFocus) {
// 保存下最新的焦点窗口
mFocusedWindow = newFocus;
// 如果导航栏有变化的话,返回FINISH_LAYOUT_REDO_LAYOUT,否则返回0
// updateSystemUiVisibilityLw会根据最新焦点窗口来计算导航栏是否有变化
if ((updateSystemUiVisibilityLw()&SYSTEM_UI_CHANGING_LAYOUT) != 0) {
// If the navigation bar has been hidden or shown, we need to do another
// layout pass to update that window.
// 返回这个说明导航栏有变化,界面的摆放位置就会有变化,需要重新布局
return FINISH_LAYOUT_REDO_LAYOUT;
}
return 0;
}
```
这个方法主要是调用updateSystemUiVisibilityLw来判断是否SystemUI有变化,这个方法里面根据各种系统方面的资源会返回SystemUI是否有变化,方法这里就不跟进了,有兴趣的同学可以自己去研究下,如果有变化的话,这个方法会返回FINISH_LAYOUT_REDO_LAYOUT,这个表示需要重新触发界面的布局,这个布局方法下面会说,这里主要是指导航栏显隐发生变化后需要界面的重新布局,我们回到前面updateFocusedWindowLocked方法。
目前这个方法前面一些代码已经可以计算出不少的窗口变化了,比如输入法目标窗口可能发生变化,或者输入法窗口的Z轴层序发生变化,那么下面就需要重新计算下整个的z轴层序或者重新布局,会根据这里mode类型来确定做什么。我们看到这里mode主要有三种类型,UPDATE_FOCUS_WILL_ASSIGN_LAYERS,UPDATE_FOCUS_PLACING_SURFACES和UPDATE_FOCUS_WILL_PLACE_SURFACES。这里简单理解的话,UPDATE_FOCUS_WILL_ASSIGN_LAYERS表示当前处于等待Z轴层序计算完成前的状态,UPDATE_FOCUS_PLACING_SURFACES表示当前需要触发重新布局的请求,UPDATE_FOCUS_WILL_PLACE_SURFACES表示当前处于等待触发布局完成前的状态。
所以上面代码中如果当前模式为UPDATE_FOCUS_PLACING_SURFACES的时候,表示要重新布局,这里会调用Display的performLayout方法,这个方法会重新测量窗口大小,我们稍后在来看这个方法。如果模式为UPDATE_FOCUS_WILL_PLACE_SURFACES,表示当前还没布局好,所以会调用assignWindowLayers方法来设置Z轴的层序值,assignWindowLayers这个方法我们之前已经分析过了,可以去之前分析的地方看下。
接着上面刚说过,如果导航栏有变化的话,会设置focusChanged为true表示需要重新布局,当然如果上面当前模式是UPDATE_FOCUS_PLACING_SURFACES的时候会调用performLayout方法重新布局过了,所以就不需要重新布局了,如果没有重新布局过,focusChanged为true的话,并且当前模式为UPDATE_FOCUS_PLACING_SURFACES的话,还是会立刻触发布局方法performLayout,这个方法后面再看。
最后如果当前模式不是UPDATE_FOCUS_WILL_ASSIGN_LAYERS的话,说明当前对z轴层序的更新已经完成了,会调用InputMonitor的setInputFocusLw,把当前焦点窗口传给InputMonitor的setInputFocusLw方法,InputMonitor类是负责WMS和InputManagerService通信的,是处理当前焦点窗口的输入事件的,这个把当前焦点窗口传给InputMonitor,相当于为用户输入事件的处理做初始化,这个关于输入事件处理的模块我们不展开,后面会有这方面专门分析的文章。
我们回到WMS的updateFocusedWindowLocked方法,接着还有一点代码,我们继续看一下:
```java
boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows) {
...........
// 调整那些和这个输入法窗口有关的目标窗口
displayContent.adjustForImeIfNeeded();
// 移除和之前焦点窗口uid一样,但是和新焦点窗口uid不一样的toast,避免影响影响当前窗口
displayContent.scheduleToastWindowsTimeoutIfNeededLocked(oldFocus, newFocus);
.............
}
```
最后还有2行代码,第一行会更新调整下和输入法窗口有关的其他窗口,第二行是处理之前窗口遗留的toast。我们先看下调整输入法窗口相关的方法:
```java
void adjustForImeIfNeeded() {
// 获取输入法窗口
final WindowState imeWin = mService.mInputMethodWindow;
// 如果输入法非null,输入法允许显示,并且已经显示了,同时没有要求被隐藏。简单来说为true就是这个输入法窗口现在显示了
final boolean imeVisible = imeWin != null && imeWin.isVisibleLw() && imeWin.isDisplayedLw()
&& !mDividerControllerLocked.isImeHideRequested();
// 为true表示是分屏模式是可见的
final boolean dockVisible = isStackVisible(DOCKED_STACK_ID);
// 获取输入法所在的TaskStack
final TaskStack imeTargetStack = mService.getImeFocusStackLocked();
// 如果是分屏可见模式,并且输入法非null,获取输入法所在的分屏窗口是哪个位置的
final int imeDockSide = (dockVisible && imeTargetStack != null) ?
imeTargetStack.getDockSide() : DOCKED_INVALID;
// 分屏上面的
final boolean imeOnTop = (imeDockSide == DOCKED_TOP);
// 分屏下面的
final boolean imeOnBottom = (imeDockSide == DOCKED_BOTTOM);
// 是否这个分屏窗口是最小化状态
final boolean dockMinimized = mDividerControllerLocked.isMinimizedDock();
// 输入法窗口的高度
final int imeHeight = mService.mPolicy.getInputMethodWindowVisibleHeightLw();
// 如果输入法窗口是可见的,同时这个高度和分屏中输入法高度是不一样的,所以说明输入法高度改变了
final boolean imeHeightChanged = imeVisible &&
imeHeight != mDividerControllerLocked.getImeHeightAdjustedFor();
// The divider could be adjusted for IME position, or be thinner than usual,
// or both. There are three possible cases:
// - If IME is visible, and focus is on top, divider is not moved for IME but thinner.
// - If IME is visible, and focus is on bottom, divider is moved for IME and thinner.
// - If IME is not visible, divider is not moved and is normal width.
// 上面的注释解释,对于在分屏中的输入法而言,如果输入法窗口是可见的,出现在分屏的上部,分割线位置不用移动,但是会变细
// 如果输入法出现在下部,分割线会移动(也就是下面窗口会顶上去一些),分割线也会变细
// 如果输入法不显示的话,那什么也不动了
// 如果输入法窗口可见,并且是分屏模式,同时输入法是上面或者下面的屏幕中,而且分屏窗口不是最小化
if (imeVisible && dockVisible && (imeOnTop || imeOnBottom) && !dockMinimized) {
// 遍历当前屏幕中所有的taskStack
for (int i = mTaskStackContainers.size() - 1; i >= 0; --i) {
// 获取每个taskStack
final TaskStack stack = mTaskStackContainers.get(i);
// 如果是属于下面的分屏窗口
final boolean isDockedOnBottom = stack.getDockSide() == DOCKED_BOTTOM;
// 如果这个taskStack可见,同时输入法窗口在下面的分屏窗口或者本身是下面的分屏窗口
// 根据上面的注释说明会调整窗口,把下面的窗口顶上去,分割线也会变细
if (stack.isVisible() && (imeOnBottom || isDockedOnBottom) &&
StackId.isStackAffectedByDragResizing(stack.mStackId)) {
// 调整输入法窗口大小
stack.setAdjustedForIme(imeWin, imeOnBottom && imeHeightChanged);
} else {
// 执行到这里不用调整输入法窗口什么,设置下退出窗口时候连带输入法一起退出flag就好了
stack.resetAdjustedForIme(false);
}
}
// 调整分屏的分割线大小
mDividerControllerLocked.setAdjustedForIme(
imeOnBottom /*ime*/, true /*divider*/, true /*animate*/, imeWin, imeHeight);
} else {
// 执行到这里可能是非分屏等情况或者输入法当前不显示等或者输入法显示在左右的分屏中这些情况,所以也会遍历当前屏幕中所有taskStack
// 比如分屏同时显示输入法,但是是左右分屏的情况。后者不分屏的情况
for (int i = mTaskStackContainers.size() - 1; i >= 0; --i) {
// 取出每个taskStack
final TaskStack stack = mTaskStackContainers.get(i);
// 如果是分屏情况,下面方法参数是true,会添加一个输入法和屏幕一起退出的flag
// 如果不是分屏情况,宿主窗口回复默认的显示大小就可以了
stack.resetAdjustedForIme(!dockVisible);
}
// 调整分屏分割线大小
mDividerControllerLocked.setAdjustedForIme(
false /*ime*/, false /*divider*/, dockVisible /*animate*/, imeWin, imeHeight);
}
// 更新下画中画输入法的显示状态和高度
mPinnedStackControllerLocked.setAdjustedForIme(imeVisible, imeHeight);
}
```
这个方法我们分两部分来看下。上面第一部分会获取输入法窗口,然后在获取当前这个输入法窗口是否在分屏的TaskStack中,由于在分屏模式下,在上面的屏幕和下面的屏幕中,输入法弹出后,后面显示的目标窗口是会有变动,比如如果输入法在下部的分屏窗口弹出,后面的屏幕就是往上挪一些,所以这里会根据输入的位置坐不同的处理。
如果一个输入法窗口显示在一个分屏的上半部,那么分屏的分界线不是移动,但是会变得细一些。如果显示在分屏的下半部分,那么分屏的分界线会往上移动一些,同样也会变细一些。输入法不显示的话,那么分屏分界线位置和宽度保持不变。
上面这段代码主要就是处理输入法在分屏下的情况,具体注释上面也有,有兴趣深入了解的同学可以再跟进看下上面的代码。我们回到前面updateFocusedWindowLocked方法,最后还有一段处理关于Toast的代码,调用的是DisplayContent的scheduleToastWindowsTimeoutIfNeededLocked方法:
```java
void scheduleToastWindowsTimeoutIfNeededLocked(WindowState oldFocus, WindowState newFocus) {
// 如果是相同uid的,不用移除
if (oldFocus == null || (newFocus != null && newFocus.mOwnerUid == oldFocus.mOwnerUid)) {
return;
}
// Used to communicate the old focus to the callback method.
// 保存下之前的焦点窗口
mTmpWindow = oldFocus;
// 移除和这个焦点窗口uid一样的toast窗口
forAllWindows(mScheduleToastTimeout, false /* traverseTopToBottom */);
}
```
这段代码主要是当焦点窗口变化的时候,如果新老窗口不是一个用户的话,会移除老窗口中的Toast。可以看到这个方法先是判断下是否新老窗口是一个用户下的,如果是一个用户那么不会移除Toast。如果不是的话,会遍历屏幕中的Toast窗口发出TimeOut的Handler,这里还是用的一个Lambda表达式:
```java
private final Consumer<WindowState> mScheduleToastTimeout = w -> {
final int lostFocusUid = mTmpWindow.mOwnerUid;
final Handler handler = mService.mH;
if (w.mAttrs.type == TYPE_TOAST && w.mOwnerUid == lostFocusUid) {
if (!handler.hasMessages(WINDOW_HIDE_TIMEOUT, w)) {
handler.sendMessageDelayed(handler.obtainMessage(WINDOW_HIDE_TIMEOUT, w),
w.mAttrs.hideTimeoutMilliseconds);
}
}
};
```
这个表达式也很简单,如果遇到的是Toast窗口,如果和老焦点窗口的uid是一样的话,那么就说明和新焦点窗口不是一个用户,那么就会通过Handler移除,这个很简单就不多说了。
到这里的话updateFocusedWindowLocked方法就分析完了,这个方法里面最主要的就是根据情况会重新布局和更新z轴层序,Z轴层序调用的方法是assignWindowLayers,我们已经分析过了,剩下的重新布局方法performLayout,这个方法由于里面牵涉到的方法非常的长我们等WMS流程分析完后,再回过头来说这个方法,这个方法的作用会根据当前屏给出的属性,进行窗口的测量,然后将他们相互之间的位置处理好,这个其实已经涉及到View模块相关的内容了,我们放到最后来说。
我们回到WMS的addWindow方法,经过焦点窗口的更新后后,如果新老焦点窗口有改变,那么前面updateFocusedWindowLocked方法里面也已经做了相关的处理了,包括对输入法的处理,所以这个这里会把imMayMove置为false,表示不需要再对输入法目标窗口进行重新计算了。我们接着看WMS还剩余的一些方法:
```java
...............
// 如果处理输入法相关窗口,那么重新计算焦点窗口
if (imMayMove) {
displayContent.computeImeTarget(true /* updateImeTarget */);
}
// Don't do layout here, the window must call
// relayout to be displayed, so we'll do it there.
// 更新z序
displayContent.assignWindowLayers(false /* setLayoutNeeded */);
// 如果焦点窗口改变了
if (focusChanged) {
// 更新和输入事件处理相关的设置
mInputMonitor.setInputFocusLw(mCurrentFocus, false /* updateInputWindows */);
}
// 同样设置事件处理分发器
mInputMonitor.updateInputWindowsLw(false /* force */);
// 如果窗口是可见并且屏幕的方向也有更新,说明config有变化,设置reportNewConfig为true
if (win.isVisibleOrAdding() && updateOrientationFromAppTokensLocked(false, displayId)) {
reportNewConfig = true;
}
...........
if (reportNewConfig) { // 如果config有变化
// 更新AMS中的三个config
sendNewConfiguration(displayId);
}
.....................
```
上面就是WMS中addWindow方法最后的一段代码了。最后这段方法里调用的很多方法之前我们都已经分析过了,比如如果输入法窗口有改变的话,会调用computeImeTarget方法。更新屏幕中所有窗口的z轴层序,调用的方法assignWindowLayers也分析过了。焦点窗口有变化的话,会设置焦点窗口给InputMonitor输入事件处理类。
这里最后会根据屏幕方法是否改变决定是否要更新窗口的config,如果要更新config的话,还可能会触发重新的测量,布局等操作。我们先看下updateOrientationFromAppTokensLocked这个方法怎么判断屏幕方向改变的:
```java
boolean updateOrientationFromAppTokensLocked(boolean inTransaction, int displayId) {
long ident = Binder.clearCallingIdentity();
try {
// 获取指定的displayContent
final DisplayContent dc = mRoot.getDisplayContent(displayId);
// 获取屏幕的方向
final int req = dc.getOrientation();
// 如果获取当前屏幕的方向和上一次方向不同
if (req != dc.getLastOrientation()) {
// 设置最后一次屏幕的方向
dc.setLastOrientation(req);
// 如果是默认屏幕
if (dc.isDefaultDisplay) {
// 设置当前屏幕方向给policy
mPolicy.setCurrentOrientationLw(req);
}
// 更新屏幕的方向成功返回true
if (dc.updateRotationUnchecked(inTransaction)) {
// changed
return true;
}
}
return false;
} finally {
Binder.restoreCallingIdentity(ident);
}
}
```
这个方法获取当前屏幕的方法,然后和屏幕中保存的上一次的方向相比较,如果不同的话,说明屏幕方法有改变,这里会调用WindowManagerPolicy的setCurrentOrientationLw方法来更新最新的屏幕方法:
```java
public void setCurrentOrientationLw(int newOrientation) {
synchronized (mLock) {
// 如果新的屏幕方向和当前的屏幕方向不一致
if (newOrientation != mCurrentAppOrientation) {
// 更新Policy中的屏幕方向
mCurrentAppOrientation = newOrientation;
updateOrientationListenerLp();
}
}
}
```
这个主要就是把新的屏幕方向保存在WindowManagerPolicy中,WindowManagerPolicy的实现类是PhoneWindowManager。我们回到前面updateOrientationFromAppTokensLocked,最后还会调用DisplayContent的updateRotationUnchecked方法来更新屏幕,这个方法里面涉及到的更新模块还是比较多了,除了在DisplayContent保存相关的数据外,还会调用DisplayManager来更新显示模块的属性,我们这里不跟这么深,这里主要看一下,在更新屏幕的过程中会有冻屏的操作:
```java
boolean updateRotationUnchecked(boolean inTransaction) {
.............
mService.startFreezingDisplayLocked(inTransaction, anim[0], anim[1], this);
...........
}
```
这个方法主要更新屏幕方向的属性,我们不具体分析和显示相关的操作了,主要看下冻屏做了一些什么,所谓冻屏,简单的说就是在显示配置变化的过程中,让屏幕不能操作,就是所谓的冻屏。我们看下WMS的startFreezingDisplayLocked方法:
```java
// 冻结屏幕,主要就是停止接受输入和动画,然后截屏显示
void startFreezingDisplayLocked(boolean inTransaction, int exitAnim, int enterAnim,
DisplayContent displayContent) {
// 已经冻结屏幕了,return
if (mDisplayFrozen) {
return;
}
// 如果屏幕还没准备好或者没有亮屏状态,return
if (!displayContent.isReady() || !mPolicy.isScreenOn()) {
// No need to freeze the screen before the display is ready, system is ready, or
// if
// the screen is off.
return;
}
if (DEBUG_ORIENTATION)
Slog.d(TAG_WM,
"startFreezingDisplayLocked: inTransaction=" + inTransaction
+ " exitAnim=" + exitAnim + " enterAnim=" + enterAnim
+ " called by " + Debug.getCallers(8));
// 防止睡眠
mScreenFrozenLock.acquire();
// 设置冻屏状态
mDisplayFrozen = true;
mDisplayFreezeTime = SystemClock.elapsedRealtime();
mLastFinishedFreezeSource = null;
// {@link mDisplayFrozen} prevents us from freezing on multiple displays at the
// same time.
// As a result, we only track the display that has initially froze the screen.
// 冻屏的DisplayId
mFrozenDisplayId = displayContent.getDisplayId();
// 输入处理的冻结
mInputMonitor.freezeInputDispatchingLw();
// Clear the last input window -- that is just used for
// clean transitions between IMEs, and if we are freezing
// the screen then the whole world is changing behind the scenes.
mPolicy.setLastInputMethodWindowLw(null, null);
// 如果有设置过渡动画的话
if (mAppTransition.isTransitionSet()) {
// 停止当前的动画
mAppTransition.freeze();
}
if (PROFILE_ORIENTATION) {
File file = new File("/data/system/frozen");
Debug.startMethodTracing(file.toString(), 8 * 1024 * 1024);
}
// 设置冻屏时候的旋转退出,然后创建一个截屏显示
if (CUSTOM_SCREEN_ROTATION) {
mExitAnimId = exitAnim;
mEnterAnimId = enterAnim;
// 获取当前的旋转动画
ScreenRotationAnimation screenRotationAnimation = mAnimator
.getScreenRotationAnimationLocked(mFrozenDisplayId);
// 结束当前旋转动画
if (screenRotationAnimation != null) {
screenRotationAnimation.kill();
}
// Check whether the current screen contains any secure content.
boolean isSecure = displayContent.hasSecureWindowOnScreen();
// TODO(multidisplay): rotation on main screen only.
displayContent.updateDisplayInfo();
// 创建新的旋转动画,这里面会有截屏
screenRotationAnimation = new ScreenRotationAnimation(mContext, displayContent,
mFxSession, inTransaction, mPolicy.isDefaultOrientationForced(), isSecure,
this);
// 设置旋转动画。这里其实会创建一个surface,截取屏幕,然后显示的是这个截屏
mAnimator.setScreenRotationAnimationLocked(mFrozenDisplayId,
screenRotationAnimation);
}
}
```
这个方法主要是在更新一个显示配置前做的一些东西。这里可以看到会设置冻屏的状态值mDisplayFrozen为true,调用InputMonitor的freezeInputDispatchingLw方法,这个方法会停止输入事件的分发。调用AppTransition的freeze方法,这个方法会停止当前的转场动画,最后还会定义旋转时候相关的动画。这个方法主要就是在屏幕旋转前做一些初始化。
我们就大概看下更新旋转屏幕就可以了,我们还是回到WMS的addWindow方法,最后如果屏幕旋转有更新,会调用sendNewConfiguration方法,这个方法还会通知下AMS中的更新,最后会调用屏幕中窗口的测量,布局方法计算窗口的属性值。我们稍稍看一眼这个方法:
```java
void sendNewConfiguration(int displayId) {
try {
// 更新AMS中的全局config或者三个config
final boolean configUpdated = mActivityManager.updateDisplayOverrideConfiguration(
null /* values */, displayId);
if (!configUpdated) { // 如果没有需要更新,需要执行performSurfacePlacement方法
// 这个方法解冻那些被冻结的屏幕,比如屏幕在旋转前就会被冻结,比如选择180度,就不需要更新
// 但是config有变化,所以屏幕是会被冻结的,所以performSurfacePlacement会解冻
// Something changed (E.g. device rotation), but no configuration update is
// needed.
// E.g. changing device rotation by 180 degrees. Go ahead and perform surface
// placement to unfreeze the display since we froze it when the rotation was
// updated
// in DisplayContent#updateRotationUnchecked.
synchronized (mWindowMap) {
if (mWaitingForConfig) {
mWaitingForConfig = false;
mLastFinishedFreezeSource = "config-unchanged";
// 获取屏幕
final DisplayContent dc = mRoot.getDisplayContent(displayId);
if (dc != null) {
// 设置下标记
dc.setLayoutNeeded();
}
// 执行surface重新摆放的操作
mWindowPlacerLocked.performSurfacePlacement();
}
}
}
} catch (RemoteException e) {
}
}
```
这个方法开始会通知AMS更新config,如果返回结果如果是false说明,当前更新的窗口可能不在AMS中,获取更新了但是不需要重启,所以这里最后会调用WindowSurfacePlacer的performSurfacePlacement,这个方法一方面对于前面的冻屏等操作会重新恢复,另一方面会重新测量窗口的大小,其中核心的方法还会调用到我们前面遗留的那个performLayout测量布局方法,所以这里我们就不跟进了,后面我们还是主要会分析一下performLayout方法。
到这次WMS的addWindow方法就都讲完了,有些分析的比较详细有些分析的比较简单,总的来说WMS的addWindow方法会把相关窗口的数据结构保存下来,然后会计算窗口之间的层序关系,之后还会测量窗口的大小,这样系统所有窗口的状态WMS就比较清楚了。
这个方法真的是非常非常的长啊,我们还记得是从哪里调用过来的话?是从ViewRootImpl的setView方法,[WMS(四)之Acivity启动流程三](https://liqi.site/archives/wms四之acivity启动流程三#FXzkGmRX)在这篇文章中跳到WMS的,我们下来回到ViewRootImpl中,继续当初的代码:
# 回到ViewRootImpl
```java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...............
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
.............
// 下面把WMS中计算的各个区域赋值给客户端
if (mTranslator != null) {
// 根据mContentInsets的值,缩放显示的内容到mContentInsets这个区域
mTranslator.translateRectInScreenToAppWindow(mAttachInfo.mContentInsets);
}
// 重置过扫描区域
mPendingOverscanInsets.set(0, 0, 0, 0);
// 用户区域
mPendingContentInsets.set(mAttachInfo.mContentInsets);
// 重置状态栏和导航栏的常占区域
mPendingStableInsets.set(mAttachInfo.mStableInsets);
// 除去输入法,状态栏等遮挡区域
mPendingVisibleInsets.set(0, 0, 0, 0);
// 看看返回结果中是否有一直显示导航栏
mAttachInfo.mAlwaysConsumeANavBar =
(res & WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_NAV_BAR) != 0;
mPendingAlwaysConsumeNavBar = mAttachInfo.mAlwaysConsumeNavBar;
..................
}
```
上面截取的代码开始的res就是WMS的addWindow方法返回的结果,如果返回值是WindowManagerGlobal.ADD_OKAY说明添加窗口成功,反之说明添加失败,这个失败的处理我们后面会看到,这里先提一下。
接着Translator是一个对屏幕做适配的类,比如对窗口打大小会进行缩放使符合当前屏幕的显示,这里会调用translateRectInScreenToAppWindow方法对用户内容区域AttachInfo的mContentInsets进行缩放,下面会把在WMS中计算的insets保存到客户端这里,关于几个Insets我们之前也有介绍过。这里过扫描区域mPendingOverscanInsets,也就是屏幕四周的黑边,初始化清零。用户内容区域mPendingContentInsets,也就是上面进行过适配的AttachInfo的mContentInsets。状态栏和导航栏的占用区域mPendingStableInsets,这个的值也是从AttachInfo的mStableInsets中获取。可见区域mPendingVisibleInsets,也就是用户区域中如果有输入法导航栏等遮挡住了,那么剩下没有遮挡的区域。最后mPendingAlwaysConsumeNavBar表示是否要显示导航栏。注意的是这里的AttachInfo中的mContentInsets和mStableInsets,这些值都是前面WMS中的addWindow方法中获取的,系统进程获取这些值后再返回给这里的客户端的。
# WMS添加错误处理
上面这部分代码主要是初始化了一些insets部分的值,我们继续看下面的代码:
```java
................
// 前面addToDisplay方法添加窗口成功会发回0,即ADD_OKAY
// 如果添加失败返回小于0,执行下面的if分支
if (res < WindowManagerGlobal.ADD_OKAY) {
// 重置这个view的根view为null
mAttachInfo.mRootView = null;
// 重置view添加为false
mAdded = false;
// 这个是回调触摸方法的函数,设置对应的view为null
mFallbackEventHandler.setView(null);
// 移除view在Handler中的准备的执行
unscheduleTraversals();
// 重置和无障碍相关的
setAccessibilityFocus(null, null);
// 根据res错误类型,抛出错误异常
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
case WindowManagerGlobal.ADD_APP_EXITING:
throw new WindowManager.BadTokenException(
"Unable to add window -- app for token " + attrs.token
+ " is exiting");
case WindowManagerGlobal.ADD_DUPLICATE_ADD:
throw new WindowManager.BadTokenException(
"Unable to add window -- window " + mWindow
+ " has already been added");
case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
// Silently ignore -- we would have just removed it
// right away, anyway.
return;
case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- another window of type "
+ mWindowAttributes.type + " already exists");
case WindowManagerGlobal.ADD_PERMISSION_DENIED:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- permission denied for window type "
+ mWindowAttributes.type);
case WindowManagerGlobal.ADD_INVALID_DISPLAY:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified display can not be found");
case WindowManagerGlobal.ADD_INVALID_TYPE:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified window type "
+ mWindowAttributes.type + " is not valid");
}
throw new RuntimeException(
"Unable to add window -- unknown error code " + res);
}
// 如果view是decorView,只有DecorView继承了RootViewSurfaceTaker
if (view instanceof RootViewSurfaceTaker) {
// 从DecorView中获取RootViewSurfaceTaker的实现
mInputQueueCallback =
((RootViewSurfaceTaker)view).willYouTakeTheInputQueue();
}
// 输入事件中介类非null
if (mInputChannel != null) {
// 输入事件处理的回调类也非null
if (mInputQueueCallback != null) {
// 创建输入事件队列
mInputQueue = new InputQueue();
// 设置输入事件队列给NativieActivity
mInputQueueCallback.onInputQueueCreated(mInputQueue);
}
// 输入事件接收器创建,把输入事件Channel和当前线程looper传入
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
}
.....................
```
这部分代码开始就是处理前面说的WMS的addWindow方法的返回值,如果小于0的话,说明WMS那里添加失败,这里会对失败的情况做处理。开始会把一些相关的变量都重置了,比如AttachInfo的mRootView置为null,mAdded置为false等等,之后就根据不同的错误抛出异常,这些也没什么好多说的,我们接着看下面代码。
DecorView类是RootViewSurfaceTaker的子类,这里如果是RootViewSurfaceTaker类中的话,会获取一个InputQueue.Callback类,这个是是一个输入事件队列InputQueue和一个线程联系获取断开时候的回调结构,通过这个接口,窗口就可以和输入事件模块通信了,所以这里会从DecorView中取出这个对象,调用的方法是DecorView的willYouTakeTheInputQueue方法:
```java
/** The feature ID of the panel, or -1 if this is the application's DecorView */
private final int mFeatureId;
public InputQueue.Callback willYouTakeTheInputQueue() {
return mFeatureId < 0 ? mWindow.mTakeInputQueueCallback : null;
}
```
这里可以看到只有mFeatureId小于0的话,才会返回这个类,而只有DecorView的mFeatureId才是小于0的,所以这个Callback只有DecorView才是直接和输入事件通信的,我们回到前面方法。
之后如果InputQueue.Callback如果非空的话,会创建InputQueue,把InputQueue设置给InputQueue.Callback,这样InputQueue.Callback就可以接收到输入事件消息了,最后还会创建WindowInputEventReceiver,这个类作为view和底层inputFlinger通信的一个事件接受分发类,启到了通信的作用,这里我们对于事件模块不过多展开,了解一下即可。我们回到ViewRootImpl的setView方法,再看最后一段代码:
```java
// 设置添加view的父view是this
view.assignParent(this);
// 是否触摸模式
mAddedTouchMode = (res & WindowManagerGlobal.ADD_FLAG_IN_TOUCH_MODE) != 0;
// 窗口是否可见
mAppVisible = (res & WindowManagerGlobal.ADD_FLAG_APP_VISIBLE) != 0;
// 下面是无障碍的一些设置
if (mAccessibilityManager.isEnabled()) {
mAccessibilityInteractionConnectionManager.ensureConnection();
}
if (view.getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
// 下面是InputStage相关,输入事件处理不同阶段,采用责任链模式
// Set up the input pipeline.
CharSequence counterSuffix = attrs.getTitle();
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
mFirstInputStage = nativePreImeStage;
mFirstPostImeInputStage = earlyPostImeStage;
mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;
```
这是setView方法的最后一部分代码了,这部分代码注意还是会初始化一些剩余的内容。首先这里会调用View的assignParent方法,我们知道每个窗口都有一个ViewRootImpl,他是操作这个窗口添加的View的,这里assignParent方法就是把ViewRootImpl设置给这个View,我们看一眼这个方法:
```java
void assignParent(ViewParent parent) {
// 如果当前view没有mParent,设置
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
// 如果参数是null,那么当前view的mParent是null
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}
```
这里参数就是ViewRootImpl,把ViewRootImpl设置给View的mParent字段,方法很简单,我们回到前面。
之后会根据从WMS方法返回的结果,赋值给ViewRootImpl,表示是否可操作以及是否可见。之后再初始化一些无障碍相关设置,以及初始化InputStage,InputStage主要是可以处理输入事件不同阶段的类,这里的初始化采用的责任链的设计模式,有关输入事件模块我们这里就不深入分析,后面会有专门的专题文章来分析。
# 流程结束
至此ViewRootImpl方法也都走完了,整个窗口的添加流程也就都走完了。从我们开始分析到现在,我们可以看到WMS主要是做了再最终绘制窗口前的准备动作,比如判断这个窗口是不是有权限被显示,如果可以正常显示的话,他和他的父子窗口之间的一些关系是怎么样的,这个比较典型的就是和AMS中ActivityStack,TaskRecord等联系起来了。除了窗口本身之间有父子层级关系,对于用户来说,还有显示的层级关系,也就是在屏幕Z轴上的顺序,在最上面的窗口也就是被用户看到的窗口,最上面的窗口也就是焦点窗口,一旦焦点窗口有改变,那么在屏幕Z轴上窗口的层级关系也会有变化,WMS也会对这些变化做出调整。所有这一切都完成后,就会交给具体的View去处理了,而View的处理,如果熟悉的同学可能也知道,主要是测量,布局和绘制过程,关于View我们在后面也会分析。但是在交给View处理之前,其实窗口也有个测量布局的过程,所谓窗口的测量和布局,就是根据屏幕的尺寸获得每个窗口的大小,从前面WMS的分析中我们可以知道,除了我们自己开发的应用有窗口,系统中还存在着很多系统级别的窗口,比如状态栏,比如导航栏,在非全屏的状态下,我们自己的Activity一般会排列在这些系统窗口的中间的,这个过程其实就是窗口的测量和布局的过程。我们前面讲了这么多WMS的分析,主要是从WMS对窗口的管理方面,以及窗口之间的层级关系来讲的,最后我们就再来看下和窗口测量布局有关地方,其实这部分的代码主要和View结合的比较紧密了,但是我们这里主要先把其中和窗口有关系拿出来看一下,理解了窗口的测量和布局,对后面理解View也是有帮助的。记得前面我们分析的时候有看到过performLayout这个方法吗,这个方法是在updateFocusedWindowLocked方法中调用的,一般来说,只要焦点窗口有变化,都会调用updateFocusedWindowLocked方法来更新窗口,这个方法我们前面也看过了,简单说就是对窗口的测量和布局,还会调整Z轴上窗口序列,其中测量布局就是调用performLayout这个方法来处理的,当然这里需要提醒一下,这个performLayout是在DisplayContent类中的performLayout,在ViewRootImpl中也有个同名的方法,那个是对View进行布局的,我们现在主要对窗口进行测量和布局,所以会看下DisplayContent的performLayout方法。这个方法引申出去的内容也不少,我们会在下一篇文章开始来分析,这个文章最后我们把从ViewRootImpl的setView方法开始的流程图给画一下,加深一下理解。

WMS(五)之Acivity启动流程四