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() 里。我把它拆成四个阶段:

  1. 预清理阶段:从 WMS 的数据结构中移除窗口
  2. 窗口销毁阶段:通知窗口状态变化
  3. Surface 释放阶段:回收图形资源
  4. 回调通知阶段:告诉应用层窗口已删除

流程图如下:

removeWindowLocked() 核心流程 removeWindowLocked() 阶段1:预清理 从 mWindowMap 移除,更新焦点/层序 阶段2:窗口销毁 调用 removeWindowInnerLocked() 阶段3:Surface 释放 WindowStateController.removeSurface() 阶段4:回调通知 onWindowRemoved() / 移除监听

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 的情况。

⚠️ 注意: Surface 释放后,应用层不能再往上面绘制内容。否则会收到 IllegalStateException: Surface was already destroyed。我曾经在线上看到过这种 crash,原因是应用在窗口销毁后还有一帧动画没画完。

15.6 回调通知:让应用层知道

窗口删除的最后一步是通知应用层。WMS 通过 IWindow.onWindowRemoved() 回调:

try {
    win.mClient.onWindowRemoved();
} catch (RemoteException e) {
    // 应用进程可能已经挂了
}

这个回调是单向的,应用层收到后可以做清理工作,比如释放自己持有的 Surface 引用、停止动画等。

除了这个回调,还有几个通知路径:

  • InputManager:移除窗口的输入事件通道
  • AccessibilityManager:更新无障碍服务状态
  • WindowManagerPolicy:通知策略层窗口已移除
💡 经验之谈: 我在调试一个窗口泄漏问题时发现,如果应用进程在 onWindowRemoved() 回调中抛异常,WMS 会 catch 住但不会重试。所以应用层一定要保证这个回调是幂等的——多次调用不会出问题。

15.7 完整时序图

把上面这些串起来,完整的调用时序是这样的:

App WMS SurfaceFlinger InputManager removeView() removeWindowLocked() 预清理:移除映射 releaseSurface() 回收Layer 移除InputChannel onWindowRemoved() 窗口删除完成

15.8 常见问题与避坑

最后分享几个我实际踩过的坑:

🔴 问题1:窗口泄漏

如果应用进程在 onWindowRemoved() 回调中崩溃,WMS 不会重试。结果就是 WMS 认为窗口已删,但应用侧的资源没释放。我建议在应用层做一个超时机制:如果 5 秒内没收到回调,主动清理本地资源。

🔴 问题2:Surface 双重释放

WMS 内部有保护,多次调用 removeSurface() 不会 crash。但应用层如果自己持有 Surface 引用并手动释放,就可能出现双重释放。记住:Surface 的释放权在 WMS,应用层只负责停止绘制。

🔴 问题3:动画中的窗口删除

如果窗口正在播放退出动画,WMS 会等待动画结束再真正删除。这个逻辑在 removeWindowInnerLocked() 中通过 mWinAnimator.hide() 触发。我曾经遇到一个 case:动画卡住了,窗口一直删不掉,导致内存泄漏。解决方案是加一个动画超时强制删除。

好了,关于 WMS 删除窗口的流程就讲到这里。记住核心思路:先断联、再销毁、释放资源、最后通知。每一步都有它的道理,少一步都可能出问题。

📌 一句话总结: 窗口删除不是简单的「删掉」,而是一个有序的资源回收过程。理解了这个流程,你就能更好地掌控窗口的生命周期。

公众号:蓝海资料掘金营,微信deep3321