本篇基于Android Q代码
根据AppWindowToken和WindowToken的添加流程和排序规则我们知道Android细分了四大窗口容器,分别是存储输入法相关的mImeWindowsContainers,存储系统窗口的mAboveAppWindowsContainers,存储应用窗口的TaskStackContainers,存储壁纸窗口的mBelowAppWindowsContainers,输入法和壁纸为何特殊,我们接下来将对输入法窗口的添加策略进行分析,
输入法类型窗口在向WMS添加之前必须显式创建自己的WindowToken,并提前添加到mImeWindowsContainers中,Android系统在开机时会启动输入法服务(InputMethodManagerService),在InputMethodManagerService的systemRunning方法中调用startInputInnerLocked方法,此方法中会给输入法创建
token,我们可以看到,其实token就是一个简单的Binder对象,然后在WMS addWindowToken方法会使用token创建WindowToken,在WindowToken构造方法中最终调到DisplayContent的addWindowToken方法,将输入法对应的WindowToken添加到mImeWindowsContainers中,关于WindowToken相关添加流程在AppWindowToken和WindowToken的添加流程和排序规则有非常详细的分析
InputBindResult startInputInnerLocked() {
......
mCurToken = new Binder();
try {
if (DEBUG) Slog.v(TAG, "Adding window token: " + mCurToken);
mIWindowManager.addWindowToken(mCurToken, TYPE_INPUT_METHOD, DEFAULT_DISPLAY);
} catch (RemoteException e) {
}
......
return null;
}
输入法这种窗口非常特殊,它永远会在需要使用输入法的窗口的上面,感觉像个子窗口,并且输入法窗口一旦第一次添加之后一般不会销毁,当我们某个应用需要使用输入法时不需要重新创建窗口,只要计算出是哪个应用,然后移动输入法窗口的堆栈位置到此窗口上面
我们具体来看看WMS是如何完成对输入法的添加策略的,输入法窗口添加有两种情况:
一是输入法窗口第一次通过WMS.addWindow方法添加到目标窗口时需要找到满足输入法添加条件的目标窗口
二是输入法窗口已经创建之后,系统Window位置调整即调用WMS的relayoutWindow方法之后,输入法位置也会调整
先看一的情况:
WMS.addWindow
public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
......
//输入法必须提前添加WindowToken的检查就在这里
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
final int rootType = hasParent ? parentWindow.mAttrs.type : type;
//如果token为空并且是输入法窗口是不允许的
if (token == null) {
......
if (rootType == TYPE_INPUT_METHOD) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
//创建输入法的WindowState
final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], seq, attrs, viewVisibility, session.mUid,
session.mCanAddInternalSystemWindow);
......
//是否需要移动输入法窗口的位置,默认需要
boolean imMayMove = true;
win.mToken.addWindow(win);
//如果是输入法窗口
if (type == TYPE_INPUT_METHOD) {
displayContent.setInputMethodWindowLocked(win);
imMayMove = false;
//如果是输入法对话框
} else if (type == TYPE_INPUT_METHOD_DIALOG) {
displayContent.computeImeTarget(true /* updateImeTarget */);
imMayMove = false;
}
}
}
要注意输入法窗口的WindowToken是在系统启动的时候添加的,但是WindowState是在输入法窗口第一次需要添加的时候才创建的,所以在刚开机在Launcher界面dump窗口信息会发现mImeWindowsContainers 只有WindowToken,而WindowToken下并没有WindowState
ROOT type=undefined mode=fullscreen
#0 Display 0 name="Built-in screen" type=undefined mode=fullscreen
#3 mImeWindowsContainers type=undefined mode=fullscreen
#0 WindowToken{f127d64 android.os.Binder@d21eaf7} type=undefined mode=fullscreen
void setInputMethodWindowLocked(WindowState win) {
mInputMethodWindow = win;
// Update display configuration for IME process.
if (mInputMethodWindow != null) {
final int imePid = mInputMethodWindow.mSession.mPid;
mWmService.mAtmInternal.onImeWindowSetOnDisplay(imePid,
mInputMethodWindow.getDisplayId());
}
//计算可以显示输入法窗口的目标窗口,true代表需要更新输入法窗口
computeImeTarget(true /* updateImeTarget */);
mInsetsStateController.getSourceProvider(TYPE_IME).setWindow(win,
null /* frameProvider */);
}
这个方法会被多次调用以精确计算出可以添加输入法的目标窗口
WindowState computeImeTarget(boolean updateImeTarget) {
//mInputMethodWindow不为空
if (mInputMethodWindow == null) {
if (updateImeTarget) {
if (DEBUG_INPUT_METHOD) Slog.w(TAG_WM, "Moving IM target from "
+ mInputMethodTarget + " to null since mInputMethodWindow is null");
setInputMethodTarget(null, mInputMethodTargetWaitingAnim);
}
return null;
}
//mInputMethodTarget等于当前输入法窗口所在的窗口
final WindowState curTarget = mInputMethodTarget;
//这个方法就是对mDeferUpdateImeTargetCount==0的判断
//mDeferUpdateImeTargetCount在调用deferUpdateImeTarget方法
//后自增1,看注释是延迟显示输入法的作用
if (!canUpdateImeTarget()) {
return curTarget;
}
mUpdateImeTarget = updateImeTarget;
//寻找可以添加输入法的目标窗口,通过mComputeImeTargetPredicate规则
//mComputeImeTargetPredicate是一个java8新增的Predicate类型
WindowState target = getWindow(mComputeImeTargetPredicate);
//后面代码等下分析的时候再贴出来
.....
}
我们先来看下输入法的目标窗口的查找规则 ,getWindow是DisplayContent父类WindowContainer的方法,DisplayContent并没有覆盖
WindowState getWindow(Predicate<WindowState> callback) {
for (int i = mChildren.size() - 1; i >= 0; --i) {
final WindowState w = mChildren.get(i).getWindow(callback);
if (w != null) {
return w;
}
}
return null;
}
我们看下这个方法,遍历mChildren,每个元素都调用getWindow方法,并且注意它是从mChildren尾部窗口开始的,什么意思呢?
在AppWindowToken和WindowToken的添加流程和排序规则我们分析过,DisplayContent的mChildren里放的是四大窗口容器,这四大容器添加规则就是越尾部的窗口Z-order越高,说明我们只需要找到Z-order最高的那个满足添加输入法条件的窗口就行了,因为输入法窗口总是给用户使用的,肯定需要出现在当前层级最高的窗口上啊,接着这四大窗口容器也会遍历自己内部的mChildren,再调用getWindow方法,这样一层一层的调用最终会调用到WindowState的getWindow,而且是系统所有WindowState都会调用,这样就实现了对系统所有窗口的遍历查找,非常的巧妙,而且我们可以去看源码,WindowContainer的所有子类,只有WindowState的对getWindow方法有具体实现。
getWindow方法参数接收的是一个Predicate,这是java8新增的类,类似断言assert的作用,将Predicate传递给每个WindowState,Predicate的test方法会将传入test的参数传递到定义的Predicate表达式中,即看WindowState是否满足canBeImeTarget的条件
private final Predicate<WindowState> mComputeImeTargetPredicate = w -> {
return w.canBeImeTarget();
};
看下WindowState的getWindow方法具体实现,test方法为true则代表找到了canBeImeTarget为true的窗口,此窗口即是能够显示输入法的窗口,则返回此窗口
WindowState getWindow(Predicate<WindowState> callback) {
//WindowState的mChildren存储的是子窗口的WindowState
//为空代表没有子窗口,则直接对当前窗口进行test判断
if (mChildren.isEmpty()) {
return callback.test(this) ? this : null;
}
int i = mChildren.size() - 1;
WindowState child = mChildren.get(i);
//对mSubLayer大于0的子窗口进行遍历,寻找满足canBeImeTarget的
//目标子窗口
while (i >= 0 && child.mSubLayer >= 0) {
if (callback.test(child)) {
return child;
}
--i;
if (i < 0) {
break;
}
child = mChildren.get(i);
}
//有子窗口的情况,对当前父窗口进行test判断
if (callback.test(this)) {
return this;
}
//如果mSubLayer大于0的子窗口和父窗口都不满足test条件则就对
//所有子窗口进行判断,包括mSubLayer小于0的
while (i >= 0) {
if (callback.test(child)) {
return child;
}
--i;
if (i < 0) {
break;
}
child = mChildren.get(i);
}
return null;
}
继续看WindowState的canBeImeTarget方法
boolean canBeImeTarget() {
//如果调用test方法的窗口也是输入法窗口直接返回false,不允许的
if (mIsImWindow) {
return false;
}
//如果当前窗口是应用类型窗口并且没有获得焦点则windowsAreFocusable为false
//否则为true,系统不会让应用在没有焦点时弹输入法,这很危险
//如果当前窗口是系统窗口则不管是否获得焦点windowsAreFocusable都为true
final boolean windowsAreFocusable = mAppToken == null || mAppToken.windowsAreFocusable();
if (!windowsAreFocusable) {
return false;
}
//FLAG_NOT_FOCUSABLE和FLAG_ALT_FOCUSABLE_IM按位或之后
//再和当前窗口的flags进行按位与
final int fl = mAttrs.flags & (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM);
final int type = mAttrs.type;
//fl != (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM)这段逻辑我
//没怎么看明白就不解释了,总之满足这三个条件则该窗口不会作为输入法
//目标窗口
if (fl != 0 && fl != (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM)
&& type != TYPE_APPLICATION_STARTING) {
return false;
}
//通过窗口是否可见或者是否添加判断是否可以弹出输入法
return isVisibleOrAdding();
}
这几个条件是:(存在Surface ||(没有调用relayoutWindow重新更新窗口布局 && 当前窗口是可见的)) && 确保设置了所有策略可见性位 && 父窗口没有隐藏 && (AppWindowToken不为空时需要确保该应用没被隐藏) && 没有执行窗口退出动画 && 没被销毁,其实这些判断都是确认当前窗口是否可见,如果是应用类型窗口还必须要求获取了焦点,这点是在上面canBeImeTarget里判断的
boolean isVisibleOrAdding() {
final AppWindowToken atoken = mAppToken;
return (mHasSurface || (!mRelayoutCalled && mViewVisibility == View.VISIBLE))
&& isVisibleByPolicy() && !isParentWindowHidden()
&& (atoken == null || !atoken.hiddenRequested)
&& !mAnimatingExit && !mDestroying;
通过getWindow方法最终就获取到了Z-order最高的并且可见的窗口,如果是应用类型窗口则获取到的是Z-order最高并且可见并且有焦点
再回到computeImeTarget方法接着往下看
WindowState computeImeTarget(boolean updateImeTarget) {
//mInputMethodWindow不为空
if (mInputMethodWindow == null) {
if (updateImeTarget) {
setInputMethodTarget(null, mInputMethodTargetWaitingAnim);
}
return null;
}
//mInputMethodTarget等于当前输入法窗口所在的窗口
final WindowState curTarget = mInputMethodTarget;
//这个方法就是对mDeferUpdateImeTargetCount==0的判断
//mDeferUpdateImeTargetCount在调用deferUpdateImeTarget方法
//后自增1,看注释是延迟显示输入法的作用
if (!canUpdateImeTarget()) {
return curTarget;
}
mUpdateImeTarget = updateImeTarget;
//寻找可以添加输入法的目标窗口,通过mComputeImeTargetPredicate规则
//mComputeImeTargetPredicate是一个java8新增的Predicate类型
WindowState target = getWindow(mComputeImeTargetPredicate);
//如果获取到了可以添加输入法的窗口并且窗口类型是启动窗口
if (target != null && target.mAttrs.type == TYPE_APPLICATION_STARTING) {
final AppWindowToken token = target.mAppToken;
if (token != null) {
//getImeTargetBelowWindow方法作用是判断如果当前启动窗口的
//AppWindowToken下有除了此启动窗口外的其他应用窗口,则将输入法目标
//窗口赋值给启动窗口前一个满足canBeImeTarget的窗口
final WindowState betterTarget = token.getImeTargetBelowWindow(target);
if (betterTarget != null) {
target = betterTarget;
}
}
}
//如果当前输入法的目标窗口不为空,并且还没被移除但是正在被移除的过程
//中,则不会给输入法寻找新窗口
if (curTarget != null && !curTarget.mRemoved && curTarget.isDisplayedLw()
&& curTarget.isClosing()) {
return curTarget;
}
//没找到可以添加输入法的目标窗口
if (target == null) {
//设置输入法目标窗口为空
setInputMethodTarget(null, mInputMethodTargetWaitingAnim);
}
return null;
}
if (updateImeTarget) {
AppWindowToken token = curTarget == null ? null : curTarget.mAppToken;
//如果当前输入法所在的目标窗口不为空,且为应用类型窗口
if (token != null) {
WindowState highestTarget = null;
//当前应用窗口正在等待执行动画或者正在执行动画
if (token.isSelfAnimating()) {
//getHighestAnimLayerWindow的作用是从这个isSelfAnimating
//的窗口开始遍历之前的所有窗口,找到第一个mRemoved为false的窗口
//作为输入法新的目标窗口
highestTarget = token.getHighestAnimLayerWindow(curTarget);
}
//如果能找到这么一个新窗口存在
if (highestTarget != null) {
//此窗口设置了切换动画,正准备执行
if (mAppTransition.isTransitionSet()) {
//将此窗口设置为输入法目标窗口,并且mInputMethodTargetWaitingAnim
//等于true,代表该窗口正在准备执行切换动画
//则推迟输入法窗口的添加
setInputMethodTarget(highestTarget, true);
return highestTarget;
}
}
}
//上面的特殊条件都没满足则给输入法设置目标窗口,并且
//mInputMethodTargetWaitingAnim为false
setInputMethodTarget(target, false);
}
return target;
}
computeImeTarget这个方法还是比较复杂的,主要是计算输入法窗口的目标窗口,总结一下:
通过DisplayContent的getWindow方法会遍历当前系统所有WindowState,都执行getWindow方法,目的是找出输入法的目标窗口,并且遍历的时候是从Z-order最高的窗口开始,一旦找到就不会在遍历了,判断输入法目标窗口的条件是调用WindowState的canBeImeTarget方法
通过getWindow获取到了输入法目标窗口之后,会对此窗口进行一些特殊性判断,(1)如果此窗口是应用启动窗口,则从此应用启动窗口开始遍历它之前的应用窗口,寻找到第一个满足canBeImeTarget的窗口作为输入法目标的新窗口,(2)如果当前输入法所在的窗口curTarget不为空,并且还没被移除但是正在被移除的过程中并且是可见的,则不会继续给输入法寻找新窗口,(3)如果找到的输入法目标窗口是应用窗口,并且正在等待执行切换动画或者正在执行动画,则此窗口不能再作为输入法目标窗口,而是调用getHighestAnimLayerWindow方法寻找新的目标窗口,规则不重复了,代码中写了注释,并且这个新的目标窗口已经设置了动画准备切换则通过setInputMethodTarget方法真正将此窗口设置为输入法目标窗口,并将mInputMethodTargetWaitingAnim赋值为true
如果通过getWindow方法找到的输入法目标窗口不是2描述的特殊窗口则调用setInputMethodTarget方法真正给输入法设置目标窗口,mInputMethodTargetWaitingAnim为false
输入法的目标窗口已经找到,接着调用setInputMethodTarget方法将输入法设置给目标窗口
private void setInputMethodTarget(WindowState target, boolean targetWaitingAnim) {
//如果当前要给输入法设置的目标窗口就是上一个输入法窗口,并且
//mInputMethodTargetWaitingAnim也没有变化则直接return
if (target == mInputMethodTarget && mInputMethodTargetWaitingAnim == targetWaitingAnim) {
return;
}
mInputMethodTarget = target;
mInputMethodTargetWaitingAnim = targetWaitingAnim;
//setLayoutNeeded为false
assignWindowLayers(false /* setLayoutNeeded */);
mInsetsStateController.onImeTargetChanged(target);
updateImeParent();
}
void assignWindowLayers(boolean setLayoutNeeded) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "assignWindowLayers");
//getPendingTransaction()处理动画相关
assignChildLayers(getPendingTransaction());
if (setLayoutNeeded) {
setLayoutNeeded();
}
//准备开始一个动画
scheduleAnimation();
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
@Override
void assignChildLayers(SurfaceControl.Transaction t) {
//设置三大窗口容器的Layer为0,1,2
mBelowAppWindowsContainers.assignLayer(t, 0);
mTaskStackContainers.assignLayer(t, 1);
mAboveAppWindowsContainers.assignLayer(t, 2);
final WindowState imeTarget = mInputMethodTarget;
boolean needAssignIme = true;
//如果输入法目标窗口不为空并且非(分屏模式或者imeTarget没有在执行动画)
//并且imeTarget的SurfaceControl不为空
if (imeTarget != null && !(imeTarget.inSplitScreenWindowingMode()
|| imeTarget.mToken.isAppAnimating())
&& (imeTarget.getSurfaceControl() != null)) {
//则对输入法系列窗口容器执行assignRelativeLayer,这个方法
//就是将输入法窗口层级调整到输入法目标窗口之上,这里调整的是
//SurfaceFlinger里面的Layer层级
mImeWindowsContainers.assignRelativeLayer(t, imeTarget.getSurfaceControl(),1);
//不需要再对输入法进行调整
needAssignIme = false;
}
//下面这四个容器都分别调用assignChildLayers方法,对自己内部所有窗口
//都会调整Layer,这里的Layer和Z-order是对应的,Layer最终是设置到
//SurfaceFlinger中去,就不去细看了
mBelowAppWindowsContainers.assignChildLayers(t);
mTaskStackContainers.assignChildLayers(t);
mAboveAppWindowsContainers.assignChildLayers(t,
needAssignIme == true ? mImeWindowsContainers : null);
mImeWindowsContainers.assignChildLayers(t);
}
到这里我们对输入法窗口向目标窗口的添加已经分析的差不多了,核心方法就是computeImeTarget,思路其实就是输入法窗口根据一定规则找到目标窗口,接着将输入法窗口容器assign到目标窗口的上面,上面分析的添加输入法窗口的情况是直接通过WMS.addWindow添加一个类型为TYPE_INPUT_METHOD的窗口,还有可能会添加一个不是输入法类型窗口,并且此窗口可以接收事件canReceiveKeys方法返回true,可以接收事件则焦点就可能发生变化,焦点变化之后输入法窗口就会重新调整位置,重新计算输入法目标窗口,源码就不详细分析了,贴一个调用流程:
WMS.addWindow -> WMS.updateFocusedWindowLocked -> RootWindowContainer.updateFocusedWindowLocked -> DisplayContent.updateFocusedWindowLocked -> DisplayContent.computeImeTarget
最终调用computeImeTarget方法重新计算输入法窗口的目标窗口
其实还有一种情况也会引起输入法目标窗口的重新计算,当WMS调用relayoutWindow对窗口重新布局的时候,但是最终都是调用的computeImeTarget来计算目标窗口,只是调用条件不同而已,我们就不再进行分析了。