关注

别再只会用 drawCircle 了!一文搞懂 Android Canvas 底层机制

前言

在 Android 开发中,Canvas 是自定义 View 和图形绘制的绝对核心。但大多数开发者对 Canvas 的认知仅仅停留在 canvas.drawCircle() 的 API 层面。
本文将从宏观的系统架构调度,到中观的底层双轨渲染机制,再到微观的具体几何绘制实战,结合核心流程图与深度源码解析,带你彻底搞懂 Android Canvas 的底层运作机制与最佳实践。

一、Android 图形渲染架构全景

Canvas 并不是孤立存在的,它嵌套在 Android 庞大的多层渲染架构中。从你调用的 Java API 到屏幕上亮起的物理像素,需要经过以下六个层级:

6. 硬件层

5. 驱动与合成层

4. 渲染引擎层

3. JNI 桥接层

2. 框架层

1. 应用层

自定义 View.onDraw

Canvas.drawXxx API

android.graphics.Canvas

android.graphics.Paint

ViewRootImpl / Choreographer

CanvasJNI.cpp

SkiaCanvas / HardwareCanvas

Skia 引擎 - 软件渲染

OpenGL ES / Vulkan - 硬件渲染

GPU Driver

SurfaceFlinger

Gralloc 缓冲区分配

GPU 硬件

Display 显示控制器

核心认知:你在 Java 层调用的 Canvas,本质上只是一个指令发射器代理对象。真正的绘图工作是在 C++ 层的 Skia 引擎或 GPU 线程中完成的。

二、从 invalidate 到屏幕像素

当我们调用 view.invalidate() 触发重绘时,到底发生了什么?以下是完整的绘制调度流程:

performTraversals 三大步

调用 view.invalidate

标记当前 View 为 DIRTY
记录脏区域 bounds

脏区域向上传递
requestLayout / invalidateChildInParent

到达 ViewRootImpl
合并所有脏区域为脏矩形

调用 scheduleTraversals
postCallback 到 Choreographer

等待下一个 VSYNC 信号到来
通常 16.6ms 间隔

Choreographer 收到 VSYNC
执行 CALLBACK_TRAVERSAL

执行 performTraversals

1. performMeasure
递归执行 onMeasure 计算大小

2. performLayout
递归执行 onLayout 确定位置

3. performDraw
创建 Canvas,递归执行 draw

是否开启
硬件加速?

创建 HardwareCanvas
绑定到 RenderNode

创建 SkiaCanvas
绑定到 Bitmap/Software Render

记录 DrawOp 到 DisplayList
不立即执行渲染

通过 JNI 直接调用 Skia
CPU 实时光栅化像素

RenderThread 异步处理
将 DisplayList 翻译成 OpenGL/Vulkan 指令

GPU 执行渲染指令
写入 Graphic Buffer

CPU 写入内存 Bitmap
提交到 Graphic Buffer

提交 Buffer 给 SurfaceFlinger

Hardware Composer 合成上屏
像素显示

流程详细解析:

  1. 异步绘制机制invalidate() 绝不是同步绘制的!它只是打个标记(设置 PFLAG_DIRTY),然后一路传递到 ViewRootImpl。真正的绘制要等 VSYNC 信号到来。
  2. Choreographer 编舞者:它是 VSYNC 信号的消费者,统一调度 Input、Animation、Traversal 三种类型的回调,保证动画和绘制的帧率与屏幕刷新率同步,避免画面撕裂。
  3. 双轨分流(关键点):在 performDraw 阶段,根据是否开启硬件加速,Canvas 的行为产生本质分歧:
    • 软件渲染:同步阻塞主线程,CPU 直接算像素。
    • 硬件加速:主线程只记录指令,真正的渲染在 RenderThread 异步完成,主线程被释放。

三、Canvas 的真实面貌

在了解了整个绘制链路之后,我们将镜头拉近,看看处于漩涡中心的 Canvas 到底是什么。

3.1 本质:指令发射器与状态栈

