在前一篇文章中我们分析到Activity启动过程中添加窗口,最后执行到ViewRootImpl类的setView方法,这篇文章我们接着这个方法继续往下进行分析。这个方法非常的长,我们分几部分来看:
# ViewRootImpl的setView方法
```java
// 参数一 view, 要添加的view
// 参数二 attrs,要加载的窗口的相关参数
// 参数三 panelParentView,view要添加到的父容器
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...............
synchronized (this) {
// 如果这个ViewRootImpl还没有对应的view
if (mView == null) {
mView = view; // 赋值view,比如ACtivity的DecorView
mAttachInfo.mDisplayState = mDisplay.getState(); // 获取屏幕状态
// 注册屏幕状态变化时候的监听,会刷新view
mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
// view的屏幕方向
mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
// 把当前view设置给处理按键的类mFallbackEventHandler
mFallbackEventHandler.setView(view);
mWindowAttributes.copyFrom(attrs); // 把传入的布局参数赋值给mWindowAttributes
if (mWindowAttributes.packageName == null) { // 没包名的话赋值包名
mWindowAttributes.packageName = mBasePackageName;
}
attrs = mWindowAttributes; // 重新赋值回attrs
setTag();
if (DEBUG_KEEP_SCREEN_ON && (mClientWindowLayoutFlags
& WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0
&& (attrs.flags&WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) == 0) {
Slog.d(mTag, "setView: FLAG_KEEP_SCREEN_ON changed from true to false!");
}
// Keep track of the actual window flags supplied by the client.
// 窗口相关的一些flag
mClientWindowLayoutFlags = attrs.flags;
// 设置无障碍
setAccessibilityFocus(null, null);
// 如果是一个DecorView,添加绘制时候触发的回调接口
if (view instanceof RootViewSurfaceTaker) {
// 获取一个回调函数surfaceHold的Callback2
// 这个接口是view需要第一次绘制以及重绘时调用的
mSurfaceHolderCallback =
((RootViewSurfaceTaker)view).willYouTakeTheSurface();
if (mSurfaceHolderCallback != null) {
mSurfaceHolder = new TakenSurfaceHolder();
mSurfaceHolder.setFormat(PixelFormat.UNKNOWN);
mSurfaceHolder.addCallback(mSurfaceHolderCallback);
}
}
// Compute surface insets required to draw at specified Z value.
// TODO: Use real shadow insets for a constant max Z.
// 如果surface的大小没有手动设置过,如果没有设置过的话,会根据view来设置
if (!attrs.hasManualSurfaceInsets) {
attrs.setSurfaceInsets(view, false /*manual*/, true /*preservePrevious*/);
}
...............
}
```
以上是这个方法的第一部分。这里首先获取当前这个显示屏幕的状态,这里的状态就是指亮屏,灭屏这种。接着会设置一个屏幕状态变化的监听mDisplayListener,我们可以稍稍看下这个方法:
```java
// 屏幕状态发生变化时候触发的回调方法
// 根据屏幕的状态,会触发绘制
private final DisplayListener mDisplayListener = new DisplayListener() {
@Override
public void onDisplayChanged(int displayId) {
// 如果当前这个view
if (mView != null && mDisplay.getDisplayId() == displayId) {
// 老的屏幕状态
final int oldDisplayState = mAttachInfo.mDisplayState;
// 新的屏幕状态
final int newDisplayState = mDisplay.getState();
// 新老屏幕状态不一致时
if (oldDisplayState != newDisplayState) {
// 更新mAttachInfo中屏幕状态为新的
mAttachInfo.mDisplayState = newDisplayState;
// 让cpu不要进入睡眠状态,下面可能会做事情
pokeDrawLockIfNeeded();
// 如果老的屏幕状态不是未知时
if (oldDisplayState != Display.STATE_UNKNOWN) {
// 看下老的屏幕状态是on还是off
final int oldScreenState = toViewScreenState(oldDisplayState);
// 看下新的屏幕状态是on还是off
final int newScreenState = toViewScreenState(newDisplayState);
if (oldScreenState != newScreenState) {
// 屏幕开关发生变化,调用view的dispatchScreenStateChanged
// 这个方法默认是空实现,我们可以自定义一些处理
mView.dispatchScreenStateChanged(newScreenState);
}
// 之前老的屏幕如果是关闭的话
if (oldDisplayState == Display.STATE_OFF) {
// Draw was suppressed so we need to for it to happen here.
// 这个置为true,表示需要将窗口的全部区域绘制
mFullRedrawNeeded = true;
scheduleTraversals();
}
}
}
}
}
```
我们虽然不会分析DisplayManager相关的内容,但是对有些方法可以看一下,大致了解他们做了些什么。这里会获取之前保存的屏幕状态和当前最新的状态,如果新老状态不一样的时候,说明发生变化了。最后我们看到如果老的状态是STATE_OFF是关闭状态,那么新的状态说明会显示,所以会调用scheduleTraversals方法,如果对View熟悉的同学会知道,这个方法会触发view的测量绘制等动作,所以会重新刷当前屏幕上的内容,这个方法我们后面还会看到,对于view来说是很重要的方法,不过我们在WMS的模块中不会分析这个方法,我们会专门在分析View的模块中在详细分析,当前我们知道这里方法会触发view的重新绘制就可以了,我们回到setView方法。
接着会获取view的方向以及按键的处理类。这里FallbackEventHandler是一个按键处理接口,他的实现类是PhoneFallbackEventHandler,他是在ViewRootImpl构造函数中初始化的。我们前面有分析过,一个窗口可能有PhoneWindow也可能没有,对于有PhoneWindow的窗口来说,他的按键处理会在PhoneWindow中处理,而那些没有PhoneWindow的窗口则会使用这里的PhoneFallbackEventHandler来处理按键事件。我们可以看到,虽然我们分析的是WMS模块,但是WMS模块中涉及到的其他模块真的是非常的多,比如这里的输入处理模块,这些模块我们这里也不详细分析,先知道一下就好,后面有时间会对输入事件这块在做专门分析。
接着会把前面传过来的LayoutParams参数赋给ViewRootImpl的mWindowAttributes变量,如果没有包名会加上包名。同样会把LayoutParams中窗口的flag赋给 mClientWindowLayoutFlags变量,在设置下无障碍相关的。接着如果是DecorView的话,会创建SurfaceHolder,SurfaceHolder是管理SurfaceView的类,我们知道Surface就是最终绘制给用户看的组件,这里创建SurfaceHolder就是管理这个ViewGroup中的SurfaceView,同样后面我们会专门来分析surface,这里知道下就可以了。
最后会设置SurfaceInsets,这里解释下类似这种xxxInsets的含义,一般来首这种xxxInsets都是一个Rect,表示的一个窗口四周需要留出来的边距,所以设置的时候都有上下左右四个值,所以他们用Rect来表示。这里可以简单的用下面这个图表示一下。

