第136篇View 绘制流程:measure、layout、draw 三部曲

简介: Android View绘制核心:一次完整流程含measure(定大小)、layout(定位置)、draw(画像素)三关,由ViewRootImpl驱动、Choreographer对齐VSync。关键判定:requestLayout走全流程;invalidate仅重绘;postInvalidate支持子线程。避坑要点:onLayout勿擅自改尺寸、善用clipChildren、数据层去重防无效刷新。

先把结论放在前面:一次 View 的完整绘制要过三关——measure(定大小)、layout(定位置)、draw(画像素),由 ViewRootImpl 驱动,Choreographer 决定时机。 三个必须张口就来的判定:requestLayout 走三关全流程、invalidate 只走 draw 一关、postInvalidate 支持子线程。这三句话答完,后面所有细节都是挂在这条主线上的。

View 绘制流程看起来简单,追问起来全是细节。这篇把它掰开揉碎,从概念到坑点过一遍。

一次帧的完整时间线

从手指抬起(或动画推进)到像素上屏,顺序是这样的:

Choreographer.doFrame()
  ├─ performTraversals()            // ViewRootImpl 内部
  │    ├─ requestLayout() 被标记?→ 走 measure + layout
  │    ├─ 计算下一帧目标时间(vsync 对齐)
  │    └─ scheduleTraversals() → 下一帧执行
  │
  ├─ performMeasure()               // 三关之一:定大小
  ├─ performLayout()                // 三关之二:定位置
  └─ performDraw()                  // 三关之三:画像素
       └─ draw() → onDraw() + 绘制子 View

关键点:measure 与 layout 遍历的是同一棵树,但 draw 有裁剪优化——ViewGroup.draw 会先 clipChildren 判断哪些子 View 需要画,若某个子 View 完全在裁剪区外且 setWillNotDraw(false),就直接跳过它的 onDraw。这也是为什么"自定义 View 放容器里不显示"要查 clipChildren。

Choreographer 的作用要说清:它把一帧的时间切成 input / animation / traversal / commit 四个阶段,并把"计算耗时"反馈到 FrameMetrics,让系统能统计卡顿率。"一帧 16.6ms"的说法并不完整——正确表述是"目标 vsync 间隔内要完成遍历、绘制、合成并上屏,超时就掉帧"。

最常见的坑是

在 onLayout 里改尺寸却不调 requestLayout,布局与实际状态不一致。

onLayout 的语义是"给子 View 定位",在父 View 的 onLayout 里调用 child.layout() 去改子 View 尺寸,在很多情况下不会触发新的 measure/layout 流程,结果是父 View 记录的尺寸与子 View 实际占用的不一致,下一次布局时又被打回,表现为界面偶发错位或滚动范围算错。

规范做法是分清两种场景:

// 场景一:需要尺寸变化引发重新布局 → 主动 requestLayout
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
   
    val w = MeasureSpec.getSize(widthMeasureSpec)
    // 依赖可用宽度的场景:算出高度后要重算
    val h = computeHeightByWidth(w)
    setMeasuredDimension(w, h)          // 写回,避免被 EXACTLY 覆盖
}

// 场景二:只是轻微偏移、不影响尺寸 → 不需要重新布局
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
   
    child.layout(l + offset, t, r + offset, b + offset)   // 手工定位
}

还有一个更隐蔽的坑:围绕 View 绘制流程协作时最有效的一条约定,是把适用前提显式写在代码旁,让 review 有据可依。落到具体就是"刷新 API 使用约定":

需求 用什么 常见误用
尺寸/位置要变 requestLayout() 用 invalidate 后手工改尺寸,布局系统不知情
只重画内容(颜色、文字、图片) invalidate() 用 requestLayout 造成整树重新遍历
子线程刷新 UI postInvalidate()(内部切主线程) 直接在子线程调 invalidate(),抛 CalledFromWrongThreadException
布局发生改变时做联动 addOnLayoutChangeListener 或自定义 onLayout 在 onMeasure 里改兄弟 View 的属性

再加一条高频误用:invalidate 频繁调用但内容没变。 典型场景是进度条每秒更新一次、动画每帧都调 invalidate,而内容其实一致。修法是先判断值是否真的变了再刷新——这也是 StateFlow 配 distinctUntilChanged 的价值所在。用数据层的去重挡住无效刷新,比在 View 层优化更彻底。

三条高频追问

