Android 图形渲染框架

Android 的图形渲染框架负责将应用程序的 UI 元素(View、Canvas、OpenGL 等)渲染到屏幕上。它的核心目标是高效、流畅地将图形内容从应用程序传递到硬件显示设备。整个框架涉及多个层次,包括应用层、系统层、图形 API 以及硬件层。

用户交互、动画、布局变化等会调用 View.invalidate()requestLayout()Choreographer 会在下一帧(VSync 信号到来时)调用 ViewRootImpl.doFrame()。这个绘制必须在VSync 屏幕刷新信号发送时间间隔完成,否则会导致掉帧。

绘制包括以下三个流程:

  1. measure:测量每个 View 的大小(View.measure()onMeasure())。
  2. layout:确定 View 在父布局中的位置(View.layout()onLayout())。
  3. draw:绘制内容(View.draw()dispatchDraw())。

绘制到 Canvas 有两种模式,一种是通过 CPU 绘制 Bitmap 位图,称为软绘制,另一种是通过 GPU 进行绘制,即硬件加速,CanvasHardwareCanvas,绘制操作会记录为渲染命令列表 DisplayList,不直接画到屏幕。

从 Android 3.0+ 开始默认开启硬件加速模式,在这个模式下,在 View.draw() 中,绘制命令不是立即执行,而是调用 RecordingCanvasdrawRectdrawBitmap 这些指令录制成 GPU 可以理解的命令列表。这样可以避免每次绘制都经过 Java → Native 的高频调用。从 Android 5.0 开始引入了 RenderThread,通过独立线程处理 GPU 绘制,减少 UI 线程卡顿。UI 线程只生成 DisplayList,提交给 RenderThread。RenderThread 用 OpenGL ES(或 Vulkan)把 DisplayList 转换成 GPU 绘制指令,并绘制到 Surface 的缓冲区。Android 默认使用 Skia 图形库调用 OpenGL ES。从 Android 10 开始支持 Vulkan Renderer(更高性能)。

绘制结果需要存放到一个 Surface,Surface 可以理解成显存中的画布,由 SurfaceViewTextureView 或 Window 产生,内部持有一个 BufferQueue。BufferQueue 是生产者-消费者模型,其中的生产者是应用进程的 RenderThread,消费者是 SurfaceFlinger。BufferQueue 使用双缓冲和三缓冲工作模式,双缓冲和三缓冲的概念源自更底层的图形缓冲机制,BufferQueue 只是 Android 中承载这种机制的实现载体。在 Android 图形渲染中,CPU(UI Thread/RenderThread)和 GPU(绘制)是异步工作的。如果只有一个缓冲区,CPU 正在写数据时,GPU 就不能读,会造成撕裂或花屏,必须等 CPU 写完,GPU 才能渲染,这样就容易卡顿,无法充分利用硬件并行性,因此需要 多缓冲 来解耦 生产者(绘制端)消费者(显示端) 的速度。三缓冲比双缓冲多一个备用缓冲,防止生产者被阻塞,提升流畅度。

接下来 Android 的系统服务 SurfaceFlinger 负责把多个应用窗口的 Surface 合成到最终屏幕图像中。SurfaceFlinger 从每个应用的 BufferQueue 中取出最新的图像缓冲,这些 Surface 可能来自主界面、弹窗、系统栏等不同应用,如果硬件支持 HWC,由 Hardware Composer 合成,性能更好,否则由 GPU 合成。SurfaceFlinger 等待硬件发来的 VSync 屏幕刷新信号来提交下一帧。这就是为什么 Android 的绘制频率通常是 60Hz / 90Hz / 120Hz。

如果硬件支持,Hardware Composer HAL 提供接口让 SurfaceFlinger 把图层交给硬件直接合成,避免 GPU 额外消耗。HWC 会直接把合成结果交给显示驱动 Framebuffer。显示驱动负责把像素信号传给屏幕的 TCON(Timing Controller),最终刷新到 LCD/OLED 面板。

这其中,Skia 是 Android 的底层 2D 图形库(Chrome 也用它),它支持 CPU 渲染(软件)和GPU 渲染(OpenGL ES / Vulkan),Canvas 的所有 API(如 drawLine、drawPath)最终都会调用 Skia。

总结一下,在这个图形绘制过程中存在 4 个关键线程:

  1. UI Thread:执行 View 的 measure/layout/draw,生成 DisplayList。
  2. RenderThread:处理 GPU 绘制命令,渲染到 Surface。
  3. SurfaceFlinger Thread:合成多个 Surface,提交到屏幕。
  4. HWC Thread:硬件合成,驱动输出到屏幕。

根据这个过程,可以得出一些性能优化的方向。由于绘制内容在 UI 主线程生成,开发者应避免阻塞 UI 线程的操作,如复杂计算或 I/O。View 的绘制指令被缓存为 DisplayList 以避免重复计算,开发者可以通过仅重绘发生变化的 View(通过 invalidate()requestLayout() )来优化重绘,同时尽量减少 View 树的变化,优化布局复杂度。同一像素被多次绘制会导致性能浪费,可以使用 clipRectclipPath 限制绘制区域,避免不必要的背景重叠,同时使用 Android Studio 的 GPU 渲染分析工具来检测 Overdraw 。

一些常见的渲染问题及解决方法:

  1. 掉帧(Jank)。原因:UI 线程或 RenderThread 过载。解决:优化布局、减少复杂动画、使用异步任务。
  2. 画面撕裂。原因:渲染与屏幕刷新不同步。解决:确保 VSync 正常工作,避免禁用硬件加速。
  3. 高内存占用。原因:过多的 GraphicBuffer 或复杂的 View 树。解决:优化 View 层级,使用轻量级组件(如 ConstraintLayout)。
  4. 兼容性问题。原因:不同设备对 OpenGL ES 或 Vulkan 的支持不同。解决:提供软件渲染作为回退方案,测试多款设备。