简介
之所以有这一章节内容,主要还是考虑 APP 优化性能这一块。对于优化性能一般从:渲染、运算和内存、电量 这三个方面进行考量的。所以还是有必要要了解一下 Android 的渲染机制。
我们知道 Android 系统每隔 16ms 就会重新绘制一次页面,也就是说,我们的应用必须要在 16ms 内完成屏幕刷新的全部逻辑操作,即每一帧只能停留 16ms,渲染机制理清楚后再去考虑如何优化 UI 卡顿,才是正确的探索之路。
View 渲染管线全景
在深入 16ms 机制之前,先了解从应用层绘制到屏幕显示的完整链路:
View.onDraw (UI Thread) |
理解这个管线的每一个阶段,才能精准定位帧率瓶颈。
16ms 机制
为什么是 16ms?
16ms 意味着 1000/60Hz = 16.6ms,相当于 60fps。这是因为人眼与大脑之间的协作无法感知超过 60fps 的画面更新。12fps 大概类似手动快速翻动书籍的帧率,这明显是可以感知到不够顺滑的。24fps 使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的效果。24fps 是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。但是低于 30fps 是无法顺畅表现绚丽的画面内容的,此时就需要用到 60fps 来达到想要的效果,超过 60fps 就没有必要了。如果我们的应用没有在 16ms 内完成屏幕刷新的全部逻辑操作,就会发生卡顿。
在现代高刷新率设备(90Hz、120Hz)上,这个窗口进一步缩小到约 11ms 或 8.3ms,对性能要求更严格。
为什么 16ms 未完成绘制会卡顿?
Android 系统每隔 16ms 就发出一次 VSYNC 信号,触发对 UI 进行渲染。VSYNC 是 Vertical Synchronization(垂直同步)的缩写,是一种在 PC 上很早就广泛使用的技术,可以简单的把它认为是一种定时中断。而在 Android 4.1(JB)中已经开始引入 VSync 机制。

上图所示是 VSync 机制下的绘制流程图。从上图可以看出,CPU 和 GPU 的处理时间都是少于一个 VSync 的间隔的,即 16.6ms。如果每个间隔都有绘制的情形下,当前的 fps 即为 60 帧。
当 CPU 和 GPU 处理时间都比较慢,或者因为其他原因,导致主线程中 do heavy work,那么就会出现如下图所示的状况:

如上图所示,CPU 和 GPU 的第一个处理时间因为各种原因均大于一个 VSync 间隔(16.6ms),所以在第二个 VSync 还在处理 1 区域的绘制时,就不可能实现理论上的 FPS60,同时也出现了丢帧(SF: Skipped Frame)情况,试想用户盯着同一张图看了 32ms 而非 16ms,当然很容易察觉出卡顿,哪怕是出现仅有的一次,用户也能感觉得到不流畅,因此找出导致没能完成绘制的罪魁祸首就能进行实质性的性能优化工作,做到事半功倍。
渲染原理
渲染操作通常依赖于两个核心组件:CPU 和 GPU。CPU 负责包括 Measure,Layout,Record,Execute 的计算操作;GPU 负责 Rasterization(栅格化) 操作。所谓的栅格化就是绘制那些 Button、Shape、Path、String、Bitmap 等组件最基础的操作。它把那些组件拆分到不同的像素上进行展示,说得通俗一点,就是解决那些复杂的 XML 布局文件和标记语言,使之转化为用户可以看得懂的图像。
因此,16ms 的时间主要是被两件事所占用,第一:将 UI 对象转换为一系列多边形和纹理操作;第二:CPU 传递处理数据到 GPU。所以很明显,只要缩短这两部分时间即可,也就是说需要尽量减少对象转换次数,以及传递数据的次数。