很多初学者以为 Canvas 就是一张带像素的纸。错!Canvas 本身不包含任何像素数据

  • 在软件渲染下,像素在 Bitmap 里。
  • 在硬件渲染下,像素在 GPU 的 Graphic Buffer 里。
    Canvas 只是持有这些像素目标的引用,你调用的 drawXxx() 是在向底层引擎发送绘制指令
    同时,Canvas 内部维护了一个非常重要的数据结构:状态栈

Canvas 状态栈

SaveCount = 1 默认
Matrix: 初始矩阵
Clip: 全屏

SaveCount = 2
Matrix: 平移 translate100,100
Clip: 缩小区域

SaveCount = 3
Matrix: 旋转 rotate45
Clip: 更小区域

canvas.save

将当前 Matrix 和 Clip 压入栈顶

修改 translate/rotate/clip

只影响当前栈顶状态

canvas.restore

弹出栈顶恢复到上一层状态

注意: Paint 不属于 Canvas 状态!
Canvas.save 不会保存 Paint 的颜色粗细等属性

save()restore() 的核心作用

  • 保存和恢复的是 Matrix(变换矩阵)Clip(裁剪区域)
  • 绝对不保存 Paint 属性!要想恢复 Paint,需要自己用 paint.reset() 或保存副本。

3.2 硬件加速 vs 软件渲染的 Canvas 差异

这是理解现代 Android 绘制性能的核心。从 Android 4.0 开始默认开启硬件加速,彻底改变了 Canvas 的工作方式。

硬件加速路径

JNI 记录

主线程结束

翻译指令

GPU 执行

Java Canvas API

DisplayList 记录

RenderThread 处理

OpenGL / Vulkan Builder

GPU 缓冲区 FBO/Texture

软件渲染路径

JNI 同步调用

CPU 计算

写像素

Java Canvas API

Skia Canvas C++

Skia 光栅化器

CPU 内存 Bitmap

维度软件渲染硬件加速
Canvas实现类SkiaCanvas (基于 SkCanvas 封装)HardwareCanvas (基于 RenderNode 封装)
执行线程主线程(同步阻塞,耗时长掉帧)主线程记录 + RenderThread 异步渲染
执行方式立即执行:调一次 draw,算一次像素延迟执行:调 draw 只是往 DisplayList 加一条 Op
属性动画每一帧都要完整走 onDraw 重新记录指令直接修改 RenderNode 属性(X/Y/Alpha),无需 onDraw
saveLayer 性能极差:CPU 内存中 new 临时 Bitmap,大量拷贝较好:GPU 中创建 Frame Buffer Object (FBO)
clipPath 抗锯齿支持不支持(会强制降级走软件渲染,性能大坑!)

3.3 View.draw() 的内部流水线与源码印证

performDraw 最终走到你的自定义 View 时,内部按严格顺序执行四件事:

onDrawForeground dispatchDraw onDraw 你重写的方法 drawBackground View.draw onDrawForeground dispatchDraw onDraw 你重写的方法 drawBackground View.draw 受 padding 影响 默认空实现自定义核心区 递归调用 child.draw Android 6.0+ 画在最上层 1. 绘制背景 background Drawable 2. 绘制自身内容 3. 绘制子 View 仅 ViewGroup 4. 绘制前景 滚动条等

我们看一下源码中 Canvas 的创建过程,这能最直观地解释它的本质分歧:
软件渲染下的 Canvas(绑定 Bitmap 持有像素):

public Canvas(Bitmap bitmap) {
    if (!bitmap.isMutable()) throw new IllegalStateException();
    // 在 C++ 层创建 SkCanvas,并让 SkCanvas 持有这个 Bitmap 的像素指针
    mNativeCanvasWrapper = nativeCreate(bitmap.getNativeInstance()); 
    mBitmap = bitmap;
}

硬件加速下的 Canvas(绑定 RenderNode 记录指令):

// 在 ViewRootImpl.draw 硬件加速分支
private boolean draw(boolean fullRedrawNeeded) {
    // 返回 HardwareCanvas,底层没有绑定 CPU Bitmap,而是绑定了 RenderNode
    hwCanvas = mSurface.lockHardwareCanvas(); 
    mView.draw(hwCanvas); // 传递给 View 树
}