可以看到上图中间那个xxxInsets就是一个Rect,他的上下左右值代表了这个view和四种的距离,所以上面setView方法中会设置surfaceView的insets也就是绘制在屏幕上的时候,四周的边距,这里具体计算surface的过程同样也不展开,后面再说。我们看完了setView方法的第一部分,下面看第二部分:
```java
............
// 获取屏幕适配时候的适配数据
CompatibilityInfo compatibilityInfo =
mDisplay.getDisplayAdjustments().getCompatibilityInfo();
// 如果是适配模式,窗口需要缩放的话,mTranslator为非空
mTranslator = compatibilityInfo.getTranslator();
// If the application owns the surface, don't enable hardware acceleration
// 硬件加速相关处理
if (mSurfaceHolder == null) {
enableHardwareAcceleration(attrs);
}
boolean restore = false;
if (mTranslator != null) {
// surface中保存下屏幕适配时候缩放的大小
mSurface.setCompatibilityTranslator(mTranslator);
restore = true;
// 备份下这个窗口的相关属性,因为下面要对窗口属性进行缩放处理
attrs.backup();
// 根据前进传过来的窗口属性值,缩放窗口大小
mTranslator.translateWindowLayout(attrs);
}
if (DEBUG_LAYOUT) Log.d(mTag, "WindowLayout in setView:" + attrs);
// 如果不支持自动适配屏幕大小,false表示支持
if (!compatibilityInfo.supportsScreen()) {
// flag设置为自动适配屏幕大小
attrs.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
// 设置最后一次在适配屏幕模式中为true
mLastInCompatMode = true;
}
// 输入法模式
mSoftInputMode = attrs.softInputMode;
mWindowAttributesChanged = true;
mWindowAttributesChangesFlag = WindowManager.LayoutParams.EVERYTHING_CHANGED;
// 这里的view,如果是Activity的话,就是一个DecorView
mAttachInfo.mRootView = view;
// 如果mTranslator非空,表示有缩放的需求
mAttachInfo.mScalingRequired = mTranslator != null;
// 获取缩放的比例
mAttachInfo.mApplicationScale =
mTranslator == null ? 1.0f : mTranslator.applicationScale;
// 如果有父窗口
if (panelParentView != null) {
// 获取父窗口的token
mAttachInfo.mPanelParentWindowToken
= panelParentView.getApplicationWindowToken();
}
mAdded = true;
int res; /* = WindowManagerImpl.ADD_OKAY; */
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
// 触发view的三大方法
requestLayout();
// 如果窗口没有禁用输入事件,那么创建一个InputChannel
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
// 是否窗口的可见性取决于decorView,而忽略app设置的可见性
mForceDecorViewVisibility = (mWindowAttributes.privateFlags
& PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;
try {
// 窗口类型
mOrigWindowType = mWindowAttributes.type;
// 是否要重新计算全局的属性,比如窗口的属性
mAttachInfo.mRecomputeGlobalAttributes = true;
// 更新下保持亮屏,状态栏和导航栏显示状态,是否有状态栏和导航栏的监听方法
// 这3个属性,看看要加载的view是否有和窗口不同,如果有的话,除了更新view
// 的AttachInfo中这3个属性,还要更新到窗口的LayoutParams属性中
collectViewAttributes();
// 继续调用Session的addToDisplay,mWindow是W对象,mWindowAttributes是传入的布局参数
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mInputChannel = null;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
throw new RuntimeException("Adding window failed", e);
} finally {
if (restore) {
attrs.restore();
}
}
..................
```
上面是第二部分代码。首先之类会获取CompatibilityInfo,这里类的数据会通过PackageManagerService解析app的minifest中的信息来获取,比如像targetSdk这种配置就属于CompatibilityInfo的数据,这里获取CompatibilityInfo为了就是让当前的View适配屏幕的大小,接着从中获取窗口适配屏幕缩放值的大小,之后会根据缩放值更新下窗口LayoutParams中宽高等值,如果窗口支持自动适配屏幕的大小,给窗口加上PRIVATE_FLAG_COMPATIBLE_WINDOW这个flag。
之后会继续保持一些数据,比如输入法模式,窗口缩放比例值,父窗口token等等,接着我们看到会调用requestLayout这个方法,这个方法如下:
```java
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 检查是不是同一个线程
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
```
这个方法里面我们又看到了scheduleTraversals这个方法,这个方法前面也提到过,里面是对于View的测量,布局,绘制等操作,会把view绘制在屏幕上显示给用户,用户就可以看到了,这个方法详细我们后面会分析View模块的时候说,这里也不跟进了。这里要提一下的是上面checkThread这个方法:
```java
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
```
我们知道会View的操作只能在主线程,如果在子线程的话就会报错,上面这个方法我们看到了就是判断这个问题的地方,mThread是创建ViewRootImpl的线程,也就是主线程,如果和当前线程不一致,那么就会报错了,这个相信android的开发同学都很清楚。这里稍稍提一个问题,有些情况下载子进程中是可以更新View的,相信很多同学也看过这个问题,比如在onCreate中开启一个新线程更新View是可以正常执行的,这个又是怎么回事呢?比如下面代码:
```java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Text tv = findViewById(R.id.text);
new Thread(new Runnable() {
@Override
public void run() {
tv.setText("text");
}
}).start();
}
```
这里开启一个新线程,线程里面调用setText方法,是可以正常运行的,有兴趣的同学可以自己去实验下。其实根据我们之前分析代码可以知道,判断View更新是否在主线程实在ViewRootImpl这个类中的,而ViewRootImpl的实例是在onResume后执行的,所以上面线程中更新子View的方法放在onResume中理论上也是可以的,之所以说理论,子线程和主线程是并行运行的,所以子线程和主线程哪个先执行不确定,所以说理论,当然实际上可能大多情况都是可以运行的。
以上面TextView的setText举例,setText这个方法在子线程中执行,具体TextView的流程这里就不详细说了,只说主要的地方,经过一系列方法会走到requestLayout方法:
```java
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
// 这里的mParent对于一个普通的View来说就是他父ViewGroup
// 对于DecorView来说,就是ViewRootImpl
// 所以如果执行到的是DecorView这里mParent就是null,比如onCreate时候自线程更新View这里就不会继续下去
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
```
这里getViewRootImpl方法是获取ViewRootImpl,我们知道ViewRootImpl是在onResume后创建的,所以这里极有可能是没有创建好,所以会跳过。下面mParent是ViewParent类型的,DecorView和ViewRootImpl都是继承ViewParent的,所以如果是Activity中的子元素,首先会走道DecorView中,DecorView的mParent会是ViewRootImpl,这个我们等下继续分析ViewRootImpl的时候会看到。所以如果此时ViewRootImpl已经创建了,那么就会调用他的requestLayout方法,这个方法上面我么看到了,会检查子线程的问题,所以一般我们说子线程不能更新View都是指的ViewRootImpl已经创建的情况,如果没创建的话,这里就不会执行。另外TextView中还会调用invalidate方法,这个方法最终也会往上走到ViewParent中,结果也是一样的,如果ViewRootImpl没有创建的话,也不会继续执行。所以这里我们可以知道在ViewRootImpl创建之前,是可以在子线程中更新View的,其实这里的的更新并不是说View已经绘制好了我们要重新绘制,正如之前我们分析onCreate中setContentView方法那样,其实在ViewRootImpl创建之前,都是创建View的数据,而并没有进行测量,布局,绘制这三部,所以正确的说不能用更新,而用初始化更确切点。由于正好分析到这部分相关代码,以上就稍微说了下关系子线程的问题,具体关于View方面的详细分析,后面有文章会专门收,我们回到ViewRootImpl中setView方法。
之后还会创建一个输入事件InputChannel,这个是保存客户端和native端之间输入事件数据的,我们这里不展开说。后面在保存下窗口可见性,窗口类型以及状态栏等的显示状态,最后会调用Session的addToDisplay方法,这个方法会把当前这个窗口的LayoutParams,AttachInfo以及W类这些参数传给WMS,继续通知WMS进行流程。
和AMS一样,客户端的窗口需要让WMS也知道,而且些窗口对于当前这个进程是否有权限显示也是需要系统来检查的,所以这会把当前客户端窗口的信息传给WMS,开始WMS的流程。我们就先去WMS那边看看,暂时离开ViewRoot的Impl方法,等那边执行完了,我们在回来看。
# 客户端通知WMS
通知WMS是我们之前介绍过的Session这个类,这个类中封装了许多不同的模块,所以客户端对外的通信只要和Session对接就可以了,我们看下调用方法:
```java
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, InputChannel outInputChannel) {
// 调用WMS的addWindow,这里window参数是rootView的W
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
```
这个方法会继续调用WindowManagerService的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) {
int[] appOp = new int[1];
// 检查是否有权限添加窗口
// appOp会被赋值一些具体的窗口权限检查类型
int res = mPolicy.checkAddPermission(attrs, appOp);
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;
}
// 窗口对应的config是否有变化
boolean reportNewConfig = false;
// 父WindowState
WindowState parentWindow = null;
long origId;
final int callingUid = Binder.getCallingUid();
final int type = attrs.type;
synchronized (mWindowMap) {
// 屏幕没有准备好,报错
if (!mDisplayReady) {
throw new IllegalStateException("Display has not been initialialized");
}
// 看看是否有这个屏幕了
final DisplayContent displayContent = mRoot.getDisplayContentOrCreate(displayId);
if (displayContent == null) {
Slog.w(TAG_WM, "Attempted to add window to a display that does not exist: "
+ displayId + ". Aborting.");
return WindowManagerGlobal.ADD_INVALID_DISPLAY;
}
// 是否有权限在这个屏幕上添加
if (!displayContent.hasAccess(session.mUid)
&& !mDisplayManagerInternal.isUidPresentOnDisplay(session.mUid, displayId)) {
Slog.w(TAG_WM, "Attempted to add window to a display for which the application "
+ "does not have access: " + displayId + ". Aborting.");
return WindowManagerGlobal.ADD_INVALID_DISPLAY;
}
// 看看是否有这个窗口了,通过binder来查,client是rootView的W
if (mWindowMap.containsKey(client.asBinder())) {
Slog.w(TAG_WM, "Window " + client + " is already added");
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
// 进入这里是一个子窗口,取出父窗口
// 比如如果是一个Fragment情况,会进入这里,取出父Activity的WindowState
parentWindow = windowForClientLocked(null, attrs.token, false);
// 父窗口为null,return
if (parentWindow == null) {
Slog.w(TAG_WM, "Attempted to add window with token that is not a window: "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
// 父窗口的类型也是一个子窗口,return
if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
&& parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
Slog.w(TAG_WM, "Attempted to add window with token that is a sub-window: "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
}
// 如果是一个私有的虚拟设备,但是DisplayContent不是私有的,return
if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) {
Slog.w(TAG_WM, "Attempted to add private presentation window to a non-private display. Aborting.");
return WindowManagerGlobal.ADD_PERMISSION_DENIED;
}
...............
}
```
以上是代码的第一部分。这里开始会先判断当前进程是否有显示这个窗口的权限,权限模块我们之前没说过,这里简单看下是怎么判断权限的,这里会调用WindowManagerPolicy的checkAddPermission方法,WindowManagerPolicy是个接口,他的实现类是PhoneWindowManager,这个对象是在SystemServer进程初始化WMS时候就创建的,从名字就可以看出这个类是处理有关窗口策略的类,比如这里的判断权限,我们看下这个方法:
```java
public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
int type = attrs.type;
// 先赋值个默认值,表示这些窗口不需要权限检查
outAppOp[0] = AppOpsManager.OP_NONE;
// 这些类型是无效的
if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)
|| (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)
|| (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {
return WindowManagerGlobal.ADD_INVALID_TYPE;
}
// 非系统窗口类型不需要检查权限,都允许
if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {
// Window manager will make sure these are okay.
return ADD_OKAY;
}
// 是否是系统的弹窗
if (!isSystemAlertWindowType(type)) {
switch (type) {
case TYPE_TOAST:
// android O之前添加toast不需要token,之后就需要了,可以参考doesAddToastWindowRequireToken方法
// Only apps that target older than O SDK can add window without a token, after
// that we require a token so apps cannot add toasts directly as the token is
// added by the notification system.
// Window manager does the checking for this.
// 如果类型是toast的话,把OP_TOAST_WINDOW值赋给outAppOp
outAppOp[0] = OP_TOAST_WINDOW;
return ADD_OKAY;
case TYPE_DREAM:
case TYPE_INPUT_METHOD:
case TYPE_WALLPAPER:
case TYPE_PRESENTATION:
case TYPE_PRIVATE_PRESENTATION:
case TYPE_VOICE_INTERACTION:
case TYPE_ACCESSIBILITY_OVERLAY:
case TYPE_QS_DIALOG:
// The window manager will check these.
return ADD_OKAY;
}
return mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
== PERMISSION_GRANTED ? ADD_OKAY : ADD_PERMISSION_DENIED;
}
// Things get a little more interesting for alert windows...
// 是否运行弹出窗口权限
outAppOp[0] = OP_SYSTEM_ALERT_WINDOW;
final int callingUid = Binder.getCallingUid();
// system processes will be automatically granted privilege to draw
// 系统进程自动有权限
if (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID) {
return ADD_OKAY;
}
ApplicationInfo appInfo;
try {
// 获取minifest的信息
appInfo = mContext.getPackageManager().getApplicationInfoAsUser(
attrs.packageName,
0 /* flags */,
UserHandle.getUserId(callingUid));
} catch (PackageManager.NameNotFoundException e) {
appInfo = null;
}
// 如果没有获得minifest信息,或者如果窗口类型不是TYPE_APPLICATION_OVERLAY的话在andrid 8后需要
// 有INTERNAL_SYSTEM_WINDOW这个权限
if (appInfo == null || (type != TYPE_APPLICATION_OVERLAY && appInfo.targetSdkVersion >= O)) {
return (mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
== PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;
}
// 看看outAppOp[0]这个权限是否运行
final int mode = mAppOpsManager.checkOpNoThrow(outAppOp[0], callingUid, attrs.packageName);
switch (mode) {
case AppOpsManager.MODE_ALLOWED:
case AppOpsManager.MODE_IGNORED:
return ADD_OKAY;
case AppOpsManager.MODE_ERRORED:
// Don't crash legacy apps
if (appInfo.targetSdkVersion < M) {
return ADD_OKAY;
}
return ADD_PERMISSION_DENIED;
default:
return (mContext.checkCallingOrSelfPermission(SYSTEM_ALERT_WINDOW)
== PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;
}
}
```
这个方法首先会判断窗口类型是否在正确的数值范围内,我们之前有分析过,窗口有三种类型,子窗口,应用窗口和系统窗口,这些类型都有对应的值,这里首先检查是否值是有效的,可以看到的话,这里只会检查系统类型的窗口,非系统类型的,自动可以显示。
对于权限来说,早期android不存在动态权限的时候,只要在minifest中申请权限就可以了,但是不是所有权限都可以在minifest中申请的,有些权限甚至是隐式的,所在android中的AppOpsManager就是专门处理这些权限的。这个类似声明了好几十种权限,我们大概看一眼:
```java
public static final int OP_NONE = -1;
/** @hide Access to coarse location information. */
public static final int OP_COARSE_LOCATION = 0;
/** @hide Access to fine location information. */
public static final int OP_FINE_LOCATION = 1;
/** @hide Causing GPS to run. */
public static final int OP_GPS = 2;
/** @hide */
public static final int OP_VIBRATE = 3;
/** @hide */
public static final int OP_READ_CONTACTS = 4;
/** @hide */
public static final int OP_WRITE_CONTACTS = 5;
/** @hide */
public static final int OP_READ_CALL_LOG = 6;
/** @hide */
public static final int OP_WRITE_CALL_LOG = 7;
/** @hide */
public static final int OP_READ_CALENDAR = 8;
/** @hide */
public static final int OP_WRITE_CALENDAR = 9;
/** @hide */
public static final int OP_WIFI_SCAN = 10;
...............
```
这里我哦们就截取了10个大概看一下,可以看到中间有很多我们熟悉的权限,比如定位,读写联系人,日历等等,上面的方法就会根据打开的窗口类型来判断当前进程是否有权限显示该窗口,我们回到前面的方法。
默认先把OP_NONE赋值给权限类型,这个表示没有具体的权限需要检查,也就是可以显示窗口。之后我们看到有些类型的窗口是不需要检查的,所以直接就返回了,Toast这个类型的窗口稍微特殊些,之前也是不需要检查的,但是在android 8后需要一个token才能显示,所以这里会把检查类型修改为Toast,然后也返回,后面会在检查。
除了一些类型的窗口,其他系统窗口需要加上OP_SYSTEM_ALERT_WINDOW这个权限,如果窗口类型不是TYPE_APPLICATION_OVERLAY这个的话,在android 8后要求持有INTERNAL_SYSTEM_WINDOW这个权限。其他窗口类型的话,就检查OP_SYSTEM_ALERT_WINDOW这个类型就可以了,这里检查权限的方法是checkOpNoThrow我们就不跟进去看了,这个上面了解下权限有关的检查就可以了,有兴趣的同学可以再深入研究下,我们回到前面WMS的addWindow方法。
判断完该进程是否有这个窗口的权限后,还有一些需要判断的。比如屏幕有没有初始化好,正常情况在SystemServer进程初始化WMS时候会初始化所有的屏幕。对于的DisplayContent也应该可以获取到。该进程也有权限显示在这个屏幕上。接着我们在WMS中会看到有个mWindowMap变量,这个是一个Map,他的key是IBinder,value是一个WindowState。我们在之前的分析中,已经看到过好几个地方会保存和窗口有关的对象了,在DisplayContent中,有四个xxxContainers会保存不同类型的WindowToken以及mTokenMap中会保存这个屏幕中所有的窗口WindowToken。这些都是和AMS中 在WindowManagerGlobal中,会保存添加窗口的View,ViewRootImpl以及LayoutParams。而在WMS中,其实会保存每个窗口的WindowState对象,这个对象的创建在下面的流程就会看到。所以我们也可以这样理解,在DisplayContent中代表窗口的是WindowToken,在每个客户端中就是View和ViewRootImpl,而在WMS中就是WindowState。这样理解的话,我们面对这么多代表同一个窗口的对象就比较容易理解些,我们回到上面的代码。
由于WMS的mWindowMap保存了所有窗口的WindowState,所以这里也会看一下,是否已经有这个窗口了,mWindowMap的key是客户端传过来的W对象实例,他是一个IBinder,所以判断下有没有这个IBinder就可以知道有没有了。如果没有的话,继续往下检查,如果是一个子窗口的话,要求需要有父窗口,并且父窗口的类型不能是子窗口。最后还检查一下窗口类型是一个私有的虚拟设备,但是显示他的屏幕不是私有的虚拟设置,这个我们也不多管。上面这些就是addWindow方法的第一部分代码,我们继续看下面一部分代码:
```java
...................
AppWindowToken atoken = null;
// 是否有父窗口
final boolean hasParent = parentWindow != null;
// Use existing parent window token for child windows since they go in the same
// token
// as there parent window so we can apply the same policy on them.
// 通过一个IBinder,获取当前屏幕中是否有这个IBinder对应的WindowTokend
// 先看看有没有父窗口,如果没有,参数attrs的token就是一个IBinder,比如一个
// Activity的话,attrs.token就是一个ActivityRecord的token
// 或者Toast的话,attrs.token是一个NotifacationService中new的Binder。
// 如果有父窗口,那么从父窗口的mAttrs中取得的也就是上面说的同样的token
// 这里看看displayContent中是否已经有这个Binder对应的WindowTOkend了,
// 这里需要注意参数中token是binder,取得的WindowTokend是WindowContainer
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
// If this is a child window, we want to apply the same type checking rules as
// the
// parent window type.
// 如果有父窗口,取出父窗口的type,否则就是自己的type
final int rootType = hasParent ? parentWindow.mAttrs.type : type;
boolean addToastWindowRequiresToken = false;
if (token == null) { // 进入这里是token为null,下面先过滤那些必须要有token的窗口
// 如果是一个app窗口,不可能为null,所以return
if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
Slog.w(TAG_WM, "Attempted to add application window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
// 输入法窗口,return
if (rootType == TYPE_INPUT_METHOD) {
Slog.w(TAG_WM, "Attempted to add input method window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
// 语音交互的窗口,return
if (rootType == TYPE_VOICE_INTERACTION) {
Slog.w(TAG_WM, "Attempted to add voice interaction window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
// 壁纸窗口,return
if (rootType == TYPE_WALLPAPER) {
Slog.w(TAG_WM, "Attempted to add wallpaper window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
// 屏保窗口,return
if (rootType == TYPE_DREAM) {
Slog.w(TAG_WM, "Attempted to add Dream window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
// quick setting的窗口,return
if (rootType == TYPE_QS_DIALOG) {
Slog.w(TAG_WM, "Attempted to add QS dialog window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
// 无障碍相关的窗口
if (rootType == TYPE_ACCESSIBILITY_OVERLAY) {
Slog.w(TAG_WM, "Attempted to add Accessibility overlay window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
// Toast窗口
if (type == TYPE_TOAST) {
// Apps targeting SDK above N MR1 cannot arbitrary add toast windows.
// 进入到这里说明没有找到存在的toast,那么要添加一个toast窗口
// 之前android版本对toast显示没什么限制,从O开始不能虽然添加toast窗口了,有些情况下必须要有token才能添加toast
if (doesAddToastWindowRequireToken(attrs.packageName, callingUid,
parentWindow)) {
// 有进入到这里属于没有token的情况。而doesAddToastWindowRequireToken有2种情况必须要有token才能弹
// 1. 有父窗口,并且是一个Activity同时sdk大于等于O,这个时候需要有token,即父Activity需要存在
// 2. 没父窗口,sdk大于等于O的时候,需要toast存在windowtoken,才能添加窗口
Slog.w(TAG_WM, "Attempted to add a toast window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}
// 进入到这里,说明当前屏幕中还没有,表示这个窗口的WindowContainer,下面会自动创建一个WindowContainer(即一个AppWindowToken)
// 当然这个分支已经排查上面一些类型的窗口了,Activity是不会到这里的
// 首先看看attrs.token是否与数据,比如toast的话是非null的,就会取这个token(binder),否则会取
// viewRootImpl(client)创建时候new的那个W
final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
// 创建WindowToken
token = new WindowToken(this, binder, type, false, displayContent,
session.mCanAddInternalSystemWindow);
} else if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
// 进入到这里说明目前displayContent已经有了窗口的WindowToken了(windowContainer),并且是一个应用类型
// 说明上面取出的token是AppWindowToken
atoken = token.asAppWindowToken();
// 看看AppWindowToken是否为null
if (atoken == null) { // 为null返回
Slog.w(TAG_WM, "Attempted to add window with non-application token "
+ token + ". Aborting.");
return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
} else if (atoken.removed) {
// 这个Activity正在退出,返回
Slog.w(TAG_WM, "Attempted to add window with exiting application token "
+ token + ". Aborting.");
return WindowManagerGlobal.ADD_APP_EXITING;
}
} else if (rootType == TYPE_INPUT_METHOD) {
// 如果目前要添加窗口的类型是输入法,但是找到的token(windowtoken)不是输入法,return
if (token.windowType != TYPE_INPUT_METHOD) {
Slog.w(TAG_WM, "Attempted to add input method window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (rootType == TYPE_VOICE_INTERACTION) {
// 语言交互类型不对,return
if (token.windowType != TYPE_VOICE_INTERACTION) {
Slog.w(TAG_WM, "Attempted to add voice interaction window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (rootType == TYPE_WALLPAPER) {
// 壁纸类型不对,return
if (token.windowType != TYPE_WALLPAPER) {
Slog.w(TAG_WM, "Attempted to add wallpaper window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (rootType == TYPE_DREAM) {
// 屏保类型不对,return
if (token.windowType != TYPE_DREAM) {
Slog.w(TAG_WM, "Attempted to add Dream window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (rootType == TYPE_ACCESSIBILITY_OVERLAY) {
// 无障碍类型不对,return
if (token.windowType != TYPE_ACCESSIBILITY_OVERLAY) {
Slog.w(TAG_WM, "Attempted to add Accessibility overlay window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_TOAST) {
// Apps targeting SDK above N MR1 cannot arbitrary add toast windows.
// 看看添加toast需不需要windowToken
addToastWindowRequiresToken = doesAddToastWindowRequireToken(attrs.packageName,
callingUid, parentWindow);
// 执行到这里说明toast有windowToken,所以下面判断在需要windowToken的时候,但是toast类型不对,return
if (addToastWindowRequiresToken && token.windowType != TYPE_TOAST) {
Slog.w(TAG_WM, "Attempted to add a toast window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_QS_DIALOG) {
// quick setting类型不匹配,return
if (token.windowType != TYPE_QS_DIALOG) {
Slog.w(TAG_WM, "Attempted to add QS dialog window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (token.asAppWindowToken() != null) {
// 添加窗口的类型不是应用类型,但是找到的windowToken类型是应用类型,所以类型不对
// 所以这个传入的参数attrs.token不对,把他置null,然后重新创建一个windowToken,
// 用viewRootImpl作为第二个参数来创建
Slog.w(TAG_WM, "Non-null appWindowToken for system window of rootType=" + rootType);
// It is not valid to use an app token with other system types; we will
// instead make a new token for it (as if null had been passed in for the
// token).
// 把之前错误的token置null
attrs.token = null;
token = new WindowToken(this, client.asBinder(), type, false, displayContent,
session.mCanAddInternalSystemWindow);
}
................
```
这段代码看起来很长,其实也都是一些检查的代码。首先会看下这个窗口是否有父窗口,如果有的话,会从DisplayContent中获取父窗口的WindowToken,前面我们刚刚说过每个窗口在DisplayContent中保存的就是WindowToken,这里取出父窗口的WindowToken,如果没有父窗口则取出自己的WindowToken。同时还会取出父窗口的类型或者自己的类型。
根据第一篇WMS的分析文章,我们知道一个Activity在AMS中创建ActvityRecord的时候,同时会创建对应的AppWindowToken,但这只是应用类型的窗口会有这个过程,其他类型的窗口并不一样会先创建WIndowToken再执行这里WMS的流程,比如Toast在android 8之前可以允许没有WindowToken,所以到这里的话可能就没有WindowToken,所以下面代码会根据WindowToken是否为空继续处理。
上面代码中可以看到,如果WindowToken为空,会先检查一下那些必须有WindowToken的窗口类型,比如应用类型窗口,输入法,壁纸等,这些窗口是要有windowToken的,如果没有就会返回错误。这里尤其注意下Toast窗口,上面我们说了,在android O之前如果没有windowToken是可以添加Toast的,但是在android O后就不可以随便添加了,这里会调用doesAddToastWindowRequireToken方法来判断是否需要windowToken,我们看下这个方法:
```java
private boolean doesAddToastWindowRequireToken(String packageName, int callingUid,
WindowState attachedWindow) {
// toast窗口可以没有token,
// Try using the target SDK of the root window
if (attachedWindow != null) {
// 有父窗口,同时父窗口是一个Activity并且sdk大于等于O,才能弹
return attachedWindow.mAppToken != null
&& attachedWindow.mAppToken.mTargetSdk >= Build.VERSION_CODES.O;
} else { // 如果没有父窗口,sdk大于等于O的时候,才能弹
// Otherwise, look at the package
try {
// 否则获取下包信息
ApplicationInfo appInfo = mContext.getPackageManager()
.getApplicationInfoAsUser(packageName, 0,
UserHandle.getUserId(callingUid));
if (appInfo.uid != callingUid) {
throw new SecurityException("Package " + packageName + " not in UID "
+ callingUid);
}
// SDK大于等于O的时候,如果没有父窗口,需要toast有Windowtoken不能添加toast
if (appInfo.targetSdkVersion >= Build.VERSION_CODES.O) {
return true;
}
} catch (PackageManager.NameNotFoundException e) {
/* ignore */
}
}
return false;
}
```
这里首先会看下是否有父窗口,我们知道在创建Toast的时候会传入Context,如果Context是一个Activity的话,自然是有父窗口的,如果是Application的话就没有父窗口。在有父窗口的时候,上面方法会检查是否有Activity的AppWindowToken,有的话那就可以正常添加Toast窗口。
如果没有父窗口,说明创建Toast的时候传入的Context可能是一个Application,那么这里会检查,如果android O一下的版本,是可以添加Toast窗口的,而从android O开始就需要有WindowToken才能添加了。如果需要WindowToken这个方法会返回true,否则返回false,我们回到前面addWindow方法。
经过上面一系列的过滤,如果都正常执行的话,说明当前这个窗口不需要有个attach的的父窗口,但是这个根据前面我们说的WindowToken是在DisplayContent中保存的所有当前屏幕下窗口的对象,所以还是需要为当前这个窗口创建一个WindowToken给DisplayContent,所以最后还是会为该窗口创建一个WindowToken,这里WindowToken的构造方法中会自动把本WindowToken添加到DisplayContent中,这个过程同创建AppWindowToken是一样的,这里就不赘述了,我们知道在DisplayContent中的mTokenMap会保存这个WindowToken,mTokenMap这个Map的key是IBinder,所以这里创建WindowToken的时候IBinder取的是前面客户端传过来的LayoutParams中的token,这个token我在们前面分析的时候知道会根据窗口的不同类型有不同的来源,比如一个子窗口就是他父窗口的token,Activity就是自己的ActivityRecord的Token,系统窗口比如Toast的话系统会创建一个IBinder对象,所以这里如果会以这个token作为保存在DisplayContent中的key,如果前面LayoutParams没有传过来token的话,这里会使用ViewRootImpl中创建的W类对象作为key,我们知道一个窗口是对应一个ViewRootImpl的,所以也是可以唯一标识一个窗口的。
对于没有token的情况最终会自动创建一个WindowToken,对于有token的情况也会做一下检查,检查下token的类型和当前要添加的窗口的类型是否是一致的,如果不一样的话也会返回错误。最后有个特殊情况,如果添加的窗口的类型不是应用类型,但是token的asAppWindowToken非空,说明这个token是个应用类型窗口的token,两者不一致了,所以不能用这个token,也会重新new一个WindowToken。好了,我们在看下一段代码:
```java
...........
final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], seq, attrs, viewVisibility, session.mUid,
session.mCanAddInternalSystemWindow);
// 死亡监听为null了,说明退出了,return
if (win.mDeathRecipient == null) {
// Client has apparently died, so there is no reason to
// continue.
Slog.w(TAG_WM, "Adding window client " + client.asBinder()
+ " that is dead, aborting.");
return WindowManagerGlobal.ADD_APP_EXITING;
}
// 如果没有屏幕,return
if (win.getDisplayContent() == null) {
Slog.w(TAG_WM, "Adding window to Display that has been removed.");
return WindowManagerGlobal.ADD_INVALID_DISPLAY;
}
// 根据添加窗口的类型,调整窗口的一些属性
// 比如设置某个窗口类型是不可接受输入事件的,锁屏是否总是要遮挡其他窗口
// toast的显示时间,窗口是否显示延伸到状态栏和导航栏的下面
mPolicy.adjustWindowParamsLw(win.mAttrs);
// 设置是否这个窗口只显示在owner用户的屏幕上
win.setShowToOwnerOnlyLocked(mPolicy.checkShowToOwnerOnly(attrs));
// 看看状态栏和导航栏窗口有没有添加,如果添加了,后面就不用继续添加窗口了
res = mPolicy.prepareAddWindowLw(win, attrs);
// 返回0,表示这个可以继续添加窗口。否则表示已经有窗口了,不需要添加return
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;
}
............
```
这段代码开始就要创建WindowState对象了,这个我们前面看到很多了,他是一个窗口在WMS中保存的类型,我们看下他的构造方法:
```java
WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token,
WindowState parentWindow, int appOp, int seq, WindowManager.LayoutParams a,
int viewVisibility, int ownerId, boolean ownerCanAddInternalSystemWindow) {
mService = service; // WMS
mSession = s; // Session,和WMS沟通的桥梁,里面还有inputManager
mClient = c; // ViewRootImpl中的W
// 进行权限检查的窗口类型
mAppOp = appOp;
// ActivtyRecord的token,或者像toast在notifacationService中创建的binder,也可能是viewRootImpl的W
mToken = token;
// AppWindowToken
mAppToken = mToken.asAppWindowToken();
mOwnerUid = ownerId; // Session中保存的uid
mOwnerCanAddInternalSystemWindow = ownerCanAddInternalSystemWindow;
mWindowId = new WindowId(this);
// 保存这请求添加窗口的参数
mAttrs.copyFrom(a);
mViewVisibility = viewVisibility;
mPolicy = mService.mPolicy;
mContext = mService.mContext;
// 创建一个死亡监听
DeathRecipient deathRecipient = new DeathRecipient();
mSeq = seq;
// 是否是适配模式
mEnforceSizeCompat = (mAttrs.privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0;
if (localLOGV) Slog.v(
TAG, "Window " + this + " client=" + c.asBinder()
+ " token=" + token + " (" + mAttrs.token + ")" + " params=" + a);
try {
// 监听窗口的死亡
c.asBinder().linkToDeath(deathRecipient, 0);
} catch (RemoteException e) {
mDeathRecipient = null;
mIsChildWindow = false;
mLayoutAttached = false;
mIsImWindow = false;
mIsWallpaper = false;
mIsFloatingLayer = false;
mBaseLayer = 0;
mSubLayer = 0;
mInputWindowHandle = null;
mWinAnimator = null;
return;
}
// 设置死亡监听
mDeathRecipient = deathRecipient;
....................
}
```
这个类的构造方法还是挺长的,上面是前半段,前半段没什么特别的操作,主要是把传入的参数保存在WindowState中,比如WMS,Session,权限,WindowToken等,这里不用多说什么。我们看第二段代码:
```java
.......................
// 子窗口
if (mAttrs.type >= FIRST_SUB_WINDOW && mAttrs.type <= LAST_SUB_WINDOW) {
// The multiplier here is to reserve space for multiple
// windows in the same type layer.
// 计算窗口的Z轴主序
// 计算主序时候公式为type*10000+1000
// 大部分情况这个值越大,越在屏幕的上面
mBaseLayer = mPolicy.getWindowLayerLw(parentWindow)
* TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET;
// 子窗口类型的有子序,计算子序值
mSubLayer = mPolicy.getSubWindowLayerFromTypeLw(a.type);
// 是子窗口
mIsChildWindow = true;
if (DEBUG_ADD_REMOVE) Slog.v(TAG, "Adding " + this + " to " + parentWindow);
parentWindow.addChild(this, sWindowSubLayerComparator);
// 是不是依附在父窗口上面的,或者说是不是父容器的子窗口
mLayoutAttached = mAttrs.type !=
WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
// 是否是输入法窗口,从父窗口的type获取
mIsImWindow = parentWindow.mAttrs.type == TYPE_INPUT_METHOD
|| parentWindow.mAttrs.type == TYPE_INPUT_METHOD_DIALOG;
// 是否是壁纸,从父窗口的类型获取
mIsWallpaper = parentWindow.mAttrs.type == TYPE_WALLPAPER;
} else {
// The multiplier here is to reserve space for multiple
// windows in the same type layer.
// 计算窗口的Z轴主序
mBaseLayer = mPolicy.getWindowLayerLw(this)
* TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET;
// 非子窗口类型的没有子序,赋值为0
mSubLayer = 0;
// 不是子窗口
mIsChildWindow = false;
// 是不是父窗口的子窗口
mLayoutAttached = false;
// 是否是输入法窗口
mIsImWindow = mAttrs.type == TYPE_INPUT_METHOD
|| mAttrs.type == TYPE_INPUT_METHOD_DIALOG;
// 是否是壁纸
mIsWallpaper = mAttrs.type == TYPE_WALLPAPER;
}
// 是否是输入法窗口或者壁纸窗口
mIsFloatingLayer = mIsImWindow || mIsWallpaper;
// 是一个Activity并且可以显示给所有用户
if (mAppToken != null && mAppToken.mShowForAllUsers) {
// Windows for apps that can show for all users should also show when the device is
// locked.
// 可以在锁屏上显示
mAttrs.flags |= FLAG_SHOW_WHEN_LOCKED;
}
// 初始化窗口动画类
mWinAnimator = new WindowStateAnimator(this);
mWinAnimator.mAlpha = a.alpha;
mRequestedWidth = 0;
mRequestedHeight = 0;
mLastRequestedWidth = 0;
mLastRequestedHeight = 0;
mXOffset = 0;
mYOffset = 0;
mLayer = 0;
// 创建处理输入分发事件的Handler
mInputWindowHandle = new InputWindowHandle(
mAppToken != null ? mAppToken.mInputApplicationHandle : null, this, c,
getDisplayId());
```
第二段代码我们主要说一下窗口的层序问题。这里我们看到有个mBaseLayer变量,这个变量代表窗口在屏幕Z轴上的位置的主序,数值越大代表他在屏幕上的位置越靠上,也就越是接近被用户看到。除了mBaseLayer这个变量之外,还有一个mSubLayer变量,这个变量只有子窗口类型的窗口才有有效值,其余类型窗口都是0,这个的意义也很好理解,子窗口由于是附着在父窗口上的,所以这个值大于0表示这个子窗口显示在父窗口的上面的位置,小于0表示显示在父窗口的下面位置,同样也是数值越大代表越接近用户。我们先看下mBaseLayer的计算方法,这里计算方法是mPolicy.getWindowLayerLw(parentWindow) * TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET,这里会先调用WindowManagerPolicy的getWindowLayerLw方法,获得值后乘以TYPE_LAYER_MULTIPLIER再加上TYPE_LAYER_OFFSET,这里TYPE_LAYER_MULTIPLIER和TYPE_LAYER_OFFSET的值分别是10000和1000,这里之所以要乘以10000和1000,个人觉的主要是防止不同窗口的值域会有重合,WindowManagerPolicy的getWindowLayerLw方法我们等下跟进看了就知道是获取一个窗口类型的值,而根据上面代码的分析我们知道,如果仅仅是不同类型的窗口,那么他们的值都是不同的,但是别忘记了还有子窗口,子窗口会在父窗口的基础上再偏移一些距离的,所以这里乘以10000再加1000就是为了让所有的窗口,不管是子窗口还是父窗口,都不要重合了。好了,我们去WindowManagerPolicy中看下getWindowLayerLw方法:
```java
default int getWindowLayerLw(WindowState win) {
return getWindowLayerFromTypeLw(win.getBaseType(), win.canAddInternalSystemWindow());
}
```
继续调用getWindowLayerFromTypeLw方法:
```java
default int getWindowLayerFromTypeLw(int type, boolean canAddInternalSystemWindow) {
// 如果是应用类型的,返回APPLICATION_LAYER
if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
return APPLICATION_LAYER;
}
// 其他类型的,返回这些类型对应的值
switch (type) {
case TYPE_WALLPAPER:
// wallpaper is at the bottom, though the window manager may move it.
return 1;
case TYPE_PRESENTATION:
case TYPE_PRIVATE_PRESENTATION:
return APPLICATION_LAYER;
case TYPE_DOCK_DIVIDER:
return APPLICATION_LAYER;
case TYPE_QS_DIALOG:
return APPLICATION_LAYER;
case TYPE_PHONE:
return 3;
case TYPE_SEARCH_BAR:
case TYPE_VOICE_INTERACTION_STARTING:
return 4;
case TYPE_VOICE_INTERACTION:
// voice interaction layer is almost immediately above apps.
return 5;
case TYPE_INPUT_CONSUMER:
return 6;
case TYPE_SYSTEM_DIALOG:
return 7;
case TYPE_TOAST:
// toasts and the plugged-in battery thing
return 8;
case TYPE_PRIORITY_PHONE:
// SIM errors and unlock. Not sure if this really should be in a high layer.
return 9;
case TYPE_SYSTEM_ALERT:
// like the ANR / app crashed dialogs
return canAddInternalSystemWindow ? 11 : 10;
case TYPE_APPLICATION_OVERLAY:
return 12;
case TYPE_DREAM:
// used for Dreams (screensavers with TYPE_DREAM windows)
return 13;
case TYPE_INPUT_METHOD:
// on-screen keyboards and other such input method user interfaces go here.
return 14;
case TYPE_INPUT_METHOD_DIALOG:
// on-screen keyboards and other such input method user interfaces go here.
return 15;
case TYPE_STATUS_BAR_SUB_PANEL:
return 17;
case TYPE_STATUS_BAR:
return 18;
case TYPE_STATUS_BAR_PANEL:
return 19;
case TYPE_KEYGUARD_DIALOG:
return 20;
case TYPE_VOLUME_OVERLAY:
// the on-screen volume indicator and controller shown when the user
// changes the device volume
return 21;
case TYPE_SYSTEM_OVERLAY:
// the on-screen volume indicator and controller shown when the user
// changes the device volume
return canAddInternalSystemWindow ? 22 : 11;
case TYPE_NAVIGATION_BAR:
// the navigation bar, if available, shows atop most things
return 23;
case TYPE_NAVIGATION_BAR_PANEL:
// some panels (e.g. search) need to show on top of the navigation bar
return 24;
case TYPE_SCREENSHOT:
// screenshot selection layer shouldn't go above system error, but it should cover
// navigation bars at the very least.
return 25;
case TYPE_SYSTEM_ERROR:
// system-level error dialogs
return canAddInternalSystemWindow ? 26 : 10;
case TYPE_MAGNIFICATION_OVERLAY:
// used to highlight the magnified portion of a display
return 27;
case TYPE_DISPLAY_OVERLAY:
// used to simulate secondary display devices
return 28;
case TYPE_DRAG:
// the drag layer: input for drag-and-drop is associated with this window,
// which sits above all other focusable windows
return 29;
case TYPE_ACCESSIBILITY_OVERLAY:
// overlay put by accessibility services to intercept user interaction
return 30;
case TYPE_SECURE_SYSTEM_OVERLAY:
return 31;
case TYPE_BOOT_PROGRESS:
return 32;
case TYPE_POINTER:
// the (mouse) pointer layer
return 33;
default:
Slog.e("WindowManager", "Unknown window type: " + type);
return APPLICATION_LAYER;
}
}
```
这个方法我们看下就行,就是根据不同的类型返回不同的值,我们回到前面的WindowState构造方法中。mBaseLayer值就如上面的公式计算好了,我们看下另一个值,子窗口层序的mSubLayer,他是调用WindowManagerPolicy的getSubWindowLayerFromTypeLw方法,我们看下:
```java
default int getSubWindowLayerFromTypeLw(int type) {
switch (type) {
case TYPE_APPLICATION_PANEL:
case TYPE_APPLICATION_ATTACHED_DIALOG:
return APPLICATION_PANEL_SUBLAYER;
case TYPE_APPLICATION_MEDIA:
return APPLICATION_MEDIA_SUBLAYER;
case TYPE_APPLICATION_MEDIA_OVERLAY:
return APPLICATION_MEDIA_OVERLAY_SUBLAYER;
case TYPE_APPLICATION_SUB_PANEL:
return APPLICATION_SUB_PANEL_SUBLAYER;
case TYPE_APPLICATION_ABOVE_SUB_PANEL:
return APPLICATION_ABOVE_SUB_PANEL_SUBLAYER;
}
Slog.e("WindowManager", "Unknown sub-window type: " + type);
return 0;
}
```
子窗口的计算更简单,子窗口主要就是上面这几种,所以直接返回这几种类型的值就可以了,这样父窗口和子窗口的层序值就都计算好了,WindowState中保存着这个窗口的层序值,但是这2个值还要经过计算后才能得到真正的显示在Z轴上的层序值,这个值会保存在mLayer这个变量中,这个变量在初始化WindowState的时候值是0,我们后面会看到这个值的更新,这里只要知道他是代表了最终显示Z轴上的位置就可以了,先记住这个意义,后面更新这个值的时候再说。计算好了窗口的层序值,如果当前这个窗口类型是一个子窗口类型的话,会调用父窗口WindowState的addChild方法来把子窗口插入到父窗口中,这个时候前面计算的窗口子序值mSubLayer就有用了,如果是非子窗口类型的话,会根据主序值mBaseLayer来进行窗口的排序。非子窗口类型的WindowState会添加到他WindowToken中,这个在后面会看到。我们这里先来看子窗口类型添加到父窗口时候的排序方法,这调用addChild方法添加到父窗口的WindowState中,同样排序方法是一个lambda表达式sWindowSubLayerComparator:
```java
private static final Comparator<WindowState> sWindowSubLayerComparator =
new Comparator<WindowState>() {
@Override
public int compare(WindowState w1, WindowState w2) {
final int layer1 = w1.mSubLayer;
final int layer2 = w2.mSubLayer;
if (layer1 < layer2 || (layer1 == layer2 && layer2 < 0 )) {
// We insert the child window into the list ordered by
// the sub-layer. For same sub-layers, the negative one
// should go below others; the positive one should go
// above others.
return -1;
}
return 1;
};
};
```
可以看到这里就是根据两个窗口的子序值mSubLayer来排序的,数值大的放在上面也就里用户越近,这个方法很简单不用多说,非子窗口类型的添加会在下面看到。这里WindowState的构造方法最后还会初始化一些窗口位置,动画等变量。我回到前面WMS的addWindow方法继续往后看。
创建完WindowState后,会再检查下WindowState中是否已经赋值了死亡监听,是否有DisplayContent等,接着会再调用WindowManagerPolicy的adjustWindowParamsLw方法,这个方法会根据当前这个窗口在调整一些属性,我们看下这个方法,这个方法是在WindowManagerPolicy的实现类PhoneWindowManager中的:
```java
// 根据添加窗口的类型,调整窗口的一些属性
// 比如有些窗口是不可接受输入事件的,锁屏是否要显示在其他界面上面
// toast的显示时间,窗口是否显示延伸到状态栏和导航栏的下面
@Override
public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
// 看下窗口是什么类型的
switch (attrs.type) {
case TYPE_SYSTEM_OVERLAY:
case TYPE_SECURE_SYSTEM_OVERLAY:
// These types of windows can't receive input events.
// 这2种类型窗口不能接受输入事件
attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
attrs.flags &= ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
break;
case TYPE_STATUS_BAR:
// 状态栏窗口
// If the Keyguard is in a hidden state (occluded by another window), we force to
// remove the wallpaper and keyguard flag so that any change in-flight after setting
// the keyguard as occluded wouldn't set these flags again.
// See {@link #processKeyguardSetHiddenResultLw}.
// 这里为true表示,锁屏界面可以被其他界面遮挡
if (mKeyguardOccluded) {
// 去掉显示壁纸界面标志
attrs.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
// 去掉不显示其他界面标志
attrs.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
}
break;
case TYPE_SCREENSHOT:
//截屏窗口不接受输入事件
attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
break;
case TYPE_TOAST:
// While apps should use the dedicated toast APIs to add such windows
// it possible legacy apps to add the window directly. Therefore, we
// make windows added directly by the app behave as a toast as much
// as possible in terms of timeout and animation.
// 如果toast显示时间小于0或者大于TOAST_WINDOW_TIMEOUT,设置为TOAST_WINDOW_TIMEOUT
if (attrs.hideTimeoutMilliseconds < 0
|| attrs.hideTimeoutMilliseconds > TOAST_WINDOW_TIMEOUT) {
attrs.hideTimeoutMilliseconds = TOAST_WINDOW_TIMEOUT;
}
// 设置窗口动画
attrs.windowAnimations = com.android.internal.R.style.Animation_Toast;
break;
}
if (attrs.type != TYPE_STATUS_BAR) {
// 如果不是TYPE_STATUS_BAR类型窗口,不能设置为PRIVATE_FLAG_KEYGUARD标志
// The status bar is the only window allowed to exhibit keyguard behavior.
attrs.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
}
// 如果在高端设备上使用硬件加速,下面会设置下状态栏和导航栏的背景色
if (ActivityManager.isHighEndGfx()) {
// 如果设置FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS背景标志
// 自动添加上SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION标志
// 这个标志表示会自动延伸Activity界面到导航栏下面,从而被导航栏遮挡
if ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0) {
attrs.subtreeSystemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
}
// 强制绘制状态栏背景色
final boolean forceWindowDrawsStatusBarBackground =
(attrs.privateFlags & PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND)
!= 0;
// 如果设置了FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS和PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND
// 并且窗口宽高都是MATCH_PARENT的,设置为全屏
if ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
|| forceWindowDrawsStatusBarBackground
&& attrs.height == MATCH_PARENT && attrs.width == MATCH_PARENT) {
attrs.subtreeSystemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
}
}
}
```
这个方法主要是处理几种特殊类型的窗口。比如有些窗口是不需要接受焦点事件的,有些是不需要接受输入事件的等,上面方法为这些窗口添加上相应的flag。
另外这里还给toast窗口添加了显示时间,可以看到Toast最多的显示时间这里设置了TOAST_WINDOW_TIMEOUT,这个值是3.5秒,所以我们自己设置toast的时间不是无限制的。
这个方法里用到了一个变量mKeyguardOccluded,这个变量代表着锁屏界面是否可以被遮挡,true表示被遮挡了,那么会去掉FLAG_SHOW_WALLPAPER和PRIVATE_FLAG_KEYGUARD这两个flag,FLAG_SHOW_WALLPAPER表示壁纸作为窗口的背景,锁屏被遮挡住了,说明有启动窗口显示在上面,所以壁纸也不能显示了。而PRIVATE_FLAG_KEYGUARD这个flag表示锁屏会显示在最前面,现在锁屏被遮挡了,所以也要去掉这个flag。从这里我们可以看到和锁屏有关的窗口状态其实是设置在状态栏的窗口里的,不过想想也有道理,锁屏情况下只有状态栏还是会显示的。
最后这个方法还会根据当前状态栏和导航栏的相关flag,设置他们背景色显示位置等属性,如果熟悉沉浸式状态栏的同学,对这些flag应该比较熟悉。好了,这个方法主要就是更新下这些特殊窗口的属性,我们回到前面addWindow方法。
上面更新了状态栏和导航栏的窗口属性后,如果当前添加的窗口是状态栏和导航栏的话,会做一些添加前的检查等,比较看下是否有权限等,调用的是PhoneWindowManager中的prepareAddWindowLw方法:
```java
public int prepareAddWindowLw(WindowState win, WindowManager.LayoutParams attrs) {
switch (attrs.type) {
case TYPE_STATUS_BAR:
// 是一个状态栏窗口,先检查是否有权限
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.STATUS_BAR_SERVICE,
"PhoneWindowManager");
// 如果已经有一个状态栏窗口的WindowState了
if (mStatusBar != null) {
// 如果这个窗口还在,return
if (mStatusBar.isAlive()) {
return WindowManagerGlobal.ADD_MULTIPLE_SINGLETON;
}
}
// 执行到这里,说明mStatusBar还是null,那么把添加的窗口设置给mStatusBar
mStatusBar = win;
// 设置状态栏窗口控制器
mStatusBarController.setWindow(win);
// 根据锁屏遮挡状态是否有改变,更新下状态栏窗口遮挡变量的值
// 如果当前锁屏界面正在显示,那么还要更新下状态栏窗口flag的值
setKeyguardOccludedLw(mKeyguardOccluded, true /* force */);
break;
case TYPE_NAVIGATION_BAR:
// 是一个导航栏窗口,先检查是否有权限
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.STATUS_BAR_SERVICE,
"PhoneWindowManager");
// 导航栏状态存在,return
if (mNavigationBar != null) {
if (mNavigationBar.isAlive()) {
return WindowManagerGlobal.ADD_MULTIPLE_SINGLETON;
}
}
// 到这里说明没有导航栏窗口,先设置导航栏窗口
mNavigationBar = win;
mNavigationBarController.setWindow(win);
// 设置导航栏窗口显示变化的监听
mNavigationBarController.setOnBarVisibilityChangedListener(
mNavBarVisibilityListener, true);
if (DEBUG_LAYOUT) Slog.i(TAG, "NAVIGATION BAR: " + mNavigationBar);
break;
case TYPE_NAVIGATION_BAR_PANEL:
case TYPE_STATUS_BAR_PANEL:
case TYPE_STATUS_BAR_SUB_PANEL:
case TYPE_VOICE_INTERACTION_STARTING:
// 检查其他窗口是否有权限
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.STATUS_BAR_SERVICE,
"PhoneWindowManager");
break;
}
// 执行到这里,说明下面可以继续添加新窗口
return ADD_OKAY;
}
```
这个方法主要是处理状态栏和导航栏这2个窗口,首先会调用enforceCallingOrSelfPermission方法检查下是否有权限添加窗口,如果有的话,在PhoneWindowManager中保存了状态栏和导航栏的WindowState的对象,如果这个对象不为空的话,并且当前还存活的话,说明已经添加过状态栏和导航栏了。否则的话会把当前的状态栏或者导航栏的WindowState保存在PhoneWindowManager中,然后状态栏的话还是更新一些锁屏相关的设置,导航栏的话会添加状态变化的监听等等,这些不多说。这个方法主要是检查权限以及看看是否有添加过状态栏和导航栏,如果添加过了,后面的添加流程就不继续了,如果没有添加过那么继续后的添加窗口流程。我们这里主要还是说添加Activity流程的,所以继续看后面的代码。
```java
.................
// 如果有接受输入事件的channel,并且没有禁用输入,那么会把输入channel注册给WindowState
final boolean openInputChannels = (outInputChannel != null
&& (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);
// 需要接受输入事件
if (openInputChannels) {
// 注册输入channel
win.openInputChannel(outInputChannel);
}
if (type == TYPE_TOAST) {
// 根据canAddToastWindowForUid方法,看看是否能再添加toast,如果一个相同uid的app显示在前台
// 那么可以添加。否则没有相同显示在前台uid的话,如果已经有toast了,不添加
if (!getDefaultDisplayContentLocked().canAddToastWindowForUid(callingUid)) {
Slog.w(TAG_WM, "Adding more than one toast window for UID at a time.");
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
// Make sure this happens before we moved focus as one can make the
// toast focusable to force it not being hidden after the timeout.
// Focusable toasts are always timed out to prevent a focused app to
// show a focusable toasts while it has focus which will be kept on
// the screen after the activity goes away.
// 到这里在添加窗口前先设置一个toast消失的时间,通过Handler来设置
// 如果添加toast需要token(android O后对添加toast进行了限制,看doesAddToastWindowRequireToken方法)
if (addToastWindowRequiresToken
|| (attrs.flags & LayoutParams.FLAG_NOT_FOCUSABLE) == 0 // 不可获取输入焦点
|| mCurrentFocus == null // 当前聚焦窗口是null
|| mCurrentFocus.mOwnerUid != callingUid) { // 调用者和当前显示前台app的uid不一样
// 以上四个条件符合一个的话,
mH.sendMessageDelayed(
mH.obtainMessage(H.WINDOW_HIDE_TIMEOUT, win),
win.mAttrs.hideTimeoutMilliseconds);
}
}
res = WindowManagerGlobal.ADD_OKAY;
// 如果前台窗口为空,把添加的windowState添加到中
if (mCurrentFocus == null) {
mWinAddedSinceNullFocus.add(win);
}
// 如果窗口类型是状态栏,导航栏或者输入法这三种类型,把win添加到mTapExcludedWindows集合中
if (excludeWindowTypeFromTapOutTask(type)) {
displayContent.mTapExcludedWindows.add(win);
}
origId = Binder.clearCallingIdentity();
// 创建surface flinger
// 把session加入到WMS的session集合中
// 更新ValueAnimator的缩放值
win.attach();
// key是viewRoot的W,win是WindowState
// mWindowMap是在WMS中的,所以这里是在WMS中保存了所有的WindowState
mWindowMap.put(client.asBinder(), win);
// 如果这个windowState是有运行时权限的
if (win.mAppOp != AppOpsManager.OP_NONE) {
// 获取是否有权限启动这个窗口,比如有些系统界面打开需要对应模块的权限,像
// 读取联系人,定位等
int startOpResult = mAppOps.startOpNoThrow(win.mAppOp, win.getOwningUid(),
win.getOwningPackage());
// 如果没有权限
if ((startOpResult != AppOpsManager.MODE_ALLOWED) &&
(startOpResult != AppOpsManager.MODE_DEFAULT)) {
// 设置窗口不可见
win.setAppOpVisibilityLw(false);
}
}
// 取出Activity的token
final AppWindowToken aToken = token.asAppWindowToken();
// 如果是一个启动窗口
if (type == TYPE_APPLICATION_STARTING && aToken != null) {
// 把WindowState设置给AppWindowToken的startingWindow
aToken.startingWindow = win;
if (DEBUG_STARTING_WINDOW)
Slog.v(TAG_WM, "addWindow: " + aToken
+ " startingWindow=" + win);
}
// 是否输入法需要移动,默认为ture
// 表示需要需要移动,比如新添加了一个窗口
// 而老的窗口输入法显示着,那么新窗口打开后
// 自然输入法的窗口需要改变
// 这里true可以理解为需要后面再处理输入法相关窗口
boolean imMayMove = true;
// 把添加的窗口windowState添加到他的windowToken子元素容器中
win.mToken.addWindow(win);
...................
```
WMS的addWindow方法确实是非常的长,这里是第三部分代码,我们看一下。如果这个窗口需要接受用户的输入事件的,那么这里会把传过来的输入缓冲器注册到WMS的InputManagerService中,这样客户端和native层就可以通过这个缓冲区来通信,关于输入事件模块也是一个非常复杂的模块,这里我们先了解下这里代码的大概含义,后面有时间我们会再分析下输入事件模块的。
# 判断Toast是否已经添加过
在InputManagerService中注册了这个窗口的事件缓冲区后,接着开始处理Toast窗口了,这里主要是判断下当前是否已经有Toast被添加了,如果有的话,也不会继续下去了,这里判断调用的是DisplayContent的canAddToastWindowForUid方法:
```java
// 对于这个uid,是否可以添加toast窗口
boolean canAddToastWindowForUid(int uid) {
// We allow one toast window per UID being shown at a time.
// Also if the app is focused adding more than one toast at
// a time for better backwards compatibility.
// 获取是参数uid的聚焦窗口WindowState,这里getWindow方法最后要
// 看WindowState里面的实现
// 寻找一个相同uid并且当前聚焦的WindowState
final WindowState focusedWindowForUid = getWindow(w ->
w.mOwnerUid == uid && w.isFocused());
// 如果当前有相同uid的app是显示在前台的,那么可以添加Toast
if (focusedWindowForUid != null) {
return true;
}
// 走到这里说明前面没有相同显示在前台的uid的app,那么要看看是否已经有相同uid的toast了,如果有了,不再添加
// 寻找一个toast类型的,相同uid,非只显示1次的窗口,非运行延迟的
final WindowState win = getWindow(w ->
w.mAttrs.type == TYPE_TOAST && w.mOwnerUid == uid && !w.mPermanentlyHidden
&& !w.mWindowRemovalAllowed);
// 如果找到了win,说明已经有了,返回false,不能在添加了
return win == null;
}
```
这个方法主要是判断对于相同uid的Toast,是否能继续添加,这里会调用getWindow来寻找一个WindowState,如果找到了说明已经添加过了,就返回了。这里getWindow方法的参数我们看到是一个Lambda表达式,表示的是相同uid且当前正在前台显示的。这里参数既然是一个Lambda表达式,我们看下getWindow方法的实现:
```java
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;
}
```
可以看到这里方法的参数是Predicate,既然前面直接可以用Lambda表达式作为参数,那么Predicate我们猜测肯定是只有一个函数的接口,我们看一下:
```java
public interface Predicate<T> {
boolean apply(T t);
}
```
确地是这样,这个是java 8后的新特性,如果接口只有一个方法的话,就可以用lambda表达式来直接表示,这个知识点稍微提一下。我们继续看getWindow这个方法的实现。
从调用处可以看到,这个getWindow是由DisplayConten对象来调用的,我们前面介绍过在每个窗口在DisplayContent中都用一个WindowToken来表示(子窗口的由他父窗口的WindowToken表示),在创建一个WindowToken的时候就会把自己添加到DisplayContent中,这个流程前面可以参看前面的代码,这里稍微提一下,记得在第一篇分析WMS文章里说过DisplayContent中有四个保存WindowToken的WindowContainer吗,当new一个WindowToken的时候最终会把自己保存在DisplayContent这四个WindowContainer中的一个,比如我们这里讨论的Toast的WindowToken会保存在DisplayContent的mAboveAppWindowsContainers这个WindowContainer中,而每个WindowToken也是一个WindowContainer的子类,他里面保存的是WindowState,所以我们回到上面getWindow方法,这里会遍历DisplayContent中的元素,最终会遍历到一个WindowState,由于WindowState也是一个WindowContainer的子类,他重写了getWindow这个方法,所以最终会根据这里参数给出的条件,返回一个满足条件的WindowState。所以这里是一个递归的过程,从DisplayContent到WindowToken,再到WindowState,每一个都是WindowContainer的子类,由于判断的条件是和WindowState有关的,所以只有WindowState重写了getWindow方法,从而可以返回满足条件的WindowState。设计的非常巧妙,大家可以仔细体会下。
看过了getWindow这个方法的逻辑,我们回到前面DisplayContent的canAddToastWindowForUid方法,主要看看要寻找WindowState的条件是什么。首先的条件是w.mOwnerUid == uid && w.isFocused(),由于同一个uid只能有一个toast在前面显示,所以这个判断是不是这个uid的toast已经在前面了,如果找到一个满足条件的就返回了,不需要添加这个Toast窗口了。接着如果没找到相同uid且在前台显示的Toast,那么会再找相同uid,但是并且非移除的并且只会显示1次的Toast,WindowState中有个mPermanentlyHidden变量,为true的话,表示这个Toast只允许显示一次,所以不可复用,如果为false那么就表示还可以继续用,默认是false。所以如果找到符合这些条件的后台Toast那么也不用添加,其余会继续添加一个新的Toast,如果继续添加一个Toast窗口的话,最后还会发送一个隐藏Toast的Handler消息,到时间后会隐藏Toast。好了,我们继续看WMS的addWindow方法。
接着如果是状态栏,导航栏,输入法窗口,这里还会把这三种窗口的WindowState保存到DisplayContent的一个集合中,这三种状态由于比较特殊都是会显示在当前用户内容的窗口上面,所以把他保存到DisplayContent中,好根据窗口的参数处理相关位置的操作。
之后会调用WindowState的attach方法,这个方法中会创建SurfaceFlinger以及Session添加到WMS中,我们看下这个方法:
```java
void attach() {
if (localLOGV) Slog.v(TAG, "Attaching " + this + " token=" + mToken);
mSession.windowAddedLocked(mAttrs.packageName);
}
```
这里继续调用Session的windowAddedLocked方法:
```java
void windowAddedLocked(String packageName) {
mPackageName = packageName; // 包名
mRelayoutTag = "relayoutWindow: " + mPackageName;
// 如果surface flinger为null
if (mSurfaceSession == null) {
if (WindowManagerService.localLOGV) Slog.v(
TAG_WM, "First window added to " + this + ", creating SurfaceSession");
// 创建surface flinger
mSurfaceSession = new SurfaceSession();
if (SHOW_TRANSACTIONS) Slog.i(
TAG_WM, " NEW SURFACE SESSION " + mSurfaceSession);
// 把当前Session加入WMS的mSessions集合中
mService.mSessions.add(this);
// 上次WMS中ValueAnimator的缩放值和现在WMS中不一样
if (mLastReportedAnimatorScale != mService.getCurrentAnimatorScale()) {
// 更新ValueAnimator的缩放值
mService.dispatchNewAnimatorScaleLocked(this);
}
}
mNumWindow++; // 窗口数量+1
}
```
我们看到这里会创建一个SurfaceSession,他是操作Surface相关的类,我们这里先不说,后面分析View模块的时候再说。然后会把当前这个窗口的Session添加到WMS的mSessions这个集合中,关于Session我们之前介绍过了,他里面持有了一个ViewRootImpl,ViewRootImpl是操作客户端View的,所以这里把窗口保存到WMS中,也是便于WMS可以通过Session来直接操作View。最后更下动画缩放值以及记录下这个Session添加过窗口的数量就好l。
回到WMS的addWindow方法后,接着可以看到会把当前这个WindowState添加到WMS的mWindowMap中了,这样窗口在WMS中的对象就被保存了。然后我们之前第二篇文章有介绍过启动窗口,启动窗口他是有一个AppWindowToken的,属于具体要启动的那个Activity,所以如果当前是一个启动窗口的话,会把WindowState赋值给AppWindowToken的startingWindow字段。
前面我们说DisplayContent中会保存有每个窗口的WindowToken,而每个WindowToken中会保存这个窗口的WindowState,所以最后还要把WindowState添加到对应这个窗口的WindowToken中。前面在创建WindowState的时候有分析过,如果是一个子窗口类型的话,会把该窗口的WindowState添加到父窗口的WindowState中,但是没有处理非子窗口类型的WindowState,在这里把WindowState添加到他对应的WindowToken中的时候就会处理非子类型的,我们看下方法:
```java
// 把新窗口windowState添加到windowToken子元素容器中
void addWindow(final WindowState win) {
// 如果这个windowState是子窗口类型的的,那么不需要添加,需要添加到他父的里面,比如一个Activity的WindowToken
if (win.isChildWindow()) {
// Child windows are added to their parent windows.
return;
}
// 如果这个子WindowContainer中没有这个WindowState,那么添加到mChildren中
if (!mChildren.contains(win)) {
if (DEBUG_ADD_REMOVE) Slog.v(TAG_WM, "Adding " + win + " to " + this);
// 把windowState添加到容器中,按照z轴主序顺序插入
addChild(win, mWindowComparator);
mService.mWindowsChanged = true;// 设置窗口有变化
// TODO: Should we also be setting layout needed here and other places?
}
}
```
可以看到这里开始会检查这个窗口是否是一个子窗口,如果是子窗口就return。否则会调用addChild方法添加到WindowToken中,这里addChild方法的第二个参数mWindowComparator也就是添加时候的比较器,我们看一下
```java
private final Comparator<WindowState> mWindowComparator =
(WindowState newWindow, WindowState existingWindow) -> {
final WindowToken token = WindowToken.this;
if (newWindow.mToken != token) {
throw new IllegalArgumentException("newWindow=" + newWindow
+ " is not a child of token=" + token);
}
if (existingWindow.mToken != token) {
throw new IllegalArgumentException("existingWindow=" + existingWindow
+ " is not a child of token=" + token);
}
return isFirstChildWindowGreaterThanSecond(newWindow, existingWindow) ? 1 : -1;
};
protected boolean isFirstChildWindowGreaterThanSecond(WindowState newWindow,
WindowState existingWindow) {
// New window is considered greater if it has a higher or equal base layer.
return newWindow.mBaseLayer >= existingWindow.mBaseLayer;
}
```
可以看到,这里检查了下两个比较的窗口WindowToken是不是当前这个,如果没问题的话会调用isFirstChildWindowGreaterThanSecond方法,这个方法就是比较他们的主序值了,主序值的计算在前面也分析过了。总的来说如果是子窗口类型的话,窗口的WindowState会添加到他父窗口的WindowState中,比较的是子序值。如果非子窗口类型的话,会添加到他对应的WindowToken中,比较的是他们的主序值。好了,这段代码分析完了,我们继续看下一部分代码:
```java
...........
// 如果是输入法窗口
if (type == TYPE_INPUT_METHOD) {
// 等待给出insets数据
win.mGivenInsetsPending = true;
// 设置输入法窗口给WMS,并且寻找和设置输入法的目标窗口
setInputMethodWindowLocked(win);
imMayMove = false; // 不需要后面再处理输入法相关窗口了
} else if (type == TYPE_INPUT_METHOD_DIALOG) {
// 输入法对话框,出现在上面的TYPE_INPUT_METHOD窗口上面
// 寻找和设置输入法的目标窗口
displayContent.computeImeTarget(true /* updateImeTarget */);
imMayMove = false; // 不需要后面再处理输入法相关窗口了
} else {
...............
}
.......
```
从这里开始对于前面新添加窗口的主要流程基本差不多了,不过后面还有一大段的代码,这些代码是对于当前窗口进行的一系列调整,由于新窗口的添加,对于老窗口来说可能会有一些影响的,比如下面我们会说到的输入法窗口,当一个新窗口打开后,如果原来显示在最上面的是输入法窗口,而新窗口可能默认进入后也是需要获得输入法焦点的,所以需要调整输入法窗口的位置,输入法窗口不像Activity,可以打开一个全新的窗口,输入法窗口只有一个,一旦新窗口打开后需要打开输入法窗口,那么就需要把输入法窗口原来的位置调整到最上面,这个就是我们说的对窗口的调整,我们下面继续看上面的代码。
# 处理和输入法窗口相关
这里有个imMayMove变量,默认是true,表示需要当前需要对输入法窗口进行调整,如果当前添加的窗口本身就是输入法窗口的话,会调用setInputMethodWindowLocked方法:
```java
void setInputMethodWindowLocked(WindowState win) {
// 设置给输入法窗口
mInputMethodWindow = win;
// 如果这个WindowState非null,获取他所在DisplayContent否则获取默认的displayContent
final DisplayContent dc = win != null
? win.getDisplayContent()
: getDefaultDisplayContentLocked();
// 寻找和设置输入法的目标窗口
dc.computeImeTarget(true /* updateImeTarget */);
}
```
这个方法最终会调用DisplayContent的computeImeTarget方法,这个方法是寻找到当前这个输入法的目标窗口是哪个,所谓目标窗口就是比如我们在一个Activity的EditText中点击弹出输入法,这个Activity就是目标窗口,我们来看下computeImeTarget这个方法:
```java
WindowState computeImeTarget(boolean updateImeTarget) {
// 如果当前输入法窗口为null,没有输入法,自然也不用寻找他的目标窗口了
// 根据是否要更新目标窗口字段,最后return
if (mService.mInputMethodWindow == null) {
// There isn't an IME so there shouldn't be a target...That was easy!
if (updateImeTarget) { // 但是要更新输入法目标窗口,那么下面方法会更新null给输入法目标窗口
if (DEBUG_INPUT_METHOD) Slog.w(TAG_WM, "Moving IM target from "
+ mService.mInputMethodTarget + " to null since mInputMethodWindow is null");
// 设置null给输入法目标窗口,更新这个目标窗口的z序以及这个屏幕里其他窗口的z序
setInputMethodTarget(null, mService.mInputMethodTargetWaitingAnim, 0);
}
return null;
}
.................
}
```
这个寻找输入法目标窗口的方法我也分几部分来看。上面这部分,首先判断下如果当前WMS中mInputMethodWindow为空,表示还没有输入法窗口,那么也自然不会有输入法目标窗口,返回为null。这里如果需要更新输入法目标窗口的话,就会调用setInputMethodTarget方法把一个null设置给当前目标窗口,我们也跟一下这个方法:
```java
private void setInputMethodTarget(WindowState target, boolean targetWaitingAnim, int layerAdj) {
// 如果要设置的输入法窗口和现在保存在WMS中是一样的,并且正在等待切换动画中,z序也相同,return
if (target == mService.mInputMethodTarget
&& mService.mInputMethodTargetWaitingAnim == targetWaitingAnim
&& mInputMethodAnimLayerAdjustment == layerAdj) {
return;
}
// 更新输入法目标窗口WindowState
mService.mInputMethodTarget = target;
// 是否等待切换动画中
mService.mInputMethodTargetWaitingAnim = targetWaitingAnim;
// 更新输入法窗口的z序
setInputMethodAnimLayerAdjustment(layerAdj);
// 更新屏幕里窗口的Z序
assignWindowLayers(false /* setLayoutNeeded */);
}
```
这里首先判断下设置的输入法目标窗口和当前的是否相同,包括是否都是等等切换动画中以及输入法窗口z轴层序调整值都相同,这里都相同的话说明不需要更新。
如果有不一样,下面会分别设置输入法目标床,是否等待切换动画状态和z轴层序的调整值。前面我们说过一个窗口最终实际的Z轴层序是mLayer表示的,实际上最终可能还需要加上一个调整值 ,比如说输入法窗口,上面方法中会调用setInputMethodAnimLayerAdjustment方法把输入法最终的一个调整值保存到DisplayContent的mInputMethodAnimLayerAdjustment中,这个调整值只有Activity中才有,一般输入法也是显示在Activity上面的,所以这个调整值确保输入法窗口显示在Activity上面。
如果能执行到这里,说明有一个新窗口加入了,最后这里要调用assignWindowLayers调整下当前窗口中窗口在Z轴上层序值:
```java
void assignWindowLayers(boolean setLayoutNeeded) {
// 更新这个屏幕里的windowState的z序
mLayersController.assignWindowLayers(this);
// 是否要重新布局
if (setLayoutNeeded) {
setLayoutNeeded();
}
}
```
这个方法会调用WindowLayersController的assignWindowLayers,之后标记下后面View刷新的时候重新测量,布局和绘制,我们跟进assignWindowLayers方法看下:
```java
// 更新这个屏幕里windowState的z序
final void assignWindowLayers(DisplayContent dc) {
if (DEBUG_LAYERS) Slog.v(TAG_WM, "Assigning layers based",
new RuntimeException("here").fillInStackTrace());
reset();
// 遍历所有子元素,最终调用WindowState的mAssignWindowLayersConsumer方法
// 这里参数二false表示是从0往后开始遍历
dc.forAllWindows(mAssignWindowLayersConsumer, false /* traverseTopToBottom */);
// 调整特殊窗口的z序
adjustSpecialWindows();
//TODO (multidisplay): Magnification is supported only for the default display.
if (mService.mAccessibilityController != null && mAnyLayerChanged
&& dc.getDisplayId() == DEFAULT_DISPLAY) {
mService.mAccessibilityController.onWindowLayersChangedLocked();
}
if (DEBUG_LAYERS) logDebugLayers(dc);
}
```
这个方法里首先是调用reset重置下当前和窗口层序有关的一些属性变量。之后会遍历屏幕中所有窗口,更加所有WindowState的主序和子序等信息。最后调用adjustSpecialWindows方法再针对特殊窗口更新下窗口的层序。这些方法由于很多都属于是View或者Surface方面相关的了,我们这就稍微看下这几个方法,大致了解他们做了什么,相关更深入的分析我们放到后面分析View或者Surface后再说。首先看下这里reset方法:
```java
private void reset() {
mHighestApplicationLayer = 0;
mPinnedWindows.clear();
mInputMethodWindows.clear();
mDockedWindows.clear();
mAssistantWindows.clear();
mReplacingWindows.clear();
mDockDivider = null;
mCurBaseLayer = 0;
mCurLayer = 0;
mAnyLayerChanged = false;
// 当前的输入法窗口目标窗口
mImeTarget = mService.mInputMethodTarget;
// 当前输入法目标窗口的最大z序值
mHighestLayerInImeTargetBaseLayer = (mImeTarget != null) ? mImeTarget.mBaseLayer : 0;
mAboveImeTarget = false;
mAboveImeTargetAppWindows.clear();
}
```
这个方法开始我们可以看到会clear几个集合,从这几个集合的名字可以看出都是一些不同类型的窗口,比如输入法,分屏等等。之后会把当前层序值置0,之后还会保存输入法目标窗口,和目标窗口的主序,这些不多解释。之后会调用DisplayContent的forAllWindows方法,这个方法有两个参数,第一个参数是一个lambda表达式,这里作为一个参数传入最后会执行这个表达。第二个参数是个bool值,表示这个方法中的执行顺序,我们看下这个方法:
```java
void forAllWindows(Consumer<WindowState> callback, boolean traverseTopToBottom) {
// 封装callbal,这个callback实际是一个方法
ForAllWindowsConsumerWrapper wrapper = obtainConsumerWrapper(callback);
// 调用上面的forAllWindows
forAllWindows(wrapper, traverseTopToBottom);
// 释放这个封装类
wrapper.release();
}
```
这里会继续调用forAllWindows方法:
```java
boolean forAllWindows(ToBooleanFunction<WindowState> callback, boolean traverseTopToBottom) {
if (traverseTopToBottom) {
// 这里遍历所有子元素,最终遍历到的是WindowState类,调用他的forAllWindows方法
// 而这个方法最终会调用callback的实现方法
for (int i = mChildren.size() - 1; i >= 0; --i) {
if (mChildren.get(i).forAllWindows(callback, traverseTopToBottom)) {
return true;
}
}
} else {
final int count = mChildren.size();
for (int i = 0; i < count; i++) {
if (mChildren.get(i).forAllWindows(callback, traverseTopToBottom)) {
return true;
}
}
}
return false;
}
```
这个方法可以看到根据第二个参数如果是true会对DisplayContent中的元素反向遍历,false会正向遍历。我们直到DisplayContent中最终保存的是WindowState对象,所以最终会对WindowState对象调用这里参数一callback,这里callback的实现是一个lambda表达式,我们看看这个表达式做了些什么:
```java
private final Consumer<WindowState> mAssignWindowLayersConsumer = w -> {
// 当前这个WindowState的layer是否有变化
boolean layerChanged = false;
// 先保存下遍历到的这个WindowState的z序值
int oldLayer = w.mLayer;
// 如果遍历到这个WindowState的主序值和前一个遍历到的一样
if (w.mBaseLayer == mCurBaseLayer) {
// 那么在前一个z序值的基础上+5
mCurLayer += WINDOW_LAYER_MULTIPLIER;
} else {
// 如果遍历到的这个主序和前一个不一样
// 用主序来作为他的z序值
mCurBaseLayer = mCurLayer = w.mBaseLayer;
}
// 保存z序到这个WindowState中,更新他的z序和动画z序
assignAnimLayer(w, mCurLayer);
// TODO: Preserved old behavior of code here but not sure comparing oldLayer to
// mAnimLayer and mLayer makes sense...though the worst case would be unintentional
// layer reassignment.
// 如果这个窗口的z序或者动画z序有变化,标记一下
if (w.mLayer != oldLayer || w.mWinAnimator.mAnimLayer != oldLayer) {
layerChanged = true;
mAnyLayerChanged = true;
}
// 这个windowState有Activity
if (w.mAppToken != null) {
// 保存最大的那个z序值,因为最大的表示当前焦点窗口
// 这里是所有有Activity的窗口中的z序最大的值
mHighestApplicationLayer = Math.max(mHighestApplicationLayer,
w.mWinAnimator.mAnimLayer);
}
// 如果当前输入法窗口目标窗口非null同时当前输入法目标窗口的主序和现在遍历到的这个窗口一样
if (mImeTarget != null && w.mBaseLayer == mImeTarget.mBaseLayer) {
// 更新输入法目标窗口的最大z序值
mHighestLayerInImeTargetBaseLayer = Math.max(mHighestLayerInImeTargetBaseLayer,
w.mWinAnimator.mAnimLayer);
}
// 收集一些特殊的窗口,比如分屏模式分界线,输入法窗口,画中画等
// 保存起来
collectSpecialWindows(w);
// 如果遍历到的windowState的z序有变化,准备窗口的显示
if (layerChanged) {
w.scheduleAnimationIfDimming();
}
};
```
这里在遍历每个WindowState的过程中,会计算当前这个WindowState在z轴的序列,之后调用assignAnimLayer方法把这些层序保存在WindowState中,还会保存Activity和输入法目标窗口的在Surfaceflinger中的层序,即这里mAnimLayer值。最后调用collectSpecialWindows方法收集这些窗口。
这里我们可以看到在遍历每个WindowState的过程中,如果主序一样的话,每个WindowState会在上一个的基础上加上WINDOW_LAYER_MULTIPLIER(值是5),最终这里的mCurLayer就是接近于最终在Surface中绘制的层序了,为什么说接近呢,因为最终可能还会加上一个调整的偏移量,比如像前面我们说到的更新输入法窗口的时候会设置一个调整的值,这里我们可以看下assignAnimLayer方法:
```java
private void assignAnimLayer(WindowState w, int layer) {
// 更新这个窗口的z序
w.mLayer = layer;
// 设置这个窗口的动画z序,其实就是调整序+主序
w.mWinAnimator.mAnimLayer = w.getAnimLayerAdjustment()
+ w.getSpecialWindowAnimLayerAdjustment();
if (w.mAppToken != null && w.mAppToken.mAppAnimator.thumbnailForceAboveLayer > 0) {
// 如果是Activity的话,如果他的mAnimLayer比现在的thumbnailForceAboveLayer值大,更新下
if (w.mWinAnimator.mAnimLayer > w.mAppToken.mAppAnimator.thumbnailForceAboveLayer) {
w.mAppToken.mAppAnimator.thumbnailForceAboveLayer = w.mWinAnimator.mAnimLayer;
}
// TODO(b/62029108): the entire contents of the if statement should call the refactored
// function to set the thumbnail layer for w.AppToken
// 获取这个Activity中元素的最大动画z序
int highestLayer = w.mAppToken.getHighestAnimLayer();
if (highestLayer > 0) {
if (w.mAppToken.mAppAnimator.thumbnail != null
&& w.mAppToken.mAppAnimator.thumbnailForceAboveLayer != highestLayer) {
w.mAppToken.mAppAnimator.thumbnailForceAboveLayer = highestLayer;
w.mAppToken.mAppAnimator.thumbnail.setLayer(highestLayer + 1);
}
}
}
}
```
可以看到这里的参数layer就是当前计算出的窗口的层序,之后还会设置mAnimLayer这个层序,这个是最终的绘制时候执行的层序,如果没有调整的值,就和参数layer是一样的,否则会调用getAnimLayerAdjustment和getSpecialWindowAnimLayerAdjustment相加,我们getAnimLayerAdjustment就是从对应Activity中获取的调整值,比如说输入法的Activity。getSpecialWindowAnimLayerAdjustment是针对特殊窗口再增加一些偏移量的值,比如像输入法和壁纸,这些窗口需要在对应的基础上在往上或者往下偏移一些,因为这些窗口往往会显示在最上面或者最下面,这里就不深入了,涉及的东西其实挺多了,我们回到正题。
我们在看下collectSpecialWindows这个方法,这个方法会搜集一些特殊的窗口保存在WindowLayersController中:
```java
private void collectSpecialWindows(WindowState w) {
// 如果这个窗口是分屏模式下,窗口的分界线,保存在mDockDivider中,return
if (w.mAttrs.type == TYPE_DOCK_DIVIDER) {
mDockDivider = w;
return;
}
// 如果这个windowState的要被替换
if (w.mWillReplaceWindow) {
// 加入要被替换的集合
mReplacingWindows.add(w);
}
// 如果这个windowState是输入法窗口
if (w.mIsImWindow) {
// 加入
mInputMethodWindows.add(w);
return;
}
// 如果当前前台有最上面的输入法窗口
if (mImeTarget != null) {
if (w.getParentWindow() == mImeTarget && w.mSubLayer > 0) {
// 如果遍历到的这个windowState父窗口是当前的输入法窗口,并且子序时大于0,说明现实在当前父窗口的上面
// Child windows of the ime target with a positive sub-layer should be placed above
// the IME.
// 加入到显示在输入法窗口上面的集合中
mAboveImeTargetAppWindows.add(w);
} else if (mAboveImeTarget && w.mAppToken != null) {
// 如果是显示在输入法窗口上的,并且这个windowState有Activity
// 加入到显示在输入法窗口上面的集合中
// windows of apps above the IME target should be placed above the IME.
mAboveImeTargetAppWindows.add(w);
}
// 如果遍历到的这个windowState是输入法窗口,设置下mAboveImeTarget为true
// 由于遍历是从0开始的,所有后面遍历到的输入法窗口的子元素,就都会显示在这个窗口上面
if (w == mImeTarget) {
mAboveImeTarget = true;
}
}
final Task task = w.getTask();
if (task == null) {
return;
}
final TaskStack stack = task.mStack;
if (stack == null) {
return;
}
// 把windowState加入画中画
if (stack.mStackId == PINNED_STACK_ID) {
mPinnedWindows.add(w);
} else if (stack.mStackId == DOCKED_STACK_ID) {
// 加入分屏
mDockedWindows.add(w);
} else if (stack.mStackId == ASSISTANT_STACK_ID) {
mAssistantWindows.add(w);
}
}
```
这个方法主要就是把不同类型的创建添加到不同的集合中,这里我们主要看下处理输入法窗口的逻辑。在创建WindowLayersController这个类的时候,我们已经把当前找到的输入法目标窗口的WindowState保存在mImeTarget变量中,所以在当前mImeTarget非空的情况下,如果遍历到一个输入法目标窗口的子窗口,且子序是大于0的,所以这个窗口会显示在输入法目标窗口上面,所以把这些显示在输入法目标窗口上的窗口添加到mAboveImeTargetAppWindows集合中。如果正好遍历到输入法目标窗口,那么设置mAboveImeTarget为true,标记一下,之后遇到mAboveImeTarget为true且是一个Activity的窗口,说明都是输入法目标窗口上面,所以都会添加到mAboveImeTargetAppWindows集合。这里收集的这些窗口都是属于特殊类型的窗口,这些窗口一般都是显示在最上面的,所以这里收集这些窗口到集合中,后面会根据这些窗口的显示层序关系,重新设置他们的z轴层序。我们回到assignWindowLayers方法中,最后会调用adjustSpecialWindows方法,调整每种类型窗口的z轴的顺序:
```java
private void adjustSpecialWindows() {
// 获取最大的z序,后面画中画和分屏,想让他们在其他窗口z序最大值的上面
int layer = mHighestApplicationLayer + WINDOW_LAYER_MULTIPLIER;
// For pinned and docked stack window, we want to make them above other windows also when
// these windows are animating.
// 如果分屏窗口非null,更新这个分屏窗口的z序
while (!mDockedWindows.isEmpty()) {
// 更新分屏窗口的z序
layer = assignAndIncreaseLayerIfNeeded(mDockedWindows.remove(), layer);
}
// 更新分屏分界线窗口的z序
layer = assignAndIncreaseLayerIfNeeded(mDockDivider, layer);
// We know that we will be animating a relaunching window in the near future, which will
// receive a z-order increase. We want the replaced window to immediately receive the same
// treatment, e.g. to be above the dock divider.
// 如果有替换窗口,更新这些窗口的z序
while (!mReplacingWindows.isEmpty()) {
layer = assignAndIncreaseLayerIfNeeded(mReplacingWindows.remove(), layer);
}
// Adjust the assistant stack windows to be above the docked and fullscreen stack windows,
// but under the pinned stack windows
// 更新assistant窗口的z序
while (!mAssistantWindows.isEmpty()) {
layer = assignAndIncreaseLayerIfNeeded(mAssistantWindows.remove(), layer);
}
// 更新画中画的z序
while (!mPinnedWindows.isEmpty()) {
layer = assignAndIncreaseLayerIfNeeded(mPinnedWindows.remove(), layer);
}
// Make sure IME is the highest window in the base layer of it's target.
// 如果有输入法窗口
if (mImeTarget != null) {
if (mImeTarget.mAppToken == null) {
// For non-app ime targets adjust the layer we start from to match what we found
// when assigning layers. Otherwise, just use the highest app layer we have some far.
// 计算输入法窗口的开始z序值
layer = mHighestLayerInImeTargetBaseLayer + WINDOW_LAYER_MULTIPLIER;
}
// 更新其他输入法窗口的z序
while (!mInputMethodWindows.isEmpty()) {
layer = assignAndIncreaseLayerIfNeeded(mInputMethodWindows.remove(), layer);
}
// Adjust app windows the should be displayed above the IME since they are above the IME
// target.
// 更新其他输入法窗口上面的窗口的z序
while (!mAboveImeTargetAppWindows.isEmpty()) {
layer = assignAndIncreaseLayerIfNeeded(mAboveImeTargetAppWindows.remove(), layer);
}
}
}
```
方法开头的mHighestApplicationLayer这个变量是在前面lambda表达式中调用assignAnimLayer方法计算出了当前显示在最上面的层序值,现在会重新更新下前面收集到的那些窗口,按照他们的先后顺序在mHighestApplicationLayer基础上调整顺序,使他们依次显示在屏幕的最上面。上面的方法都会调用assignAndIncreaseLayerIfNeeded方法来从这些集合中更新窗口Z轴顺序,最终也是调用到assignAnimLayer方法来更新,像这里我们看到分屏,画中画,输入法以及输入法以上窗口都依次重新计算Z轴的值,这样在后面SurfaceFlinger绘制他们的时候就会依据这些值。
我们上面说了那么多,回到最开始我们是要寻找当前输入法窗口的目标窗口的方法DisplayContent的computeImeTarget方法中,前面说到如果没有输入法窗口的话,也不存在输入法目标窗口,返回null。我们接着看其他情况:
```java
WindowState computeImeTarget(boolean updateImeTarget) {
..................
mUpdateImeTarget = updateImeTarget;
// 获取输入法的目标窗口
WindowState target = getWindow(mComputeImeTargetPredicate);
..................
}
```
这里第二段代码,会在DisplayContent中寻找有效的输入法目标窗口,这里getWindow方法是遍历DisplayContent所有元素,最终会通过lambda表达式mComputeImeTargetPredicate来返回找到的目标窗口,经过之前的分析,这个讨论我们都很熟悉了,直接看lambda表达式mComputeImeTargetPredicate:
```java
private final Predicate<WindowState> mComputeImeTargetPredicate = w -> {
if (DEBUG_INPUT_METHOD && mUpdateImeTarget) Slog.i(TAG_WM, "Checking window @" + w
+ " fl=0x" + Integer.toHexString(w.mAttrs.flags));
return w.canBeImeTarget();
};
```
这个方法会继续调用WindowState的canBeImeTarget:
```java
// 是否能够成为一个输入法的目标窗口
boolean canBeImeTarget() {
// 如果这个窗口本身是一个输入法窗口,不能成为输入法的宿主窗口,返回false
if (mIsImWindow) {
return false;
}
// 是否这个窗口可以接受输入输入事件,以及可以和输入法窗口交互
final int fl = mAttrs.flags & (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM);
// 这个窗口的类型
final int type = mAttrs.type;
// Can only be an IME target if both FLAG_NOT_FOCUSABLE and FLAG_ALT_FOCUSABLE_IM are set or
// both are cleared...and not a starting window.
// 如果这个窗口不是是可以接受输入事件,并且不是可以和输入法交互的,并且不是启动窗口
// 那大概率会是一个应用窗口,想想一个应用窗口都不能接受输入事件也不能和输入法交互,那么肯定
// 不能作为输入法的目标窗口,返回false
if (fl != 0 && fl != (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM)
&& type != TYPE_APPLICATION_STARTING) {
return false;
}
// 执行到这里了,最后在看看窗口是否是可见的
return isVisibleOrAdding();
}
```
这个方法就是判断当前WindowState是否能成为一个输入法目标窗口。首先这个窗口本身是一个输入法窗口,那么自然不可能成为输入法目标窗口,返回false。如果该窗口不可以和输入法窗口进行交互的话,那也不可以成为输入法目标窗口,也返回false,注意这里特地排除了启动窗口,如果是启动窗口类型的话,后面还会特殊处理,我们马上就会看到。最后会调用isVisibleOrAdding方法看看这个窗口是否已经可以显示了,或者正在添加过程中,我们也看下这个方法:
```java
boolean isVisibleOrAdding() {
final AppWindowToken atoken = mAppToken;
// 如果surface是存在的或者没有重新更新布局,即调用relayout方法但是view是可见状态
return (mHasSurface || (!mRelayoutCalled && mViewVisibility == View.VISIBLE))
// 同时这个窗口是可见的,他的父窗口非隐藏的
&& mPolicyVisibility && !isParentWindowHidden()
// 同时AppWindowToken是空或者Activity非隐藏的
&& (atoken == null || !atoken.hiddenRequested)
// 同时没有运行退出动画也没有销毁
&& !mAnimatingExit && !mDestroying;
}
```
这个方法就是通过一些状态来判断当前这个窗口是否能正常显示,如果能正常显示的话,就可以成为一个输入法目标窗口。比如这里mHasSurface非空,说明这个窗口的测量,布局,绘制流程已经好了,可以正常显示了。如果mRelayoutCalled是false的话,说明测量,布局,绘制虽然还没有好,但是状态是可见说,说明在添加的过程中。另外这里我们可以看到atoken为null的时候也可以成为一个输入法目标窗口,说明不一样一定是Activity才可以成为输入法目标窗口的。我们知道这个方法主要就是判断这个窗口可以正常显示,可以正常显示就说明找到了一个输入法目标窗口了。我们回到前面DisplayContent的computeImeTarget方法,继续看下面的流程:
```java
WindowState computeImeTarget(boolean updateImeTarget) {
.............
// 如果这个窗口是一个启动窗口
if (target != null && target.mAttrs.type == TYPE_APPLICATION_STARTING) {
// 获取启动窗口的AppWindowToken,说明这个启动窗口是一个Activiy的启动窗口
final AppWindowToken token = target.mAppToken;
if (token != null) { // AppWindowToken非null
// 从这个AppWindowToken容器中
final WindowState betterTarget = token.getImeTargetBelowWindow(target);
// 如果从启动窗口同一个AppWindowToken中找到了可以作为目标窗口的WindowState
// 赋值给target变量
if (betterTarget != null) {
target = betterTarget;
}
}
}
final WindowState curTarget = mService.mInputMethodTarget;
// 如果当前输入法的目标窗口非null,并且处于可见状态,同时正处于退出动画或者关闭状态
if (curTarget != null && curTarget.isDisplayedLw() && curTarget.isClosing()
// 如果没有找到其他的输入法窗口的目标窗口或者当前输入法窗口的目标窗口的z序值大于
// 上面找到的输入法窗口的目标窗口的z序值
&& (target == null
|| curTarget.mWinAnimator.mAnimLayer > target.mWinAnimator.mAnimLayer)) {
if (DEBUG_INPUT_METHOD) Slog.v(TAG_WM, "Current target higher, not changing");
// 那么还是返回当前这个输入法窗口的目标窗口
return curTarget;
}
// 执行到这里,说明上面当前输入法窗口的目标窗口也不能用
// 并且如果上面没有找到输入法窗口的目标窗口,而且需要更新输入法目标窗口
if (target == null) {
// 但是要求更新输入法目标窗口
if (updateImeTarget) {
if (DEBUG_INPUT_METHOD) Slog.w(TAG_WM, "Moving IM target from " + curTarget
+ " to null." + (SHOW_STACK_CRAWLS ? " Callers="
+ Debug.getCallers(4) : ""));
// 更新输入法目标窗口,以及他的z序和屏障中其他窗口的z序
setInputMethodTarget(null, mService.mInputMethodTargetWaitingAnim, 0);
}
// 没找到目标窗口返回null
return null;
}
..............
}
```
这段代码有几部分。第一部分,是判断寻找到的输入法目标窗口是否是一个启动窗口,启动窗口的话不能作为一个输入法目标床, 但是我们分析前面一段代码的时候说过会跳过启动窗口,这里就会特地来处理启动窗口,我们知道启动窗口是先于那个真正要启动的Activity启动的,所以会显示在前面,并且他的mAppToken是那个真正要启动的Activity的,所以这里会从真正要启动的Activity里寻找是否有输入法目标窗口,这里我们看到会调用AppWindowToken的getImeTargetBelowWindow方法来寻找:
```java
// 从这个AppWindowToken的容器中,寻找一个可以作为输入法目标窗口返回
// 这里参数w是一个启动窗口,肯定不能作为输入法的目标窗口,看看他下面一个
// 能不能用
WindowState getImeTargetBelowWindow(WindowState w) {
// 获取参数w在容器中的下标,这个参数是一个启动窗口
final int index = mChildren.indexOf(w);
if (index > 0) {
// 启动窗口不能作为输入法的目标窗口,取下一个
final WindowState target = mChildren.get(index - 1);
// 下一个窗口如果能够成为目标窗口,返回
if (target.canBeImeTarget()) {
return target;
}
}
return null;
}
```
启动窗口的WindowState也是保存在他要启动的Activity的AppWindowToken下面的,所以上面这个方法先获取启动窗口WindowState的下标,然后启动窗口下面的一个窗口就是将要显示的窗口,所以获取下一个窗口的WindowState,同样会调用WindowState的canBeImeTarget方法来判断这个窗口是否能成为启动窗口,能的话就返回这个窗口,否则返回null。好了,启动窗口处理好了,我们在看上面computeImeTarget方法中的流程。
虽然到目前我们可能已经找到一个输入法目标窗口target,但是还是和当前保存在WMS中的输入法目标窗口比较一下,看是用当前保存在WMS中的还是找到的输入法目标窗口。如果当前WMS保存了一个输入法目标窗口,虽然还在显示着,但是处理正在关闭的状态,那么就判断一下,如果也没找到一个新的输入法目标窗口,或者当前WMS中保存的这个输入法目标窗口在Z轴上显示的位置要高于找到的输入法目标窗口,那么还是返回当前WMS中的这个输入法目标窗口。如果当前WMS中的输入法目标窗口不满足上面这些要求,比如WMS中的这个输入法目标窗口目前没有正在关闭的状态,那么会再看下当前找到的这个输入法目标窗口是否为null,如果为null就返回了。
如果当前找到的这个输入法目标窗口为不null,那么会继续下面的流程,我们继续看代码:
```java
WindowState computeImeTarget(boolean updateImeTarget) {
................
// 执行到这里,说明找到了一个非null的目标窗口target,但是在这之前再检查下当前输入法目标窗口curTarget
// 所在的AppWindowToken容器内,是否有正在执行动画的窗口,如果有的话,那么把这种作为目标窗口,先不把前面找出的
// target作为目标窗口
if (updateImeTarget) {
// 先看看前面有没有找到当前的输入法目标窗口,获取他的AppWindowToken
AppWindowToken token = curTarget == null ? null : curTarget.mAppToken;
if (token != null) { // 如果当前有输入法目标窗口
WindowState highestTarget = null;
// 这个目标窗口正在执行动画,或者这个目标窗口的动画非null,表示要执行动画了
// 获取这个相同AppWindowToken中z序最高的
if (token.mAppAnimator.animating || token.mAppAnimator.animation != null) {
highestTarget = token.getHighestAnimLayerWindow(curTarget);
}
if (highestTarget != null) {
// 获取转场动画
final AppTransition appTransition = mService.mAppTransition;
// 有设置转场动画
if (appTransition.isTransitionSet()) {
// 先把输入法目标窗口设置这个要执行动画的窗口
setInputMethodTarget(highestTarget, true, mInputMethodAnimLayerAdjustment);
return highestTarget;
} else if (highestTarget.mWinAnimator.isAnimationSet() &&
highestTarget.mWinAnimator.mAnimLayer > target.mWinAnimator.mAnimLayer) {
// 如果如果WMS中没有设置转场动画,但是这个要执行动画的窗口里有动画,并且窗口的z序比前面找到
// 的目标窗口target高,那么目标窗口先设置为这个
setInputMethodTarget(highestTarget, true, mInputMethodAnimLayerAdjustment);
return highestTarget;
}
}
}
// 更新找到的这个目标窗口为最新的输入法目标窗口,当然还有更新z序等动作
setInputMethodTarget(target, false, target.mAppToken != null
? target.mAppToken.getAnimLayerAdjustment() : 0);
}
return target;
}
```
如果能够执行到上面这段代码说明目前可以找到一个输入法目标窗口,但是同时也说明了当前WMS中保存的输入法目标窗口不是关闭的状态,说明当前WMS中保存的当前输入法目标窗口还在正常运行,所以在使用新的目标窗口前,这个方法中会查找当前输入法目标窗口下是否还有窗口是正在执行转场动画的,也就是说这些窗口还是运行中,并不是保持静态等到用户操作的,所以会把输入法目标窗口先设置给这些还在"运动"中的窗口,等待他们完成后,都达到了一种"静态"状态后再重新设置输入法目标窗口。
如果没有这些还在"运动"中的窗口,那么最后就会设置前面找到的窗口作为输入法目标窗口。至此,寻找输入法目标窗口的流程就完成了,整个过程还是非常复杂的,有些细节的具体场景,我也没遇到过,对于输入法这块也并非是专门研究的,所以尽可能的根据代码所表达的以及平时开发中遇到的场景来猜测他们的含义,有不对之处还请指正。但是大概理解到这样对于我们研究WMS的流程来说,基本也够用,我们还是回到WMS中继续看添加窗口的流程。
```java
...........
if (type == TYPE_WALLPAPER) { // 壁纸窗口
// 重置壁纸显示的超时时间
displayContent.mWallpaperController.clearLastWallpaperTimeoutTime();
// 表示需要调整下壁纸窗口
displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
} else if ((attrs.flags & FLAG_SHOW_WALLPAPER) != 0) {
// FLAG_SHOW_WALLPAPER这个flag表示这个窗口显示在壁纸前,壁纸窗口显示在窗口后的这种
// 有点透明的效果,所以需要调整壁纸窗口
displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
} else if (displayContent.mWallpaperController.isBelowWallpaperTarget(win)) {
// 这个表示如果新添加的窗口在当前壁纸目标窗口的下面,所以就等于看不到了,所以需要调整下壁纸窗口
displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
}
.............
```
接着的代码会处理和壁纸相关的窗口。如果是壁纸窗口,除了清零壁纸窗口的超时时间外,这里会添加FINISH_LAYOUT_REDO_WALLPAPER到DisplayContent的pendingLayoutChanges变量中,这个flag表示该壁纸窗口的位置可能需要重新调整。同样后面的分析如果窗口的flag有FLAG_SHOW_WALLPAPER这个值的时候,也是需要添加FINISH_LAYOUT_REDO_WALLPAPER到DisplayContent的pendingLayoutChanges中,FLAG_SHOW_WALLPAPER这个flag我们前面在分析添加启动窗口的时候遇到过,表示使用壁纸作为一个窗口的背景,所以这里也是需要调整壁纸窗口。最后调用WallpaperController的isBelowWallpaperTarget方法查看是否当前添加的窗口在壁纸窗口的下面,如果在壁纸窗口的下面的话,也需要调整壁纸窗口,这部分我们大概知道下就可以,主要对添加不同窗口后,引起相关窗口间的变化,这里先加上flag。我们继续后面的代码:
```java
....................
// 如果这个窗口被添加到一个因为输入法被调整的taskstack中,那么这个窗口也要调整下
win.applyAdjustForImeIfNeeded();
// 如果是分屏的分割线
if (type == TYPE_DOCK_DIVIDER) {
// 设置分屏分割线窗口,以及他的dim层可见性
mRoot.getDisplayContent(displayId).getDockedDividerController().setWindow(win);
}
................
```
这里首先会判断如果当前这个窗口所在的TaskStack由于输入法的原因,有位置的移动,比如我们知道一个Activity中如果弹出输入法后,界面会往上偏移一段距离,所以这里在这个TaskStack中的Task是都会有影响的,所以这里会调用WindowState的applyAdjustForImeIfNeeded方法来调整,我们看下这个方法:
```java
// 如果这个窗口所在的taskStack是因为输入法被调整的,那么这个窗口的task也要被调整
void applyAdjustForImeIfNeeded() {
// 获取这个窗口所在的task
final Task task = getTask();
// 如果task非null,同时ActivityStack非null,并且需要因为输入法调整窗口
if (task != null && task.mStack != null && task.mStack.isAdjustedForIme()) {
// 调整task
task.mStack.applyAdjustForImeIfNeeded(task);
}
}
```
这里会判断TaskStack是否因为输入法原因要调整,是的话,就会调用TaskStack的applyAdjustForImeIfNeeded方法来调整,我们再跟进一下:
```java
// 根据输入法窗口调整的大小,也调整下taskstack的大小(如果需要的话)
void applyAdjustForImeIfNeeded(Task task) {
// 如果缩放的比例非0,表示还没缩放调整完,不能为输入法调整。或者没有因为输入法需要调整窗口,或者调整大小的rect非null
if (mMinimizeAmount != 0f || !mAdjustedForIme || mAdjustedBounds.isEmpty()) {
return;
}
// 如果当前输入法窗口离开了(即不显示),用mBounds来,否则表示显示着用mFullyAdjustedImeBounds
// 简单说如果没有输入法,那么taskStack的rect不变,还是原来的mBounds
// 如果有输入法,就是mFullyAdjustedImeBounds为task的rect了
final Rect insetBounds = mImeGoingAway ? mBounds : mFullyAdjustedImeBounds;
// 参数一 mAdjustedBounds要调整为的rect
// 参数二 insetBounds为taskStack将要变成的rect
// 参数三 是否是分屏的上边屏幕
// 由于上面mMinimizeAmount为0才会执行到这里,所以这个方法是会做移动,而不会缩放
// 移动主要就是根据这里第三个参数,按照参数一来做边界的对齐
task.alignToAdjustedBounds(mAdjustedBounds, insetBounds, getDockSide() == DOCKED_TOP);
// 标记下需要layout
mDisplayContent.setLayoutNeeded();
}
```
这个方法开始做一些检查,这里mAdjustedForIme表示因为输入法的原因需要对这个TaskStack调整,调整的数据保存在mAdjustedBounds这个Rect里,所以对这些数据做检查后,如果都没问题的话,会调用alignToAdjustedBounds方法对参数的Task进行调整,我们稍稍看一眼这个方法:
```java
// 把当前task的rect按照参数一的adjustedBounds对齐
void alignToAdjustedBounds(Rect adjustedBounds, Rect tempInsetBounds, boolean alignBottom) {
// 如果该task大小不可变或者配置属性是空,return
if (!isResizeable() || Configuration.EMPTY.equals(getOverrideConfiguration())) {
return;
}
// 获取当前task的尺寸
getBounds(mTmpRect2);
// true表示是分屏的上面部分屏幕,底部对齐
if (alignBottom) {
int offsetY = adjustedBounds.bottom - mTmpRect2.bottom;
mTmpRect2.offset(0, offsetY);
} else {// 否则就是左边和顶部对齐
mTmpRect2.offsetTo(adjustedBounds.left, adjustedBounds.top);
}
// 把insets保存到task中,这个在绘制的时候会用到,这里没用到
setTempInsetBounds(tempInsetBounds);
// 把这个新的task的rect更新到task中
resizeLocked(mTmpRect2, getOverrideConfiguration(), false /* forced */);
}
```
这个方法参数一就是要调整的Rect,会根据这个参数调整当前Task的位置和尺寸,这里的第三个参数主要是根据分屏模式表示是分屏的不同屏幕,来决定输入法弹出后怎么调整task,比如在分屏的上面和下面,对后面界面的调整是不同的,所以这里做了不同的处理。我们看一下这个方法就行,细节这里也就不深入了,我们还是主要关注WMS的整体流程,我们回到前面addWindow方法。
# 计算窗口的四周边衬
除了调整输入法窗口引起的其他窗口变化外,在分屏模式下,不同窗口之间是有一根分割线的,这根分割线也是一个窗口,这里会调用DockedStackDividerController的setWindow方法设置分割线窗口,然后还是设置分割线的颜色等等,这里也不跟进了。继续看下一段代码:
```java
...............
// 获取窗口的转场动画
final WindowStateAnimator winAnimator = win.mWinAnimator;
// 需要一个转场动画
winAnimator.mEnterAnimationPending = true;
// 第一次窗口加入或者动画显示但没有执行显示完毕后的View.dispathcOnWindowShownCallback方法
// 时,设置为true
winAnimator.mEnteringAnimation = true;
// Check if we need to prepare a transition for replacing window first.
// 如果Activity是存在可见的,并且如果没有替代的窗口
if (atoken != null && atoken.isVisible()
&& !prepareWindowReplacementTransition(atoken)) {
// If not, check if need to set up a dummy transition during display freeze
// so that the unfreeze wait for the apps to draw. This might be needed if
// the app is relaunching.
// 到这里再看看这个添加的窗口需不需要过段动画
prepareNoneTransitionForRelaunching(atoken);
}
// 如果是默认的屏幕
if (displayContent.isDefaultDisplay) {
// 获取屏幕信息
final DisplayInfo displayInfo = displayContent.getDisplayInfo();
final Rect taskBounds;
// 如果Activity非null同时所在的task也非null
if (atoken != null && atoken.getTask() != null) {
taskBounds = mTmpRect;
// 把这个Activity所在的task大小赋值给mTmpRect
atoken.getTask().getBounds(mTmpRect);
} else { // Activity是null或者他所在的task也是null
taskBounds = null; // task的rect置null
}
// 计算outContentInsets和outStableInsets区域
// outContentInsets是四周状态栏的insets
// outStableInsets是状态栏和导航栏常占区域
if (mPolicy.getInsetHintLw(win.mAttrs, taskBounds, displayInfo.rotation,
displayInfo.logicalWidth, displayInfo.logicalHeight, outContentInsets,
outStableInsets, outOutsets)) {
// 这里会计算一些inset的值,返回true表示会显示导航栏
res |= WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_NAV_BAR;
}
} else {
// 除去状态栏和导航栏的区域置null
outContentInsets.setEmpty()
// 系统窗口覆盖的区域置null
outStableInsets.setEmpty();
}
...............
```
这段代码有两部分。第一部分是设置设置窗口的转场动画,这个我们不多说。下面看下第二部分,如果是默认的显示屏幕,这里有两个变量outContentInsets和outStableInsets,之前文章也有介绍过,outContentInsets表示的是除了中间用户的内容外四周的边衬衫,比如上面的状态栏,下面的导航栏,还有输入法等,这些四周的边距要留出来给那些对应的窗口用,下面这个图之前文章有过,现在再看一下:

中间那块就是用户内容的区域,四周就是这里的outContentInsets。而outStableInsets这个代表的是那些状态栏和导航栏固定占用的区域,和显示不显示无关,只计算他们的大小。这里调用的是WindowManagerPolicy的getInsetHintLw方法来计算的,这个计算方法就不跟进了,他的实现类是PhoneWindowManager,这个方法里面会根据当前屏幕中的各种信息,来计算这些值,最后返回的一个bool值,表示是否要显示导航栏,如果要显示导航栏,在返回的时候会加上ADD_FLAG_ALWAYS_CONSUME_NAV_BAR这个flag,表示需要显示导航栏。
如果非默认显示屏幕,这里就不用计算outContentInsets和outStableInsets的值了,清空就可以了。我们继续看后面的代码:
```java
..............
if (mInTouchMode) {
// 如果是可触摸的模式下,加上这个flag
res |= WindowManagerGlobal.ADD_FLAG_IN_TOUCH_MODE;
}
// 如果mAppToken为null说明不是Activity,那么可能是一个子窗口
// 一般只有在Activiy可见的情况下才会创建子窗口,所以认为是可见的
// 后一个条件非等于isClientHidden表示Activity是可见的,自然加上这个flag
if (win.mAppToken == null || !win.mAppToken.isClientHidden()) {
// 可见的flag
res |= WindowManagerGlobal.ADD_FLAG_APP_VISIBLE;
}
// 设置输入事件窗口更新标识
mInputMonitor.setUpdateInputWindowsNeededLw();
..............
```
这部分方法首先如果这个添加的窗口是可以接受输入事件和用户交互的,那么会加上ADD_FLAG_IN_TOUCH_MODE这个flag。
接着如果添加的窗口非Activity,如果非Activity的话,一般是子窗口或者系统窗口,子窗口一般是在父窗口可见的情况下才能添加的,所以一般也是可见的,而系统窗口一般也是可见的,所以非Activity的话这里要添加上ADD_FLAG_APP_VISIBLE这个flag,表示可见。而对于Activity来说AppWindowToken的isClientHidden方法表示隐藏,如果非隐藏的话也会添加上ADD_FLAG_APP_VISIBLE这个flag。
之后调用InputMonitor的setUpdateInputWindowsNeededLw这个方法表示可以对输入事件处理队列的更新,主要是用户处理和用户交互的输入事件队列的,这块我们这里也不展开,后面分析和输入事件有关的模块再说。
到目前为止和这个添加窗口有关的一些前置的工作基本做的差不多了,下面会触发这个窗口有关的测量,布局,有关的工作。鉴于这篇文章已经不少了,而且后面的方法还是挺重要的,所以还是需要花不少笔墨来描述,所以这篇文章就暂时到这里,我们休息一下,下篇文章继续。
WMS(四)之Acivity启动流程三