删除窗口的时机
Android中 的 窗 口 和 Windows操 作 系 统 中 的 窗 口 管 理 有 一 个 明 显 差 别 ,就 是 窗 口 的 右 上 角 没 有 关 闭
按 钮 , 即 用 户 不 能 直 接 关 闭 窗 口 。
从 SD K 的 角 度 来 看 , SDK希 望 程 序 员 不 要 直 接 去 操 作 窗 口 , SD K 已 经 对 窗 口 进 行 了 各 种 封 装 ,
Activity,菜 单 、 对 话 框 等 , 这 些 控 件 的 背 后 都 对 应 一 个 窗 口 , 然 而 对 程 序 员 来 讲 , 不 需 要 关 心 这 些 控
件 背 后 的 窗 口 , 而 是 直 接 使 用 这 些 控 件 类 。
对 于 Activity而 言 , 当 程 序 员 要 关 闭 该 窗 口 时 , 可 以 调 用 finishO方 , ; 对 于 一 个 对 话 框 , 当 需 要 关
闭 其 窗 口 时 , 可 以 调 用 dismiss()方 法 。 本 节 将 要 分 析 隐 藏 在 这 些 方 法 背 后 的 秘 密 , 即 背 后 到 底 是 如 何 真正地删除一个窗口的。
当然, SDK并没有完全封装窗口的操作,它提供了一个WindowManager类 ,当程序员需要一个完
全自定义的窗口时,依然可以调用该类的addView()方法添加一个窗口,并可以调用removeView()删除
一个窗口。这 里View的语义实际上就是指一个真正的窗口,尽管表面上看来这两个方法的操作是View
对象,而实际上内部会自动创建一个真正的窗口,并将参数中的View对象作为窗口的可视对象。
因此,删除窗口的时机可以大致分为两类。
第一类是隐性删除,即不直接调用WindowManager类 的 removeView()来删除窗口,比 如 Activity
窗口,程序只要调用finish()方法,后台便会删除该Activity对应的窗口。
第二类是显性删除,即直接调用WindowManager类 的 removeView()来删除窗口。
关于隐性删除,本节仅介绍Activity对象的删除过程,对于对话框、菜单框等窗口的删除过程,其
本质上都是通过调用WindowManager类 的 removeView()完成的,读者可自行分析。
隐性删除常见的情况包括:当用户按下Back键后, AmS会间接调用destroyActivity(); 当程序员调
用 finishO函 数 后 , AmS也 会 间 接 调 用 到 destroyActivity(); 当 系 统 内 存 低 时 , A m S也会调用到
destroyActivity()。总之,这三种情况都是AmS调 用ActivityThread的 destroyActivity,
ActivityThread中的scheduleDestroyActivity()函数中将处理删除窗口的操作
**首 先 , 无 论 何 种 情 况 , W m S 要 销 毁 一 个 A ctivity相 关 的 窗 口 时 , 都 必 须 调 用ActivityThread.AppicationThread 类的 scheduleDestroyActivity()。该函数是异步执行的,它会发送一个Handler消息,然后返回。消息的处理函数是handleDestroyActivity,该函数内部调用了两个重要函数,
- 第一个是removeViewlmeediate(),
- 另一个是closeAll()。 前者用于删除Activity窗口,后者用于删除Activity相 关 的 窗 口 , 比 如 Activity的 启 动 窗 口 、 菜 单 窗 口 、 对 话 框 窗 口 等 。
这 两 个 函 数 又 都 会 调 用 各 自 窗 口 对 应 的 ViewRoot对 象 的 die()函 数 , 每 个 窗 口 都 会 对 应 于 一 个ViewRoot 对 象 , 该 函 数 又 会 调 用 doDie(), doDie()又 会 调 用dispatchDetachedFromWindow( ) , 该 函 数 内部 调 用 sWindowSession.remove()。 这 是 一 个 IPC远 程 调 用 , sWindowSession是 客 户 端 中 的 一 个 全 局 静
态 变 量 , 每 个 客 户 端 对 应 一 个 该 对 象 。
以 上 就 是 隐 性 删 除 窗 口 的 时 机 , 至 于 显 性 删 除 , 则 直 接 从 WindowManager类 的
removeViewImmediate()开 始 , 过 程 与 隐 性 删 除 相 同 。**
该函数中,首先调用windowForClientLocked()寻找目标窗口是否存在,读者可能觉得奇怪,既然是删除窗口,窗口为什么会不存在呢?典型 的情况就是当程序员连续调用两次removeWindowO,并且参数相同时,则第二次调用时,windowForClientLocked()就
会返回为空。该函数内部逻辑很简单,就是从全局变量mWindowMap中查看目标窗口是否存在,对mWindowMap而言,一个窗口就是指ViewRoot类中的W对象它是一个IWindow对象。如果存在,则调用removeWindowLocked()开始删除,该函数内部的关键逻辑是,首先会为窗口添加一个动画,这就是所谓的窗口关闭动画,并且直到该动画结束后才真正删除窗口.
*@param session
* @param client
*/
public void removeWindow(Session session, IWindow client) {
synchronized(mWindowMap) {
WindowState win = windowForClientLocked(session, client, false);
if (win == null) {
return;
}
removeWindowLocked(session, win);
}
}
```
接着 分析 removeWindowLocked
public void removeWindowLocked(Session session, WindowState win) {
if (localLOGV || DEBUG_FOCUS) Slog.v(
TAG, "Remove " + win + " client="
+ Integer.toHexString(System.identityHashCode(
win.mClient.asBinder()))
+ ", surface=" + win.mSurface);
/**
* 一 调用win.disposeInputChannel(),其作用是
* 清除pipe对,因为在创建窗口的同时创建了一个pipe
* 对用于传递输入消息,而此时窗口就要被删除了,所
* 以pipe()对也就应该被删除。由于删除pipe对发生
* 在窗口关闭动画之前,因此,在动画运行期间,用
* 户虽然能够看见该窗口,但是却不能同该窗口交互。
*/
final long origId = Binder.clearCallingIdentity();
win.disposeInputChannel();
if (DEBUG_APP_TRANSITIONS) Slog.v(
TAG, "Remove " + win + ": mSurface=" + win.mSurface
+ " mExiting=" + win.mExiting
+ " isAnimating=" + win.isAnimating()
+ " app-animation="
+ (win.mAppToken != null ? win.mAppToken.animation : null)
+ " inPendingTransaction="
+ (win.mAppToken != null ? win.mAppToken.inPendingTransaction : false)
+ " mDisplayFrozen=" + mDisplayFrozen);
// Visibility of the removed window. Will be used later to update orientation later on.
boolean wasVisible = false;
// First, see if we need to run an animation. If we do, we have
// to hold off on removing the window until the animation is done.
// If the display is frozen, just remove immediately, since the
// animation wouldn't be seen.
/**
* 二 判断是否可以为该窗口添加一个动画。可以添加动画的条件有三;第一是该窗口已经存在
* Surface; 第二是屏幕没有被冻结(frozen),冻结是指当屏幕发生旋转时,系统会暂时冻结屏幕,直到
* 屏幕被重绘;第三是屏幕处于打开状态(screenon),当发生锁屏后,屏幕就处于非打开状态。
*/
if (win.mSurface != null && !mDisplayFrozen && mPolicy.isScreenOn()) {
// If we are not currently running the exit animation, we
// need to see about starting one.
/**
* 2.1 如果可以进行动画,则先调用win.isWinVisibleLwO判断当前是否已经设置过动画,因为如果
* 设置过,该函数就会返回false,即当窗口正在进行动画时,是不可见的。因此,如果该函数返回为true,
* 则说明这是第一次调用removeWindowLocked(),关闭动画还没有设置,因此,调用
* applyAnimationLocked()为该窗口设置一个动画。
*/
if (wasVisible=win.isWinVisibleLw()) {
int transit = WindowManagerPolicy.TRANSIT_EXIT;
if (win.getAttrs().type == TYPE_APPLICATION_STARTING) {
transit = WindowManagerPolicy.TRANSIT_PREVIEW_DONE;
}
// Try starting an animation.
if (applyAnimationLocked(win, transit, false)) {
win.mExiting = true;
}
}
/**
* 2.2 如果isWinVisibleLw()返回false,则说明已经设置过动画,因此继续判断动画是否结束,如
* 果没有结束,则调用performLayoutAndPlaceSurfacesLocked()对窗口进行重排。该函数是WmS内部的
* 最核心函数,其作用是根据窗口列表的参数值,重新计算窗口大小,并调整窗口层值,使之真正被反映
* 到屏幕上,关于该函数的内部执行过程见后面小节。重排结束后就直接返回,因为正在进行动画,所以
*不能删除窗口 。
*/
if (win.mExiting || win.isAnimating()) {
// The exit animation is running... wait for it!
//Slog.i(TAG, "*** Running exit animation...");
win.mExiting = true;
win.mRemoveOnExit = true;
mLayoutNeeded = true;
updateFocusedWindowLocked(UPDATE_FOCUS_WILL_PLACE_SURFACES);
performLayoutAndPlaceSurfacesLocked();
if (win.mAppToken != null) {
win.mAppToken.updateReportedVisibilityLocked();
}
//dump();
Binder.restoreCallingIdentity(origId);
return;
}
}
/**
* 三 能执行到本步,说明动画已经结束,事实上,动画结束的标志是win对象的mSurface为空,
*关于这一点将在动画小节中介绍。此时就可以调用removeWindowInnerLockedO方法,该方法将从记录
*目标窗口的变量中移除目标窗口,具体执行过程见下一小节。
*/
removeWindowInnerLocked(session, win);
// Removing a visible window will effect the computed orientation
// So just update orientation if needed.
/**
* 四 窗口删除后,下一个焦点窗口的屏幕旋转方向有可能与当前的旋转方向不一致,因此需要调用
* updateOrientationFromAppTokensLocked()查看是否需要调整旋转方向。如果需要,则发送一个异步
*Handler消息SEND—NEW_CONFIGRATION,消息的处理函数将通知AmS设置新的旋转方向。
*/
if (wasVisible && computeForcedAppOrientationLocked()
!= mForcedAppOrientation
&& updateOrientationFromAppTokensLocked()) {
mH.sendEmptyMessage(H.SEND_NEW_CONFIGURATION);
}
/**
* 五 最后,调用updateFocusedWindowLocked()寻找新的焦点窗口 。
*/
updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL);
Binder.restoreCallingIdentity(origId);
}
···
该函数的作用是从各种记录窗口信息的变量中移除目标窗口对应的记录,因此了解清楚记录窗口相关的变量就很容易理解该函数的执行过程。该函数有两个参数,分别为Sessionsession和WindowState第一个参数实际上并没有被使用,第二个参数代表要被删除的目标窗口。
* @param session
* @param win
*/
private void removeWindowInnerLocked(Session session, WindowState win) {
win.mRemoved = true;
/**
*一、判断要删除的窗口是否是mlnputMethodTarget。该变量保存的是输入法下层的窗口,因此如果
* 要 删 除的窗口就是该窗口,则可能需要调整输入法窗口。
*/
if (mInputMethodTarget == win) {
moveInputMethodWindowsIfNeededLocked(false);
}
if (false) {
RuntimeException e = new RuntimeException("here");
e.fillInStackTrace();
Slog.w(TAG, "Removing window " + win, e);
}
/**
* 二 如前面对“策略”的解释,此处在删除窗口时,也需要先通知策略,以便它在删除之前做
* 点什么 。
*/
mPolicy.removeWindowLw(win);
/**
* 三 执行win.removeLocked(),该函数内部真正完成删除窗口对应的Surface操作。
*/
win.removeLocked();
/**
* 四、分别从mWindowMap和mWindows列表中删除该窗口对应的记录 。
*/
mWindowMap.remove(win.mClient.asBinder());
mWindows.remove(win);
mWindowsChanged = true;
if (DEBUG_WINDOW_MOVEMENT) Slog.v(TAG, "Final remove of window: " + win);
/**
* 五由于WmS内部用了另外两个变量保存和输入法窗口相关的窗口,因此如果此处删除的就是输
* 入法相关的窗口,那么同时需要从这两个变量中清除相关记录,分别是mlnputMethodWindow和
* mlnputMethodDialogs。前者是一个WindowState对象,对应输入法窗口,系统中只能有一个输入法窗口;
* 后者是输入法对话框窗口,可以有多个输入法对话框
*/
if (mInputMethodWindow == win) {
mInputMethodWindow = null;
} else if (win.mAttrs.type == TYPE_INPUT_METHOD_DIALOG) {
mInputMethodDialogs.remove(win);
}
/**
* 六 由于每个窗口在它对应的WindowToken对象中还有一个记录,即token.windows列表,因此还
* 需要从列表中清除该记录。清除完毕后,如果该WindowToken对象内部的窗口数为0,那么该
* WindowToken对象本身也就需要被清除了,因此从mTokenMap和mTokenList列表中清除该
* WindowToken对象。此处需要注意的是,仅当token.explicit为true时才清除mTokenMap和mTokenList
* 中的记录 ,explicit的意思是“显式的”,系统中创建WindowToken有两处,一处是在addWindow()时,另一处
* 是在addWindowToken()函数中,后者仅用于IMMS和WallpaperManagerService中,并且只有后者创建
* WindowToken时才使用了explicit模式。也就是说只有输入法窗口和墙纸窗口对应的WindowToken对象
* 才是explicit的,即这两个窗口在删除时不从mTokenMap和mTokenList中清除记录 。
*/
final WindowToken token = win.mToken;
final AppWindowToken atoken = win.mAppToken;
token.windows.remove(win);
if (atoken != null) {
atoken.allAppWindows.remove(win);
}
if (localLOGV) Slog.v(
TAG, "**** Removing window " + win + ": count="
+ token.windows.size());
if (token.windows.size() == 0) {
if (!token.explicit) {
mTokenMap.remove(token.token);
mTokenList.remove(token);
} else if (atoken != null) {
atoken.firstWindowDrawn = false;
}
}
/**
* 七 如果该窗口是一个和Activity相关的窗口,那么它还对应一个AppWindowToken对象,并在该
* 对象的allAppWindows列表中保存了一个记录,因此需要清除 。
*/
if (atoken != null) {
if (atoken.startingWindow == win) {
atoken.startingWindow = null;
} else if (atoken.allAppWindows.size() == 0 && atoken.startingData != null) {
// If this is the last window and we had requested a starting
// transition window, well there is no point now.
atoken.startingData = null;
} else if (atoken.allAppWindows.size() == 1 && atoken.startingView != null) {
// If this is the last window except for a starting transition
// window, we need to get rid of the starting transition.
if (DEBUG_STARTING_WINDOW) {
Slog.v(TAG, "Schedule remove starting " + token
+ ": no more real windows");
}
Message m = mH.obtainMessage(H.REMOVE_STARTING, atoken);
mH.sendMessage(m);
}
}
/**
* 八 如果被删除的窗口类型是墙纸窗口(TYPE_WALLPAPER)或者窗口背景是墙纸(win.attrs.flags
* 包含FLAG_SHOW_WALLPAPER),则调用adjustWallpaperWindowsLocked()重新调整墙纸窗口 。
*/
if (win.mAttrs.type == TYPE_WALLPAPER) {
mLastWallpaperTimeoutTime = 0;
adjustWallpaperWindowsLocked();
} else if ((win.mAttrs.flags&FLAG_SHOW_WALLPAPER) != 0) {
adjustWallpaperWindowsLocked();
}
/**
*九 如果当前函数调用不是发生在窗口重置调用中,则需要重新对窗口进行重置。变量mlnLayout
* 在重置调用前被置为true,并在调用后被置为false,用于标识当前是否正在进行重置,
*/
if (!mInLayout) {
assignLayersLocked();
mLayoutNeeded = true;
performLayoutAndPlaceSurfacesLocked();
if (win.mAppToken != null) {
win.mAppToken.updateReportedVisibilityLocked();
}
}
/**
* 十、至此,已经清楚了被删除窗口相关的全部信息,最后要做的就是把这种变化通知给消息输入模
* 块,从而使其能够根据这种变化选择正确的新的焦点窗口。通知的方法很简单,就
*是调用 mInputMonitor.updateInputWindowsLw( )
*/
mInputMonitor.updateInputWindowsLw();
}