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() 是核心。它做了三件大事:
- 派发移除前回调:调用
dispatchDetachedFromWindow() - 销毁 Surface:释放底层资源
- 清理 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 完整流程图
下面这张图把整个流程串起来了:
9.9 避坑指南
我的经验:如果你在 onDetachedFromWindow() 里做了资源释放,记得检查 isAttachedToWindow()。因为某些极端情况下(比如 removeView 被调用两次),这个方法可能被重复执行。
另外,removeView 和 removeViewImmediate 的区别要搞清楚:
removeView:等当前绘制完成再移除,安全但有时延removeViewImmediate:立即移除,但可能引发绘制问题
我建议:99% 的场景用 removeView。只有当你确定当前没有绘制操作时,才用 removeViewImmediate。比如在 onPause() 里移除 SurfaceView,用立即移除会更及时。
好了,removeView 的完整生命周期就这些。从入口到清理,每一步都有它的设计考量。理解这些,你就能避免很多视图移除相关的坑。