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();
}

顺序是:先子后父。也就是说,子 View 的 onDetachedFromWindow() 比父 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 被调用两次),这个方法可能被重复执行。

另外,removeView 和 removeViewImmediate 的区别要搞清楚:

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

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

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

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