15、WMS 删除窗口流程:removeWindow() 方法的执行路径、窗口销毁、Surface 释放、回调通知
窗口的创建和销毁,就像人的生与死。创建时有多复杂,销毁时就有多讲究。今天我们来聊聊 WMS 中删除窗口的完整流程。说实话,这块内容我当年啃源码时绕了不少弯路,尤其是 Surface 释放的时机,稍不注意就会踩坑。
15.1 删除窗口的入口:removeWindow()
删除窗口的起点是 WindowManagerService.removeWindow()。这个方法被谁调用?通常是应用进程通过 ViewRootImpl 请求移除窗口。比如 Activity 销毁时,或者你主动调用 windowManager.removeView()。
来看核心代码路径:
public int removeWindow(Session session, IWindow client, ...) {
// 1. 查找窗口
WindowState win = windowForClientLocked(session, client, false);
if (win == null) {
return WindowManagerGlobal.REMOVE_RESULT_NO_WINDOW;
}
// 2. 权限检查
// 3. 执行移除
removeWindowLocked(session, win, ...);
return REMOVE_RESULT_REMOVED;
}
这里有个细节:windowForClientLocked() 是通过 IWindow Binder 对象来匹配的。每个窗口在创建时都会注册一个唯一的 Binder token。我遇到过一个问题:应用侧重复调用 removeView,导致第二次进来时窗口已经没了,返回 NO_WINDOW。嗯,调用方要做好幂等处理。
15.2 核心流程:removeWindowLocked()
真正的删除逻辑在 removeWindowLocked() 里。我把它拆成四个阶段:
- 预清理阶段:从 WMS 的数据结构中移除窗口
- 窗口销毁阶段:通知窗口状态变化
- Surface 释放阶段:回收图形资源
- 回调通知阶段:告诉应用层窗口已删除
流程图如下:
15.3 预清理阶段
这个阶段 WMS 会做三件事:
- 从 mWindowMap 移除:
mWindowMap.remove(win.mClient.asBinder()) - 更新焦点窗口:如果被删的窗口是焦点窗口,需要重新计算焦点
- 更新层序:调整 Z-order,保证其他窗口的层级正确
我个人习惯把这一步叫做「断联」——先把窗口从 WMS 的大家庭里摘出来,避免后续操作产生副作用。你想想看,如果先释放 Surface 再移除映射,万一中间有异常,WMS 可能还在引用一个已经销毁的 Surface,那就崩了。
15.4 窗口销毁:removeWindowInnerLocked()
这个方法才是真正的「杀手」。它做了几件关键的事:
private void removeWindowInnerLocked(WindowState win) {
// 1. 从父窗口移除
win.removeFromWindowList();
// 2. 通知 DisplayContent
mRoot.removeWindowToken(win.mToken);
// 3. 清理动画
if (win.mWinAnimator != null) {
win.mWinAnimator.hide();
}
// 4. 调度布局
scheduleLayoutNeeded();
}
这里有个容易忽略的点:removeFromWindowList() 会同时处理子窗口。如果一个窗口有子窗口(比如 PopupWindow),父窗口销毁时子窗口也要跟着销毁。我曾经在项目中遇到一个 bug:Dialog 关闭后,它上面的 PopupWindow 还残留着,就是因为子窗口的清理逻辑没走对。
15.5 Surface 释放:最关键的环节
Surface 释放是删除窗口中最容易出问题的地方。WMS 通过 WindowStateController.removeSurface() 来触发:
void removeSurface() {
synchronized (mGlobalLock) {
// 1. 释放 SurfaceControl
if (mSurfaceControl != null) {
mSurfaceControl.release();
mSurfaceControl = null;
}
// 2. 清理 SurfaceSession
mSession.windowRemoved();
// 3. 通知 SurfaceFlinger
// SurfaceFlinger 会回收对应的 Layer
}
}
为什么要加锁?因为 Surface 的释放涉及跨进程通信。SurfaceFlinger 在另一个进程,释放操作是异步的。如果 WMS 这边不加锁,可能出现多个线程同时释放同一个 Surface 的情况。
IllegalStateException: Surface was already destroyed。我曾经在线上看到过这种 crash,原因是应用在窗口销毁后还有一帧动画没画完。
15.6 回调通知:让应用层知道
窗口删除的最后一步是通知应用层。WMS 通过 IWindow.onWindowRemoved() 回调:
try {
win.mClient.onWindowRemoved();
} catch (RemoteException e) {
// 应用进程可能已经挂了
}
这个回调是单向的,应用层收到后可以做清理工作,比如释放自己持有的 Surface 引用、停止动画等。
除了这个回调,还有几个通知路径:
- InputManager:移除窗口的输入事件通道
- AccessibilityManager:更新无障碍服务状态
- WindowManagerPolicy:通知策略层窗口已移除
15.7 完整时序图
把上面这些串起来,完整的调用时序是这样的:
15.8 常见问题与避坑
最后分享几个我实际踩过的坑:
如果应用进程在 onWindowRemoved() 回调中崩溃,WMS 不会重试。结果就是 WMS 认为窗口已删,但应用侧的资源没释放。我建议在应用层做一个超时机制:如果 5 秒内没收到回调,主动清理本地资源。
WMS 内部有保护,多次调用 removeSurface() 不会 crash。但应用层如果自己持有 Surface 引用并手动释放,就可能出现双重释放。记住:Surface 的释放权在 WMS,应用层只负责停止绘制。
如果窗口正在播放退出动画,WMS 会等待动画结束再真正删除。这个逻辑在 removeWindowInnerLocked() 中通过 mWinAnimator.hide() 触发。我曾经遇到一个 case:动画卡住了,窗口一直删不掉,导致内存泄漏。解决方案是加一个动画超时强制删除。
好了,关于 WMS 删除窗口的流程就讲到这里。记住核心思路:先断联、再销毁、释放资源、最后通知。每一步都有它的道理,少一步都可能出问题。
公众号:蓝海资料掘金营,微信deep3321