问:requestLayout、invalidate、postInvalidate 三者区别?

requestLayout 标记"布局需要重算",会走 measure + layout + draw 三关(ViewRootImpl 下一次 performTraversals 统一处理);invalidate 只标记"需要重画",跳过 measure/layout 直接进 draw;postInvalidate 内部把请求投递到主线程的 ViewRootImpl.Handler,因此可以在子线程调用。延伸问题:会死循环吗? 不会——requestLayout 在同一帧内只会被处理一次(有个"布局请求已消费"的标记),所以"onLayout 里 requestLayout"虽不推荐但不会无限循环,只是会把下一帧也占掉。

问:为什么 measure 可能被调用多次?

因为 onMeasure 内部调 measureChildWithMargins 时可能触发子 View 再 measure,而 LinearLayout 的 weight、RelativeLayout 的依赖链(layout_above 等)会引发二次测量(measure 阶段就要用 measureChildBeforeLayout 推算位置)。更深一层:LinearLayout 的 weight 重测会把 useLargestChild 的策略触发成二次完整 measure。优化手段是用 ConstraintLayout 替代嵌套权重布局——它用一次求解代替多轮测量,这也是它性能优势的真实来源。

问:自定义 View 的 onDraw 没被调用?

四种情况:① 没调 setWillNotDraw(false)——ViewGroup 默认 willNotDraw = true 会跳过自身 onDraw;② View 是不可见的(GONE/透明/尺寸 0);③ 父容器 clipChildren 把它裁掉了;④ 被 ViewGroup 的绘制优化跳过(子 View 完全在裁剪区外)。排查顺序按这个列表走,几乎都能命中。

问:帧率与流畅度的关系?

60Hz 屏幕每帧预算约 16.6ms,120Hz 约 8.3ms。但卡顿的真正瓶颈通常不在 onDraw 本身,而在"measure/layout 的重复测量"与"主线程被非绘制工作占用"(比如在主线程做 JSON 解析、布局里做 I/O)。所以定位卡顿要先分清是"绘制耗时"还是"布局耗时"还是"业务耗时"——FrameMetrics 与 Choreographer 的 trace 都能给出答案。

落到项目里怎么做

一条能直接落地的规则:状态变更 → 选对刷新 API → 用数据层去重挡无效刷新。 三步走完,View 层的性能问题基本消失。

配套实践三条:① 统一刷新入口:所有 UI 更新经过一个 render(state) 方法,内部先 if (newState == oldState) return;② 布局层面用 ConstraintLayout 减少嵌套层级与 weight 重测;③ 接 FrameMetrics 采集卡顿数据,把"掉帧率"纳入性能看板——没有数据的性能优化是猜。

给正在准备面试的你

这题要答成"一张地图 + 三个判定 + 四类排查"。推荐这条线:先画一遍 Choreographer → performTraversals → measure/layout/draw 的时间线 → 立起三个判定(requestLayout 全流程 / invalidate 只重绘 / postInvalidate 可子线程)→ 讲为什么 measure 会多次(weight 重测、ConstraintLayout 一次求解)→ 落到 onDraw 不被调用的四种情况排查 → 最后用"刷新 API 约定表 + 主线程纪律"收尾。能被追问到帧率与 FrameMetrics 还不慌,这题就稳了。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。

「Android软件开发面试·从入门到精通」连载系列

上一篇:进程优先级与进程回收:ADJ-与查杀顺序

下一篇预告:MeasureSpec-与测量模式:三种模式的取舍

有任何问题欢迎在评论区留言交流。

相关文章
|
19天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
8783 25
|
17天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
3344 15
|
17天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
2194 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
11天前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
17天前
|
云安全 人工智能 安全
|
3天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型深度拆解:原理解读、实战测评与保姆级落地教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为**System One Model(系统一模型)**,对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
363 1
|
6天前
|
人工智能 Linux Windows
千问办公(QwenWork)官网入口:其实有2个,一个是网页端千问办公,一个是介绍指南页面
千问办公(QwenWork)是阿里云推出的AI智能办公平台,支持网页端直接使用及Windows/Mac/Linux客户端下载。提供PPT生成、财报分析、网页搭建等AI功能,个人版免费,企业版198元/席/月。详情见官网qwenwork.cn或阿里云产品页。
803 0
千问办公(QwenWork)官网入口:其实有2个,一个是网页端千问办公,一个是介绍指南页面

热门文章

最新文章