刚开始学安卓时,我总疑惑:写好的 XML 布局(比如一个按钮加一段文字),怎么就变成手机屏幕上能看见的界面了?直到后来慢慢接触 View 相关知识,才知道这背后藏着一套 “渲染流水线”—— 从确定 View 的大小,到安排它的位置,再到把内容画出来,每一步都有讲究。今天我们就来聊聊View绘制的底层逻辑。

一、核心前提:View树与渲染三阶段

在开始之前,我们必须要明确一件事情,安卓界面的本质是”View树“结构:顶层为DecorView(对应整个屏幕容器),向下包含ActionBar、内容布局(开发者编写的layout),再逐层嵌套子View(按钮、文本等)。渲染的核心,就是将这颗树逻辑层面的View树,转化为屏幕上的物理像素。

整个渲染流程按固定顺序分为三大阶段,且均为自上而下的递归执行——从DecorView开始,逐层处理所有的子View,直至完成全树渲染:

  1. 测量(Measure):计算每个View的精确宽高(mMeasuredWidth / mMeasuredHeight)
  2. 布局(Layout):确定每个View在父容器中的坐标( left / top / right / bottom )
  3. 绘制(Draw):将View的背景、内容、子View及装饰元素依次绘制到屏幕

二、测量(Measure):View尺寸的确定逻辑

我在最初自定义View时,常遇到wrap_content失效(表现与match_parent一致)的问题,核心原因在于未理解测量阶段的协作机制——View的尺寸并非独立决定,需遵循父View传递的规则。

关键规则:MeasureSpec(测量约束)

其本质是32位整数,包含“模式(Mode)“与”尺寸(Size)“两部分:

        模式(Mode):定义子View的尺寸限制类型

  • EXACTLY:精确尺寸(对应match_parent或具体数值如100dp),父View已确定子View的固定尺寸,子View需直接使用
  • AT_MOST:最大尺寸(对应wrap_content),子View尺寸可根据自身内容调整,但不得超过父View给出的最大限制
  • UNSPECIFIED:无约束(多用于系统内部场景,如ScrollView的子View),子View可自由定义尺寸

        尺寸(Size):配合模式的具体数值(如EXACTLY模式下的100dp,AT_MOST模式下的父容器宽度)