四、 核心几何图形绘制 API 详解

在理解了底层原理后,回到最落地的部分:如何使用 Canvas 绘制几何图形。

前置铁律:调用 drawXxx() 时,Canvas 决定“画在哪、怎么变换”,Paint 决定“画成什么样(颜色、粗细、样式)”。以下所有 Paint 均作为成员变量提前初始化,严格禁止在 onDraw 中 new 对象。

4.1 矩形、圆角矩形与线段

// --- 成员变量初始化 ---
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    color = Color.parseColor("#00E5A0")
    style = Paint.Style.FILL              // 填充模式
}
private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    color = Color.parseColor("#FFFFFF") 
    style = Paint.Style.STROKE            // 描边模式
    strokeWidth = 10f                     
}
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    // 1. 纯色填充矩形
    canvas.drawRect(50f, 50f, 300f, 200f, fillPaint)
    // 2. 描边矩形
    canvas.drawRect(50f, 50f, 300f, 200f, strokePaint)
    // 3. 圆角矩形 (rx, ry 为圆角半径)
    val roundRect = RectF(50f, 250f, 300f, 400f)
    canvas.drawRoundRect(roundRect, 20f, 20f, strokePaint)
    // 4. 线段 (strokeWidth 向线条两侧均匀扩展)
    canvas.drawLine(50f, 450f, 300f, 450f, strokePaint)
    // 5. 批量线段 (性能更好,减少 GPU 调用)
    val linePts = floatArrayOf(50f, 500f, 150f, 550f, 150f, 550f, 300f, 500f)
    canvas.drawLines(linePts, strokePaint)
}

4.2 圆形与椭圆

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    // 1. 正圆
    canvas.drawCircle(200f, 200f, 100f, fillPaint)
    // 2. 椭圆 (被限制在外接矩形内)
    canvas.drawOval(50f, 350f, 350f, 500f, strokePaint)
    //  底层原理:Skia 的 drawCircle() 实际上就是算出一个正方形边界,直接调用 drawOval()
}

4.3 弧形与扇形 —— 高频易错点

drawArc 是绘制饼图、进度条的核心,也是面试高频考点。

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val oval = RectF(50f, 50f, 300f, 300f)
    
    // API: drawArc(oval, startAngle, sweepAngle, useCenter, paint)
    //  易错:3点钟方向为 0°,顺时针增加。12点钟方向是 -90°
    
    // 1. 弧线 (月牙边) - useCenter = false
    canvas.drawArc(oval, 0f, 90f, false, strokePaint) 
    
    // 2. 扇形 (饼图切块) - useCenter = true,连接圆心
    canvas.drawArc(oval, 0f, 90f, true, Color.RED)     
    
    // 3. 实战:顶部开始的 75% 进度弧
    val progressOval = RectF(50f, 350f, 300f, 600f)
    canvas.drawArc(progressOval, -90f, 360f * 0.75f, false, fillPaint)
}

4.4 Path 路径 —— 终极武器

当标准图形无法满足需求(如波浪、多边形),Path 记录几何轨迹。

private val pathPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    color = Color.parseColor("#00C2FF"); style = Paint.Style.STROKE
    strokeWidth = 8f; strokeJoin = Paint.Join.ROUND; strokeCap = Paint.Cap.ROUND
}
private val wavePath = Path() //  成员变量复用,禁止在 onDraw new
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    wavePath.reset() 
    
    // 1. 直线多边形
    wavePath.moveTo(50f, 200f)  // 移动画笔不画线
    wavePath.lineTo(150f, 100f) // 画线
    wavePath.lineTo(250f, 200f)
    wavePath.close()             // 自动闭合回起点
    canvas.drawPath(wavePath, pathPaint)
    
    // 2. 二阶贝塞尔曲线 (平滑波浪)
    wavePath.reset()
    wavePath.moveTo(50f, 400f)
    // quadTo(控制点x, 控制点y, 终点x, 终点y)
    wavePath.quadTo(125f, 300f, 200f, 400f)
    wavePath.quadTo(275f, 500f, 350f, 400f)
    canvas.drawPath(wavePath, pathPaint)
}

