一、View绘制流程总览:从Activity启动到屏幕显示
大家好,我是你们的老朋友。今天咱们来聊聊Android View绘制流程的全貌。
说实话,我刚入行那会儿,对View的绘制也是一头雾水。只知道setContentView扔个布局进去,屏幕就显示出来了。至于中间发生了什么,完全是个黑盒。直到有一次,我接手了一个卡顿严重的项目——列表滑动掉帧、界面加载慢得像蜗牛。那时候我才意识到,不懂绘制流程,你连性能优化的门都摸不着。
所以,这一章咱们先把整条链路打通。从Activity启动,到屏幕上出现像素,中间经历了什么?核心三件套——measure、layout、draw,它们各自干什么?调用顺序是怎样的?
嗯,别急,咱们一步步来。
1.1 整体链路:从Activity到屏幕
先看一张图,把整件事串起来。
这张图把整条链路画清楚了。我个人的习惯是,遇到复杂的流程,先画一张这样的总览图,心里就有底了。
咱们拆开来看:
- Activity启动:系统创建Activity实例,调用onCreate()。
- setContentView:解析XML布局,构建View树。注意,这时候View还没显示出来,只是把结构搭好了。
- DecorView & ViewRootImpl:DecorView是顶级容器,ViewRootImpl是连接View和WindowManager的桥梁。ViewRootImpl负责触发绘制流程。
- 核心三件套:measure、layout、draw,由ViewRootImpl.performTraversals()统一调度。
- 屏幕显示:绘制结果通过SurfaceFlinger合成,最终显示在屏幕上。
关键点:ViewRootImpl.performTraversals() 是整个绘制流程的入口。它会在什么时候被调用?当窗口需要刷新时(比如第一次显示、调用invalidate()、布局变化等),系统会通过Choreographer机制触发一次遍历。
1.2 核心三件套:measure、layout、draw
好,现在咱们聚焦到核心三件套。这三个方法,说白了就是回答三个问题:
- measure:这个View该有多大?
- layout:这个View该放在哪?
- draw:这个View该怎么画?
顺序是固定的:先measure,再layout,最后draw。为什么?你想想看,不知道大小,怎么确定位置?不知道位置,往哪儿画?
我曾经在项目里遇到过一个问题:自定义ViewGroup的子View显示不全。排查了半天,发现是measure阶段算出来的尺寸不对,导致layout阶段把子View放到了父容器外面。嗯,从那以后,我每次写自定义ViewGroup,都会先确认measure的结果。
1.3 measure:测量阶段
measure阶段的目标是确定每个View的宽高。系统会从根View(DecorView)开始,递归地调用每个子View的measure()方法。
measure()方法接收两个参数:widthMeasureSpec和heightMeasureSpec。这两个参数包含了父容器对子View的尺寸约束。说白了,就是父容器告诉子View:「你最多能长这么宽,你自己看着办。」
每个View在onMeasure()中根据MeasureSpec计算出自己的宽高,然后调用setMeasuredDimension()保存结果。
个人经验:MeasureSpec有三种模式:UNSPECIFIED(无限制)、EXACTLY(精确值)、AT_MOST(最多不超过)。我刚开始做开发时,经常搞混EXACTLY和AT_MOST的区别。后来我总结了一个记忆方法:EXACTLY是「我说了算」,AT_MOST是「你说了算,但不能超过我定的上限」。
1.4 layout:布局阶段
layout阶段的目标是确定每个View在父容器中的位置。系统会从根View开始,递归调用每个子View的layout()方法。
layout()方法接收四个参数:left、top、right、bottom。这四个参数决定了View的四个边界位置。说白了,就是告诉子View:「你就待在这个矩形区域里,别乱跑。」
每个View在onLayout()中会遍历自己的子View,调用它们的layout()方法,把位置信息传递下去。
注意:layout阶段只决定位置,不改变尺寸。尺寸是在measure阶段定下来的。如果你在layout阶段试图修改尺寸,虽然技术上可行,但会导致额外的measure开销。我曾经见过一个项目,在onLayout里改了子View的尺寸,结果触发了多次measure,性能直接崩了。
1.5 draw:绘制阶段
draw阶段的目标是把View的内容画到Canvas上。系统会从根View开始,递归调用每个View的draw()方法。
draw()方法做的事情,说白了就是三步:
- 绘制背景(如果有的话)
- 绘制自身内容(调用onDraw())
- 绘制子View(调用dispatchDraw())
注意,绘制顺序是:先画父View的背景,再画父View的内容,然后画子View。所以子View会覆盖在父View的内容之上。
我遇到过一个问题:自定义View的onDraw()里画的东西被子View挡住了。后来发现是绘制顺序的问题——子View在父View之后绘制,自然就盖住了。解决方案是把子View的绘制放到父View之前,或者调整View的层级。
1.6 调用顺序与触发机制
咱们用一张表总结一下三件套的调用顺序和职责:
| 阶段 | 方法 | 职责 | 触发条件 |
|---|---|---|---|
| measure | measure() → onMeasure() | 确定View的宽高 | ViewRootImpl.performTraversals() |
| layout | layout() → onLayout() | 确定View的位置 | measure完成后自动触发 |
| draw | draw() → onDraw() | 绘制View的内容 | layout完成后自动触发 |
触发机制是这样的:当ViewRootImpl收到刷新请求时,会调用performTraversals()。这个方法内部会依次调用performMeasure()、performLayout()、performDraw()。这三个方法分别触发measure、layout、draw的递归遍历。
注意,这三个阶段是串行的。measure没完成,layout不会开始;layout没完成,draw不会开始。所以,任何一个阶段出现性能问题,都会拖慢整个绘制流程。
避坑指南:我曾经在一个项目中,发现某个自定义View的onMeasure()里做了大量计算,导致measure阶段耗时严重。整个界面加载慢得像PPT翻页。后来我把计算逻辑移到了后台线程,measure阶段只做简单的尺寸判断,性能就上来了。记住:onMeasure()里不要做耗时操作,它会被频繁调用。
1.7 小结
这一章咱们把View绘制流程的全貌梳理了一遍。从Activity启动到屏幕显示,中间经历了setContentView、DecorView创建、ViewRootImpl建立,最后是核心三件套:measure、layout、draw。
measure决定大小,layout决定位置,draw决定内容。顺序不能乱,缺一不可。
下一章,咱们会深入measure阶段,看看MeasureSpec到底是怎么工作的,以及如何正确地测量一个View。到时候我会分享一些我在实际项目中踩过的坑,希望对你有帮助。
公众号:蓝海资料掘金营,微信deep3321