测量的执行流程

  1. 父View(如LinearLayout)根据自身布局规则为每个子View计算对应的MeasureSpec并通过子View的measure()传递给子组件
  2. 子View调用onMeasure(int widthMeasureSpec, int heightMeasureSpce)计算自身宽高(系统View已内置onMeasure实现,会结合内容与MeasureSpce自动适配,自定义View需重写onMeasure,若使用默认实现,wrap_conten会被当做match_parent处理,导致尺寸异常
  3. 子View通过setMeasuredDimension(int measuredWidth, int measuredHeight)保存测量结果,父View收集所有子View尺寸后,确定自身宽高,完成测量

简单来说,父View的onMeasure核心职责有两个:一是计算自身宽高,二是驱动子View完成测量(调用子View的measure),子View的measure被调用后又会触发自己的onMeasure,最终通过setMeasuredDimension保存自身宽高完成测量,若子View也是ViewGroup,它会以同样的逻辑在自己的onMeasure中继续驱动它的子View执行测量,直到所有的View都通过setMeasuredDimension保存好宽高,测量过程完成。

三、布局(Layout):View 位置的定位逻辑​

测量阶段确定尺寸后,布局阶段需明确 View 在父容器中的具体位置,核心是理解安卓 View 的 “相对坐标体系”。​

1. 坐标定义:基于父容器的四值定位​

View 的位置由四个参数描述,均相对于父容器左上角:​

  • left:View 左上角到父容器左侧的距离​
  • top:View 左上角到父容器顶部的距离​
  • right:View 右下角到父容器左侧的距离(计算公式:right = left + 测量宽度)​
  • bottom:View 右下角到父容器顶部的距离(计算公式:bottom = top + 测量高度)​

实例参考:若父容器为宽 300dp、高 200dp 的 LinearLayout,子 View(宽 100dp、高 50dp)需置于左上角,则其坐标为left=0、top=0、right=100、bottom=50,直观反映位置与尺寸的关联。

2. 布局执行流程

  1. 父 View 调用onLayout(boolean changed, int l, int t, int r, int b),遍历所有子 View,结合自身布局规则计算子 View 的坐标
  2. 父 View 调用子 View 的layout()方法传递坐标,子 View 通过layout()保存位置信息
  3. 子 View 重复上述逻辑,为自身的子 View 分配位置,完成全树布局

同样的,父View的onLayout核心职责有两个:一是确认自身位置,二是驱动子View完成布局(调用子View的layout);子View的layout被调用后会保存自身位置,若子View是ViewGroup还会触发自己的onLayout,最终所有View都保存好自身位置,完成布局。

四、绘制(Draw):View 内容的渲染顺序​

绘制阶段需严格遵循固定顺序,否则易出现内容覆盖(如文字被背景遮挡),核心是理解draw()方法的执行步骤。​

1. 绘制核心步骤

View 的draw()方法按以下顺序执行,确保内容层级正确:​

  1. 绘制背景:调用drawBackground(canvas),渲染background属性设置的颜色或图片​
  2. 绘制自身内容:调用onDraw(canvas),为 View 核心内容的绘制逻辑,如 TextView 绘制文字、ImageView 绘制图片;自定义 View 需在此方法中实现个性化绘制(如绘制图形、路径)​
  3. 绘制子 View:调用dispatchDraw(canvas),遍历子 View 并调用其draw()方法,实现 “父 View 先画自身,子 View 后画” 的层级关系(子 View 会覆盖父 View 重叠区域)​
  4. 绘制装饰元素:调用onDrawForeground(canvas),渲染滚动条、foreground属性及tooltip提示文字等装饰内容

需注意的实用细节​

  • 硬件加速兼容:安卓 3.0 + 默认开启 GPU 硬件加速,提升渲染效率,但部分Canvas操作(如drawTextOnPath)不支持,可在 AndroidManifest 中通过android:hardwareAccelerated="false"关闭特定页面的硬件加速​
  • 过度绘制优化:多个不透明 View 重叠时,同一像素会被重复绘制,浪费性能。可通过 “开发者选项→过度绘制检测” 查看(红色区域代表过度绘制严重),优化方式包括移除不必要的背景、使用merge标签减少布局层级

五、屏幕刷新机制:避免卡顿的关键​

新手开发中常遇到界面卡顿,核心原因是渲染耗时超过屏幕刷新周期 —— 安卓屏幕普遍为 60Hz 刷新频率(每秒刷新 60 次),单次渲染(测量 + 布局 + 绘制)需在 16.67ms 内完成,否则会出现 “掉帧”。​

核心触发机制:VSYNC 信号​

  • 屏幕每完成一次刷新,会发送 “垂直同步(VSYNC)” 信号​
  • 信号触发Choreographer(系统调度器),启动 View 树的渲染流程​
  • 若渲染耗时超过 16.67ms,未赶上当前 VSYNC 信号,需等待下一次信号,导致掉帧与卡顿

六、自定义View的关键点

自定义View需要注意的问题本质上都是为了遵循Android View体系的底层设计逻辑,避免因为违背这些逻辑而导致功能异常、性能崩溃或用户体验变差

1. 必须处理wrap_content

因为wrap_content对应AT_MOST模式——父布局只给一个最大可用尺寸,但不指定具体值,如果自定义View不处理AT_MOST,会默认使用父布局给的最大尺寸,这就和match_parent一个效果了

2. 处理padding和margin

拿padding来说,它属于View自身尺寸的一部分,View的绘制区域必须是自身尺寸-padding,否则内容会画出padding区域,这是Android对View内部空间划分的强制约定:父布局给的padding是View的”不可绘制区域“

3. 避免在onDraw中做耗时操作

因为UI渲染依赖“VSync信号”,每16ms刷新一次(对应60fps的流畅度),onDraw运行在UI线程,如果里面有耗时操作,会导致单次绘制超过16ms,错过VSync信号,出现“掉帧”,更严重会导致UI线程被阻塞

4. 不要滥用invalidate()

invalidate()会触发onDraw重绘整个View,即使只有1px的区域变化也会重绘全屏,这会导致GPU做大量无用功,在滑动、动画等高频场景下会显著降低流畅度,局部刷新(invalidate(rect))只重绘指定区域

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