在 CPU 方面,最常见的性能问题是不必要的布局和失效,这些内容必须在视图层次结构中进行测量、清除并重新创建,引发这种问题通常有两个原因:一是重建显示列表的次数太多,二是花费太多时间作废视图层次并进行不必要的重绘,这两个原因在更新显示列表或者其他缓存 GPU 资源时导致 CPU 工作过度。在 GPU 方面,最常见的问题是我们所说的过度绘制(overdraw),通常是在像素着色过程中,通过其他工具进行后期着色时浪费了 GPU 处理时间。
- CPU 产生的问题:不必要的布局和失效
- GPU 产生的问题:过度绘制(overdraw)
硬件加速深度解析
什么是硬件加速
Android 从 3.0(API 11)开始支持硬件加速,从 4.0(API 14)开始默认开启。硬件加速的核心思想是将 View 的绘制命令录制为 GPU 可执行的指令(DisplayList/RenderNode),交给 GPU 执行,而不是由 CPU 直接渲染像素。
DisplayList(RenderNode)录制机制
源码位置:frameworks/base/core/java/android/view/View.java
开启硬件加速后,View 的绘制过程变成录制绘制命令而非直接绘制:
public void draw(Canvas canvas) { |
updateDisplayListIfDirty 只有在 View 内容”脏”了(调用过 invalidate)时才会重新录制 DisplayList:
public RenderNode updateDisplayListIfDirty() { |
DisplayList 被录制并缓存后,只要 View 的内容没变,后续帧不需要重新执行 onDraw。View 的平移、缩放、旋转、透明度变化都是通过修改 RenderNode 的变换矩阵来实现的,完全不触发 DisplayList 重录。这就是硬件加速带来巨大性能提升的关键原理。
RenderThread:渲染线程分离
Android 5.0 引入了 RenderThread,将渲染管线拆分为 UI 线程和独立渲染线程两个阶段:
UI Thread (主线程): |
关键优势:UI 线程的准备工作可以与 GPU 的绘制工作流水线并行。当 GPU 在处理第 N 帧时,UI 线程已经可以开始准备第 N+1 帧。
源码位置:frameworks/base/libs/hwui/renderthread/RenderThread.cpp
RenderNode 的变换属性
每个 RenderNode 都支持独立的变换操作,这些操作不触发 DisplayList 重录:
renderNode.setTranslationX(x); // 水平平移 |
因此,View 的属性动画(如 translationX/Y、scaleX/Y、alpha 等)在硬件加速下非常高效。
三重缓冲与 BufferQueue
BufferQueue 模型
Android 图形系统使用生产者-消费者模型管理图形缓冲区:
生产者(应用进程/GPU):绘制内容到缓冲区 |
三重缓冲
三重缓冲(Triple Buffering)维护三个 GraphicBuffer:
| 缓冲区 | 状态 | 说明 |
|---|---|---|
| Buffer A | 正在显示 | 屏幕当前显示的内容 |
| Buffer B | GPU 处理中 | 正被渲染的帧 |
| Buffer C | 空闲 | CPU 可以开始准备下一帧 |
三重缓冲的优势:当 GPU 偶尔慢于一个 VSYNC 周期时,CPU 不必等待 GPU 完成即可使用第三个缓冲区继续准备下一帧,避免”流水线空等”导致的连锁丢帧。
dumpsys gfxinfo:帧性能统计
dumpsys gfxinfo 是分析 UI 帧性能最直接的工具:
adb shell dumpsys gfxinfo com.example.app |
输出示例:
Stats since: 18557448508958ns |
关键指标解读:
| 指标 | 含义 | 优化方向 |
|---|---|---|
| Janky frames | 超过 16ms 的帧 | 整体性能优化衡量指标 |
| Missed Vsync | 错过 VSYNC 信号的帧数 | 检查主线程耗时操作 |
| High input latency | 输入延迟高的帧 | 检查 Input 处理回调 |
| Slow UI thread | UI 线程慢的帧 | 优化 measure/layout/draw |
| Slow bitmap uploads | 位图上传慢的帧 | 检查位图尺寸和解码时机 |
| Slow issue draw commands | GPU 命令提交慢的帧 | 检查过度绘制和复杂 Shader |
framestats 详细帧分解
adb shell dumpsys gfxinfo com.example.app framestats |
输出的每帧数据包含各阶段耗时(纳秒),列含义:FRAME_TIME, VSYNC, INPUT_HANDLING, ANIMATION, MEASURE_LAYOUT, DRAW, SYNC, COMMAND_ISSUE, SWAP_BUFFERS 等。可以解析这些数据,生成每帧各阶段耗时分布图,精确定位瓶颈。
Profile GPU Rendering(GPU 呈现模式分析)
开发者选项中的”GPU 呈现模式分析”以柱状图形式可视化每帧耗时。
各颜色段从上到下对应渲染管线的不同阶段:
| 颜色 | 阶段 | 说明 |
|---|---|---|
| 紫色 | Input Handling | 处理输入事件 |
| 红色 | Animation | 执行动画回调 |
| 橙色 | Measure/Layout | onMeasure + onLayout |
| 黄色 | Draw | 创建/更新 DisplayList |
| 浅蓝色 | Sync/Upload | 同步 RenderNode、上传 bitmap 纹理 |
| 深绿色 | Issue Commands | RenderThread 向 GPU 提交绘制命令 |
| 浅红色 | Swap Buffers | GPU 完成绘制后交换缓冲区 |
绿色横线代表 16ms 基准线。解读方法:
- 橙色段高:布局过于复杂,优化布局层级。
- 黄色段高:onDraw 慢,检查过度绘制和复杂路径。
- 浅蓝色段高:可能正在上传大尺寸 bitmap 到 GPU 纹理。
- 深绿色段高:GPU 执行慢,可能有过多的图层或复杂 Shader。
FrameMetrics API(API 24+)
Android 7.0 引入 FrameMetrics API,可在代码中监听每帧性能数据:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { |
相比 dumpsys gfxinfo 的事后统计,FrameMetrics API 可在线监听并做出实时反应。
过渡绘制(Overdraw)检测
Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的 UI 结构里面,如果不可见的 UI 也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的 CPU 以及 GPU 资源。
按照以下步骤打开 Show GPU Overdraw 的选项:设置 -> 开发者选项 -> 调试 GPU 过度绘制 -> 显示 GPU 过度绘制。
颜色含义:
- 原色:无过度绘制(1 次绘制)
- 蓝色:1 次过度绘制(2 次绘制)
- 绿色:2 次过度绘制(3 次绘制)
- 浅红色:3 次过度绘制(4 次绘制)
- 深红色:4 次及以上过度绘制
优化目标是让大部分区域保持在蓝色到绿色范围内。
Overdraw 的处理方案
Overdraw 处理方案一:去掉 window 的默认背景
当使用 Android 自带某些主题时,window 会被默认添加一个纯色背景,这个背景是被 DecorView 所持有的。
当我们的自定义布局时又添加了一张背景图或者设置背景色,那么 DecorView 的 background 此时对我们来说是无用的,但是它会产生一次 Overdraw,带来绘制性能损耗。
处理方法:
可以在 onCreate() 中 setContentView() 之后调用 getWindow().setBackgroundDrawable(null);
或者在 theme 中添加 android:windowBackground="null"。
Overdraw 处理方案二:去掉其他不必要的背景
有时为了方便会先给 layout 设置一个整体的背景,再给子 View 设置背景,这时也会造成重叠绘制,如果子 View 宽度为 match_parent,则会覆盖 layout 的部分,因此这里可以通过分别设置背景来减少重绘;
再比如如果采用的是 selector 作为背景,将 normal 状态的 color 设置为 "@android:color/transparent",也可以解决同样问题。
Overdraw 处理方案三:clipRect 的使用
通过 canvas.clipRect() 来帮助系统识别可见区域。
处理方法:指定一块矩形区域,并且只可以在这个区域操作绘制,其他区域忽视。
方案优势:这个系统 API 可以很好的帮助处理那些有多组重叠组件的自定义 View 来控制显示的区域。同时 clipRect 还可以帮助节约 CPU 和 GPU 的资源开销,在 clipRect 区域之外的绘制指令是不会执行的。
Overdraw 处理方案四:自定义 ViewGroup 裁剪子 View
对于已知重叠关系的 ViewGroup,可在 dispatchDraw 中精确裁剪:
|
Overdraw 处理方案五:ViewStub
ViewStub 又称为”延迟化加载”,程序无需显示 ViewStub 所指向的布局文件,只有在特定的某些条件下,此时 ViewStub 所指向的布局文件才需要被 inflate,且此布局文件直接将当前 ViewStub 替换掉,具体是通过 viewStub.inflate() 或 viewStub.setVisibility(View.VISIBLE) 来完成;
示例 xml 如下:
|
private void showNetError() { |
Overdraw 处理方案六:Merge 标签
Merge 标签可以干掉一个 view 层级。
Merge 的作用很明显,但是也有一些使用条件的限制。有两种情况下我们可以使用 Merge 标签来做容器控件:
使用场景:
第一种:子视图不需要指定任何针对父视图的布局属性,也就是说父容器仅仅作为容器,子视图只需要直接添加到父视图上用于显示就行。
第二种:子视图的”根”布局和父布局(视图)一样的,这样就多一层无用的嵌套。
【注1】 <merge /> 只可以作为 xml layout 的根节点。
【注2】 当需要扩充的 xml layout 本身是由 merge 作为根节点的话,需要将被导入的 xml layout 置于 ViewGroup 中,同时需要设置 attachToRoot 为 True。
Hierarchy Viewer - 优化布局层次
HV 的使用
Hierarchy Viewer 接触过 Android 的人估计都用过,如果在真机上可以使用 ViewServer 这个第三方库。
在 Hierarchy Viewer 窗口中,所有的子 View 上面都有 3 个圈圈(取色范围是红、黄、绿),这三个圈圈分别代表 measure、layout、draw 的速度,并且可以看到实际的运行速度,如果发现某个 View 的圈是红色的,那么说明这个 View 相对其他的 View,其操作运行较慢,当然这只是相对别的 View。
布局常见问题及优化建议
没有用的父布局是指没有背景绘制或者没有大小限制的父布局,这样的布局不会对 UI 效果产生任何影响。我们可以把没有用的父布局,通过
<merge/>标签合并来减少 UI 的层次;使用线性布局 LinearLayout 排版导致 UI 层次变深,如果有这类问题,我们就使用相对布局 RelativeLayout 或者 ConstraintLayout 代替 LinearLayout,减少 UI 的层次;
不常用的 UI 被设置成 GONE,比如异常的错误页面,如果有这类问题,我们需要用
<ViewStub/>标签,代替 GONE 提高 UI 性能。
ConstraintLayout:扁平化布局
ConstraintLayout 通过约束关系替代嵌套布局,大幅减少布局层级。其内部使用 Cassowary 线性约束求解算法,在单次测量中同时计算所有子 View 的位置和尺寸。
核心优势:
- 避免嵌套 LinearLayout 带来的双重测量(weight 场景)。
- 单层即可实现复杂的相对位置关系。
- 支持 Barrier、Group、Flow 等高级辅助控件。
- 支持百分比尺寸(layout_constraintWidth_percent)。
常见性能陷阱与优化案例
陷阱一:在 onDraw 中分配对象
// 错误做法:每帧分配新对象,触发频繁 GC |
陷阱二:频繁触发 requestLayout
每次 requestLayout 都会沿视图树向上标记,最终触发 ViewRootImpl 的完整 measure + layout + draw 流程。如果只是内容变化(如文字颜色改变),应使用 invalidate() 代替。
陷阱三:未启用硬件层优化动画
对于动画过程中内容不变(只有位置、透明度或缩放变化)的 View:
view.setLayerType(View.LAYER_TYPE_HARDWARE, null); |
硬件层将 View 渲染到 GPU 纹理中,动画期间只需对纹理做矩阵变换,无需每帧重新执行 onDraw。
注意:不要对大量 View 同时使用硬件层——每个层都是一个 GPU 纹理,消耗显存。通常只在动画持续期间临时启用,动画结束后立即释放。
陷阱四:RecyclerView 中的复杂 item
优化手段:
- 使用
setHasFixedSize(true)减少不必要的重新测量。 - 使用
setItemViewCacheSize()增加离屏缓存。 - 使用
RecycledViewPool在多 RecyclerView 间共享 ViewHolder。 - 使用
GapWorker在空闲时提前预加载。
陷阱五:大图直接解码在 UI 线程
BitmapFactory.decodeResource 在 UI 线程解码大图可能阻塞数百毫秒。应在后台线程解码:
// 使用 Glide / Coil 等图片库,它们已做好异步处理 |
Systrace / Perfetto 帧分析
Systrace 简介
Systrace 是 Android 平台最强性能分析工具之一,收集内核和 Framework 的时间信息,生成可交互的时间轴。
# Android 9 及以下使用 systrace.py |
在 trace 中定位帧问题
打开 trace 后,关键查找目标:
- F 标记:代表一帧的开始,出现在 Choreographer#doFrame 行。
- 绿色 F 标记:正常帧(< 16ms);红色 F 标记:卡顿帧。
- SurfaceFlinger 行:查找 VSYNC-sf 和 VSYNC-app 信号。
- measure/layout/draw 阶段:在 ViewRootImpl 行找到 performTraversals,展开可见各阶段耗时。
常见模式诊断:
- F 标记间距 > 16ms:主线程有耗时操作,检查此时执行的代码。
- 连续的红色 F 标记:主线程长时间阻塞,可能是 GC 或 IO 操作。
- VSYNC-app 和 VSYNC-sf 间距大:GPU 处理慢,检查过度绘制。
总结
Android View 的渲染机制是一条多层协作的精密管线:
- VSYNC 信号触发 Choreographer.doFrame。
- UI Thread 完成 measure、layout,并录制 DisplayList(硬件加速)或直接绘制到 Canvas(软件绘制)。
- RenderThread(硬件加速下)将 DisplayList 树提交到 GPU 执行。
- BufferQueue 管理缓冲区交换,三重缓冲防止流水线停顿。
- SurfaceFlinger 合成所有应用层的 Surface,通过 HWC 或 GPU 最终输出到显示器。
性能优化的核心思路:
- 减少 CPU 工作:扁平化布局、避免不必要的 requestLayout、减少 onDraw 开销。
- 减少 GPU 工作:消除过度绘制、合理使用硬件层。
- 消除流水线停顿:确保单帧处理在 16ms 内完成。
关键源码文件
| 文件 | 路径 |
|---|---|
| View.java | frameworks/base/core/java/android/view/View.java |
| ViewRootImpl.java | frameworks/base/core/java/android/view/ViewRootImpl.java |
| Choreographer.java | frameworks/base/core/java/android/view/Choreographer.java |
| ThreadedRenderer.java | frameworks/base/core/java/android/view/ThreadedRenderer.java |
| RenderNode.java | frameworks/base/graphics/java/android/graphics/RenderNode.java |
| DisplayListCanvas.cpp | frameworks/base/libs/hwui/DisplayListCanvas.cpp |
| RenderThread.cpp | frameworks/base/libs/hwui/renderthread/RenderThread.cpp |
| SurfaceFlinger | frameworks/native/services/surfaceflinger/ |
| BufferQueue | frameworks/native/libs/gui/ |

