9、removeView 流程源码分析:视图移除的完整生命周期

说实话,addView 大家聊得比较多,但 removeView 往往被忽视。我早年做性能优化时,就踩过移除视图的坑——视图是移掉了,但内存泄漏了。嗯,今天咱们就把 removeView 的完整生命周期扒个底朝天。

9.1 入口:从 ViewManager 到 WindowManagerGlobal

你平时调用 windowManager.removeView(view),其实它只是个壳。真正的逻辑在 WindowManagerGlobal 里。

// WindowManagerImpl.java
@Override
public void removeView(View view) {
    mGlobal.removeView(view, false);
}

这里传了个 false,表示“不是立即移除”。那什么时候立即移除?后面会讲。

进入 WindowManagerGlobal.removeView() 后,第一件事就是找索引:

public void removeView(View view, boolean immediate) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }

    synchronized (mLock) {
        int index = findViewLocked(view, true);
        ViewRootImpl root = mRoots.get(index);
        // 开始移除
        root.die(immediate);
    }
}

这里有个细节:findViewLocked 第二个参数是 true,表示找不到就抛异常。我见过不少线上崩溃就是这里报的 "View not attached to WindowManager"。说白了,你移除一个已经移过的视图,就会挂。

9.2 ViewRootImpl.die():优雅还是暴力?

die() 方法很有意思。它有两种模式:

模式 触发条件 行为
异步移除 immediate = false 发送消息到主线程,等当前绘制完成再移除
同步移除 immediate = true 直接执行 doDie(),不等绘制
// ViewRootImpl.java
void die(boolean immediate) {
    if (immediate) {
        doDie();
    } else {
        if (!mIsDrawing) {
            doDie();
        } else {
            // 等绘制完再处理
            mHandler.sendEmptyMessage(MSG_DIE);
        }
    }
}

我个人习惯用异步移除。为什么?因为同步移除如果在绘制中间调用,会导致 Canvas 状态不一致。我曾经在自定义 SurfaceView 里试过同步移除,结果画面撕裂了……嗯,血的教训。

9.3 doDie():真正的清理战场

doDie() 是核心。它做了三件大事:

  1. 派发移除前回调:调用 dispatchDetachedFromWindow()
  2. 销毁 Surface:释放底层资源
  3. 清理 ViewRootImpl:从全局列表中移除自己
void doDie() {
    // 1. 检查是否已经执行过
    if (mRemoved) {
        return;
    }
    mRemoved = true;

    // 2. 派发分离事件
    dispatchDetachedFromWindow();

    // 3. 销毁 Surface
    mSurface.release();

    // 4. 从 WindowManagerGlobal 中移除
    WindowManagerGlobal.getInstance().doRemoveView(this);
}

注意那个 mRemoved 标志位。它防止 doDie() 被重复调用。我见过有些开发者手动调了 removeView 又调 removeViewImmediate,结果因为标志位保护,只执行了一次。这设计挺巧妙的。

9.4 dispatchDetachedFromWindow():视图的临终时刻

这个方法会触发 View.onDetachedFromWindow()。你想想看,这其实是视图生命周期里最后一个回调了。

流程是这样的:

void dispatchDetachedFromWindow() {
    // 1. 通知 View 树
    mView.dispatchDetachedFromWindow();

    // 2. 清理动画
    mAnimator.cancel();

    // 3. 移除焦点
    mView.clearFocus();

    // 4. 移除所有回调
    mHandler.removeCallbacksAndMessages(null);

    // 5. 通知 InputMethodManager
    InputMethodManager imm = InputMethodManager.peekInstance();
    if (imm != null) {
        imm.windowDismissed(mWindowAttributes.token);
    }
}

这里有个坑:mHandler.removeCallbacksAndMessages(null) 会清空所有消息。如果你在 Handler 里挂了 Runnable 引用,那这些 Runnable 就不会执行了。我曾经在 postDelayed 里做网络重试,视图移除后重试逻辑直接丢了……后来改成用 Lifecycle 管理才解决。

9.5 视图树的递归分离

mView.dispatchDetachedFromWindow() 会递归遍历整个 View 树:

// View.java
void dispatchDetachedFromWindow() {
    // 先通知子 View
    if (mAttachInfo != null) {
        for (int i = 0; i < mChildrenCount; i++) {
            getChildAt(i).dispatchDetachedFromWindow();
        }
    }

    // 再通知自己
    onDetachedFromWindow();
    onDetachedFromWindowInternal();
}

顺序是:先子后父。也就是说,子 ViewonDetachedFromWindow() 比父 View 先执行。这个顺序很重要——如果你在父 View 里引用了子 View,子 View 已经清理完了,你可能会拿到空引用。

核心要点onDetachedFromWindow() 里不要依赖子 View 的状态。因为子 View 可能已经被清理了。

9.6 Surface 的释放时机

mSurface.release() 这行代码看似简单,其实暗藏玄机。它释放的是 Surface 对象背后的 SurfaceControl,也就是 SurfaceFlinger 那边的资源。

释放之后,这个 View 对应的窗口就彻底消失了。如果你在 onDetachedFromWindow() 里还尝试 invalidate(),那就会收到 "Surface is already destroyed" 的警告。

注意:Surface 释放后,Canvas 不能再使用。任何绘制操作都会抛出异常。我曾经在自定义 View 的 onDetachedFromWindow 里调了 postInvalidate(),结果 logcat 刷了一堆错误。

9.7 从全局列表中移除

最后一步是 WindowManagerGlobal.doRemoveView()

void doRemoveView(ViewRootImpl root) {
    synchronized (mLock) {
        int index = mRoots.indexOf(root);
        if (index >= 0) {
            mRoots.remove(index);
            mViews.remove(index);
            mParams.remove(index);
            mDyingViews.remove(index);
        }
    }
}

这里同时清理了四个列表:

  • mRoots:ViewRootImpl 列表
  • mViews:根 View 列表
  • mParams:布局参数列表
  • mDyingViews:正在消亡的 View 列表

为什么要清理 mDyingViews?因为 die() 异步执行时,会先把 View 加到 mDyingViews 里,等真正移除时再删掉。这个列表就是为了防止重复移除。

9.8 完整流程图

下面这张图把整个流程串起来了:

removeView 完整生命周期流程图 removeView(view) findViewLocked() ViewRootImpl.die() immediate? 异步:发送 MSG_DIE 同步:直接 doDie() doDie() 清理完成

9.9 避坑指南

我的经验:如果你在 onDetachedFromWindow() 里做了资源释放,记得检查 isAttachedToWindow()。因为某些极端情况下(比如 removeView 被调用两次),这个方法可能被重复执行。

另外,removeViewremoveViewImmediate 的区别要搞清楚:

  • removeView:等当前绘制完成再移除,安全但有时延
  • removeViewImmediate:立即移除,但可能引发绘制问题

我建议:99% 的场景用 removeView。只有当你确定当前没有绘制操作时,才用 removeViewImmediate。比如在 onPause() 里移除 SurfaceView,用立即移除会更及时。

好了,removeView 的完整生命周期就这些。从入口到清理,每一步都有它的设计考量。理解这些,你就能避免很多视图移除相关的坑。

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