五、 结合坐标变换绘制仪表盘

为了将“几何图形”、“坐标变换”、“状态栈”融会贯通,我们来看一个完整的实战:带刻度的半圆仪表盘

class DashboardView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
    private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#1A2332"); style = Paint.Style.STROKE
        strokeWidth = 20f; strokeCap = Paint.Cap.ROUND
    }
    private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#00E5A0"); style = Paint.Style.STROKE
        strokeWidth = 20f; strokeCap = Paint.Cap.ROUND
    }
    private val tickPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.WHITE; style = Paint.Style.STROKE
        strokeWidth = 4f; strokeCap = Paint.Cap.ROUND
    }
    private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.WHITE; textAlign = Paint.Align.CENTER; textSize = 36f
    }
    private val arcRect = RectF()
    private var radius = 0f
    private var progress = 0.7f // 70%
    override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {
        super.onSizeChanged(w, h, oldW, oldH)
        radius = Math.min(w, h) / 2f - 60f
        arcRect.set(-radius, -radius, radius, radius)
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // ================= 核心技巧:坐标系平移 =================
        // 将原点移到 View 正中心,后续数学计算摆脱 left/top 偏移烦恼
        canvas.save()
        canvas.translate(width / 2f, height / 2f)
        // 1. 底部灰色圆弧 (半圆,180°到360°)
        canvas.drawArc(arcRect, 180f, 180f, false, bgPaint)
        // 2. 进度圆弧
        canvas.drawArc(arcRect, 180f, 180f * progress, false, progressPaint)
        // 3. 利用旋转 + 状态栈 绘制刻度线 (核心体现 save/restore 价值)
        val totalTicks = 10
        for (i in 0..totalTicks) {
            canvas.save() // 【关键】每次循环 save 隔离旋转影响
            
            // 旋转坐标系:从180°开始,每个刻度旋转 18°
            canvas.rotate(180f + (i * 180f / totalTicks))
            
            // 旋转后 Y轴负方向就是刻度指向,只需画一条固定的垂直线
            canvas.drawLine(0f, -radius + 30f, 0f, -radius - 10f, tickPaint)
            
            canvas.restore() // 【关键】恢复坐标系,进入下一次循环
        }
        // 4. 绘制中心文字 (使用 FontMetrics 精确垂直居中)
        val percentText = "${(progress * 100).toInt()}%"
        val fm = textPaint.fontMetrics
        val textHeight = fm.descent - fm.ascent
        canvas.drawText(percentText, 0f, textHeight / 2f - fm.descent, textPaint)
        canvas.restore() // 恢复最外层 translate
    }
}

代码深度解析

  1. 坐标变换的艺术translate 后,RectF 直接写成负数坐标,数学直觉更清晰。
  2. 状态栈的典型场景:绘制刻度如果不使用 save/restore,需要用 sin/cos 计算坐标。通过 rotate + save/restore,只需永远画一条垂直线,让 Canvas 旋转,复杂度骤降。
  3. 硬件加速下的行为:这些 drawArcdrawLine 不会立刻渲染,转化为 DrawArcOp 等 DisplayList Op,等 onDraw 结束后统一交给 RenderThread 翻译为 GPU 指令,保证 60fps。

六、 总结

理解 Android Canvas,必须跳出 “画板” 的思维定势,建立以下三个核心认知:

  1. 宏观上:Canvas 是连接 Java 世界与 Skia/GPU 世界的桥梁。它的一举一动都被 Choreographer 的 VSYNC 节拍和 ViewRootImpl 的调度流程严格控制。
  2. 微观上:Canvas 是一个状态机 + 指令发射器。它维护着 Matrix 和 Clip 的栈结构,通过 save/restore 实现复杂的空间变换;它本身不生产像素,只是像素加工单的下达者。
  3. 实战上:永远不要在 onDraw 中分配对象;善用坐标变换与状态栈化解复杂绘制逻辑;以**硬件加速(DisplayList/RenderNode)**为默认视角去思考,避免触发意外的软件渲染回退,是高端绘制性能的关键。

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/csdn_silent/article/details/159686748

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--