12、requestLayout与forceLayout:请求重新布局的流程
这一章我们来聊聊 View 体系里两个容易被混淆的方法——requestLayout 和 forceLayout。说实话,我见过不少开发者在项目里随手调 requestLayout,但根本不清楚它到底触发了什么流程。嗯,今天我们就把它彻底讲透。
12.1 requestLayout 干了什么?
先问一个问题:你什么时候会调 requestLayout?
最常见的场景是——你改变了 View 的尺寸、位置、或者内部布局参数,希望系统重新走一遍 measure 和 layout。比如你动态改了 layoutParams 的宽高,或者调了 setPadding,这时候就需要 requestLayout 来帮忙。
它的核心逻辑其实很简单:从当前 View 开始,向上递归标记父 View,直到根 View,然后触发一次完整的 measure + layout 流程。
我给大家拆解一下源码里的关键步骤:
// View.java
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
// 关键:标记当前 View 需要重新布局
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
// 向上递归通知父 View
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}
看到没?它做了两件事:
- 给自己打上
PFLAG_FORCE_LAYOUT标记——告诉系统「我下次 measure 必须重新算」 - 向上通知父 View——让整个 ViewTree 都知道「有人要重新布局了」
这里有个细节我特别想强调:它不会直接触发 measure 和 layout。它只是「请求」,真正的执行要等到下一个 VSync 信号到来,由 ViewRootImpl 统一调度。
核心要点:requestLayout 是「标记 + 通知」机制,不是「立即执行」机制。
12.2 forceLayout 又是什么?
forceLayout 这个名字听起来更暴力,对吧?其实它比 requestLayout 温和得多。
看源码:
// View.java
public void forceLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
}
它只做了一件事——给自己打上 PFLAG_FORCE_LAYOUT 标记。仅此而已。
它不会向上通知父 View,也不会触发任何绘制流程。说白了,它只是「告诉这个 View 自己,下次 measure 时别偷懒,必须重新算」。但如果你不主动调 requestLayout 或者系统不触发布局流程,这个标记就白打了。
我个人习惯把 forceLayout 看作一个「辅助工具」。它通常配合 requestLayout 一起用,或者在某些自定义 ViewGroup 里,你想强制某个子 View 重新 measure,但又不想惊动整个 ViewTree 时,可以用它。
我的经验:在自定义 ViewGroup 的 onLayout 方法里,如果你发现某个子 View 的尺寸不对,可以调 child.forceLayout() 然后 child.measure() 手动重新测量。这样比调 requestLayout 更精准,性能也更好。
12.3 requestLayout 如何触发 measure 和 layout?
好,这是本章最核心的问题。我们一步步看。
当 requestLayout 一路向上传到 ViewRootImpl 后,ViewRootImpl 会做什么?
// ViewRootImpl.java
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
关键就两行:
mLayoutRequested = true—— 记下「有人要布局」scheduleTraversals()—— 安排一次完整的遍历流程
scheduleTraversals 最终会走到 performTraversals,这里面就是 measure → layout → draw 三部曲。但注意,requestLayout 只会触发 measure 和 layout,不会触发 draw。为什么?
因为 requestLayout 没有设置 PFLAG_DIRTY 或 PFLAG_DIRTY_OPAQUE 这些绘制标记。所以 ViewRootImpl 在执行 performTraversals 时,会判断是否需要 draw。如果只有布局变化,没有内容变化,draw 阶段会被跳过。
我画了一张流程图,帮你理清整个链路:
12.4 与 invalidate 的区别
这是面试高频题,也是实际开发中容易搞混的地方。我直接列个对比表:
| 对比维度 | requestLayout | invalidate |
|---|---|---|
| 触发流程 | measure + layout | draw(仅重绘) |
| 标记 | PFLAG_FORCE_LAYOUT | PFLAG_DIRTY / PFLAG_DIRTY_OPAQUE |
| 向上传播 | 递归通知所有父 View | 只通知到 ViewRootImpl |
| 性能开销 | 较大(可能触发整棵树重新测量) | 较小(只重绘脏区域) |
| 典型场景 | 改变尺寸、位置、布局参数 | 改变颜色、文字、背景等显示内容 |
我举个例子你就明白了。假设你有一个 TextView,你改了它的文字颜色——这时候只需要调 invalidate,因为尺寸没变,只是显示内容变了。但如果你改了文字大小,那就必须调 requestLayout,因为尺寸可能变了,需要重新测量。
我曾经踩过的坑:有个同事在 RecyclerView 的 Adapter 里,每次数据更新都同时调 requestLayout 和 invalidate。结果列表滑动卡成狗。后来我查了一下,其实 notifyDataSetChanged 内部已经处理了布局请求,他多调的那次 requestLayout 完全是多余的。记住:不要滥用 requestLayout,它的开销比 invalidate 大得多。
12.5 实际开发中的建议
最后,我给大家几个实操建议:
- 能用 invalidate 就别用 requestLayout——只改显示内容时,invalidate 就够了
- 自定义 View 里,如果只改尺寸,记得调 requestLayout——光调 invalidate 不会触发重新测量
- 避免在 onMeasure 或 onLayout 里调 requestLayout——这会导致无限循环,我见过有人这么干,直接 ANR
- forceLayout 适合局部强制测量——在自定义 ViewGroup 里,配合 measure 手动调用,比 requestLayout 更高效
嗯,这一章的内容就到这里。你想想看,requestLayout 其实就是一个「请求信号」,它不干活,只是告诉系统「该干活了」。真正干活的是 ViewRootImpl 的 performTraversals。理解了这一点,你就掌握了 View 布局流程的入口。