我们来深入解析 Android 中 View 的软件绘制流程。区别于依赖 GPU 的硬件加速绘制,软件绘制全程由 CPU 借助 Skia 库完成,虽然性能稍逊,但流程更简单清晰,适合作为理解 Android 绘制原理的切入点。
一、软件绘制 vs 硬件加速:核心区别
-
硬件加速:Android 4.0 后默认开启,利用 GPU 并行计算,适合复杂 UI(如动画、3D 效果),但初始化复杂、内存占用大。
-
软件绘制:通过 CPU 调用 Skia 库直接绘制,适合简单 UI 或关闭硬件加速的场景(如在 AndroidManifest 中设置
android:hardwareAccelerated="false")。
核心优势:流程直观,便于理解底层绘制原理(如 Skia 库如何将绘制指令转化为像素数据)。
二、Skia 库:软件绘制的 “画笔”
Skia 是 Google 开源的 2D 图形库,Android 的软件绘制完全依赖它。核心组件:
-
SKBitmap:存储图形数据,相当于 “画纸”,记录每个像素的颜色和位置。
java
SkBitmap bitmap = new SkBitmap(); bitmap.setConfig(SkBitmap.kRGB_565_Config, 800, 480); // 设置格式和尺寸 bitmap.allocPixels(); // 分配内存空间 -
SKCanvas:封装绘制操作,相当于 “画布”,提供画线、矩形、文本等 API。
java
SkCanvas canvas(bitmap); // 关联画纸 canvas.drawColor(SK_ColorRED); // 填充红色 -
SkPaint:设置绘制风格,相当于 “画笔”,控制颜色、线条粗细、抗锯齿等。
java
SkPaint paint; paint.setAntiAlias(true); // 开启抗锯齿 paint.setColor(SkColorSetRGB(255, 0, 0)); // 红色 canvas.drawRect(10, 10, 100, 100, paint); // 画红色矩形
三、绘制流程起点:ViewRootImpl 触发绘制
当 View 树准备好绘制(如布局完成),ViewRootImpl 的draw方法会根据是否启用硬件加速选择路径:
java
if (isHardwareEnabled()) {
// 硬件绘制(复杂,暂不展开)
} else {
drawSoftware(surface, ...); // 软件绘制入口
}
关键方法:drawSoftware
- 获取画布(lockCanvas) :从 Surface 获取与缓冲区关联的 Canvas 对象,准备绘制。
- 绘制 View 树(mView.draw (canvas)) :遍历 View 树,每个 View 调用
onDraw方法,使用 Canvas 绘制自身内容。 - 提交缓冲区(unlockCanvasAndPost) :将绘制好的缓冲区提交给系统,等待显示。
四、Canvas 初始化:从 Java 到 Native 的桥梁
1. Java 层:Surface.lockCanvas
-
调用
Surface.lockCanvas(dirty)获取 Canvas,实际触发 Native 层操作:java
canvas = mSurface.lockCanvas(dirty); // mSurface是Surface对象 -
lockCanvas会申请一块图形缓冲区(ANativeWindow_Buffer),并将其与 Canvas 关联。
2. Native 层:构建 SkiaCanvas
-
通过 JNI 调用
nativeLockCanvas,在 Native 层创建graphics::Canvas对象,内部封装 Skia 的SkCanvas:cpp
// 将ANativeWindow_Buffer转换为SkBitmap,供SkCanvas使用 SkBitmap bitmap; convert(buffer, dataspace, &bitmap); // buffer是Native缓冲区 SkiaCanvas skiaCanvas(bitmap); // SkiaCanvas持有SkCanvas实例 -
最终,Java 层的 Canvas 对象(
mCanvas)背后是 Native 层的 SkiaCanvas,所有绘制操作都会转发到 Skia 库。
五、View 树绘制:从 onDraw 到 Skia 渲染
当mView.draw(canvas)被调用,View 树开始绘制:
-
遍历 View 树:从根 View(DecorView)开始,递归调用每个 View 的
draw方法。 -
调用 onDraw:自定义 View 重写
onDraw,使用 Canvas API 绘制内容:java
@Override protected void onDraw(Canvas canvas) { Paint paint = new Paint(); paint.setColor(Color.BLUE); canvas.drawRect(100, 100, 200, 200, paint); // 画蓝色矩形 canvas.translate(300, 0); // 移动画布原点 paint.setColor(Color.RED); canvas.drawCircle(100, 100, 50, paint); // 画红色圆(在新原点) } -
Skia 库渲染:Java 层的绘制指令(如
drawRect)通过 JNI 转发到 Native 层的 SkiaCanvas,最终操作 SkBitmap 的像素数据。
六、缓冲区提交:从绘制到显示
绘制完成后,通过surface.unlockCanvasAndPost(canvas)提交缓冲区:
-
解除 Canvas 与缓冲区的关联:断开 SkiaCanvas 与 SkBitmap 的连接,防止后续修改。
-
提交缓冲区到 BLAST 队列:将包含像素数据的缓冲区提交给系统的缓冲区队列(BLASTBufferQueue),等待 SurfaceFlinger 合成到屏幕。
cpp
// Native层关键步骤:解锁缓冲区并提交 status_t err = surface->unlockAndPost(); // 缓冲区进入队列,后续由系统决定何时显示(如配合Vsync信号)
七、关键类关系:从 Java 到 Native 的映射
| Java 层类 | Native 层对应类 | 作用 |
|---|---|---|
| Surface | BBQSurface(Surface 子类) | 管理缓冲区,连接 Java 与 Native |
| Canvas | SkiaCanvas(继承 Canvas) | 封装 Skia 绘制操作,关联 SkBitmap |
| Paint | Paint(继承 SkPaint) | 存储绘制样式,转发到 SkPaint |
| ANativeWindow_Buffer | GraphicBuffer | 实际存储像素数据的缓冲区 |
八、软件绘制的应用场景
- 简单 UI 场景:如纯文字、静态图片,CPU 绘制足够高效。
- 硬件加速兼容性问题:某些自定义 View 在硬件加速下可能出现显示错误,关闭硬件加速后用软件绘制兜底。
- 调试需求:软件绘制流程简单,便于追踪绘制问题(如错位、颜色错误)。
总结:软件绘制的核心流程
-
准备画布:通过 Surface 获取与缓冲区关联的 Canvas(基于 Skia 的 SkCanvas)。
-
绘制内容:View 树遍历,调用 onDraw 方法,通过 Skia 库操作 SkBitmap 的像素数据。
-
提交显示:将绘制好的缓冲区提交到系统队列,等待合成到屏幕。
软件绘制就像 “手工画画”:先准备好画纸(SKBitmap)和画笔(SkPaint),在画布(SKCanvas)上按顺序绘制每个元素,最后将画好的作品交给展示系统(SurfaceFlinger)。虽然没有硬件加速的 “流水线” 高效,但清晰的流程是理解 Android 绘制原理的重要起点。通过掌握软件绘制,能更好地理解 View 的测量、布局与绘制的整体逻辑,为优化复杂 UI 性能打下基础。