12、requestLayout与forceLayout:请求重新布局的流程

这一章我们来聊聊 View 体系里两个容易被混淆的方法——requestLayoutforceLayout。说实话,我见过不少开发者在项目里随手调 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_DIRTYPFLAG_DIRTY_OPAQUE 这些绘制标记。所以 ViewRootImpl 在执行 performTraversals 时,会判断是否需要 draw。如果只有布局变化,没有内容变化,draw 阶段会被跳过。

我画了一张流程图,帮你理清整个链路:

requestLayout 触发 measure & layout 流程图 View.requestLayout() 标记 PFLAG_FORCE_LAYOUT 向上递归通知父 View ViewRootImpl.scheduleTraversals() performTraversals() measure() layout() 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 布局流程的入口。


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