4、ViewGroup的深度剖析:addView与removeView的内部逻辑,LayoutParams的传递机制
各位同学,今天我们聊聊ViewGroup里最核心的两个操作——添加和移除子View。说白了,你写界面就是在不断地addView和removeView。这两个方法看起来简单,但内部逻辑其实挺深的。我当年刚接触Android时,以为addView就是把一个View塞进去完事,结果踩了不少坑。
4.1 addView的完整链路
我们先从addView开始。你调用addView(child),系统到底干了什么?
// ViewGroup.java - 简化版
public void addView(View child) {
addView(child, -1);
}
public void addView(View child, int index) {
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
}
addView(child, index, params);
}
嗯,这里有个关键点:LayoutParams的生成时机。如果你传进来的child没有LayoutParams,系统会调用generateDefaultLayoutParams()生成一个默认的。不同的ViewGroup子类会生成不同的默认参数。比如LinearLayout生成的是LinearLayout.LayoutParams,而FrameLayout生成的是FrameLayout.LayoutParams。
核心逻辑:addView最终都会走到addViewInner()方法,这里面做了四件大事:
- 检查child是否已经有父容器,如果有则抛出异常
- 处理LayoutParams的兼容性
- 将child添加到子View数组中
- 触发重新布局
我个人习惯在自定义ViewGroup时,一定要重写generateDefaultLayoutParams()和generateLayoutParams(AttributeSet attrs)这两个方法。否则你可能会遇到子View布局参数丢失的诡异问题。
4.2 removeView的内部逻辑
removeView的逻辑相对简单一些,但坑也不少。我们来看核心流程:
// ViewGroup.java - 简化版
public void removeView(View view) {
removeViewInternal(view);
requestLayout();
invalidate();
}
private void removeViewInternal(View view) {
int index = indexOfChild(view);
if (index >= 0) {
removeViewInternal(index, view);
}
}
这里有个细节:removeView会触发requestLayout和invalidate。这意味着每次移除子View,整个ViewGroup都会重新测量和绘制。如果你在循环中频繁removeView,性能会急剧下降。
我曾经踩过的坑:在RecyclerView的Adapter中,我试图在onBindViewHolder里动态removeView。结果列表滑动时卡成PPT。后来改用removeViewsInLayout()批量处理,才解决了问题。
removeView还有一个重要行为:它会调用child.onDetachedFromWindow()。如果你的子View持有资源(比如动画、监听器),一定要在这个回调里释放,否则会造成内存泄漏。
4.3 LayoutParams的传递机制
LayoutParams的传递,说白了就是父容器如何告诉子View它应该怎么布局。这个过程发生在三个层面:
| 阶段 | 行为 | 关键方法 |
|---|---|---|
| 添加时 | 检查并生成默认LayoutParams | generateDefaultLayoutParams() |
| 测量时 | 根据LayoutParams计算子View尺寸 | measureChildWithMargins() |
| 布局时 | 根据LayoutParams确定子View位置 | onLayout() |
你想想看,为什么我们在XML里写android:layout_width="match_parent",在Java代码里却要写成new LinearLayout.LayoutParams(...)?因为XML解析时,系统会调用generateLayoutParams(AttributeSet attrs)来创建对应的LayoutParams对象。
一个小技巧:如果你在代码中动态添加子View,最好显式指定LayoutParams的类型。比如:
Button btn = new Button(context);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
btn.setLayoutParams(lp);
parent.addView(btn);
这样可以避免类型转换异常。我在项目中见过太多因为LayoutParams类型不匹配导致的崩溃了。
4.4 知识体系图
下面这张图展示了addView、removeView和LayoutParams之间的核心关系:
4.5 避坑指南
最后,我总结几个实战中容易踩的坑:
- 重复添加:同一个View不能同时被两个父容器持有。如果你想把View从A移到B,必须先remove再add。
- LayoutParams类型不匹配:比如把
LinearLayout.LayoutParams传给RelativeLayout,虽然不会崩溃,但布局效果完全不对。 - removeView后的内存泄漏:子View如果注册了监听器或持有Context引用,remove后不会自动释放,需要手动清理。
- 批量操作性能:连续add/remove多个子View时,建议使用
removeViewsInLayout()或addViewInLayout(),避免频繁触发布局。
嗯,关于addView和removeView的核心逻辑,今天就讲到这里。LayoutParams的传递机制其实贯穿了整个View的测量和布局过程,后面讲onMeasure和onLayout时我们还会深入探讨。