Android窗口显示过程分析4之软件绘制

259 阅读5分钟

我们来深入解析 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 的软件绘制完全依赖它。核心组件:

  1. SKBitmap:存储图形数据,相当于 “画纸”,记录每个像素的颜色和位置。

    java

    SkBitmap bitmap = new SkBitmap();
    bitmap.setConfig(SkBitmap.kRGB_565_Config, 800, 480); // 设置格式和尺寸
    bitmap.allocPixels(); // 分配内存空间
    
  2. SKCanvas:封装绘制操作,相当于 “画布”,提供画线、矩形、文本等 API。

    java

    SkCanvas canvas(bitmap); // 关联画纸
    canvas.drawColor(SK_ColorRED); // 填充红色
    
  3. 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

  1. 获取画布(lockCanvas) :从 Surface 获取与缓冲区关联的 Canvas 对象,准备绘制。
  2. 绘制 View 树(mView.draw (canvas)) :遍历 View 树,每个 View 调用onDraw方法,使用 Canvas 绘制自身内容。
  3. 提交缓冲区(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 树开始绘制:

  1. 遍历 View 树:从根 View(DecorView)开始,递归调用每个 View 的draw方法。

  2. 调用 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); // 画红色圆(在新原点)
    }
    
  3. Skia 库渲染:Java 层的绘制指令(如drawRect)通过 JNI 转发到 Native 层的 SkiaCanvas,最终操作 SkBitmap 的像素数据。

六、缓冲区提交:从绘制到显示

绘制完成后,通过surface.unlockCanvasAndPost(canvas)提交缓冲区:

  1. 解除 Canvas 与缓冲区的关联:断开 SkiaCanvas 与 SkBitmap 的连接,防止后续修改。

  2. 提交缓冲区到 BLAST 队列:将包含像素数据的缓冲区提交给系统的缓冲区队列(BLASTBufferQueue),等待 SurfaceFlinger 合成到屏幕。

    cpp

    // Native层关键步骤:解锁缓冲区并提交
    status_t err = surface->unlockAndPost(); 
    // 缓冲区进入队列,后续由系统决定何时显示(如配合Vsync信号)
    

七、关键类关系:从 Java 到 Native 的映射

Java 层类Native 层对应类作用
SurfaceBBQSurface(Surface 子类)管理缓冲区,连接 Java 与 Native
CanvasSkiaCanvas(继承 Canvas)封装 Skia 绘制操作,关联 SkBitmap
PaintPaint(继承 SkPaint)存储绘制样式,转发到 SkPaint
ANativeWindow_BufferGraphicBuffer实际存储像素数据的缓冲区

八、软件绘制的应用场景

  1. 简单 UI 场景:如纯文字、静态图片,CPU 绘制足够高效。
  2. 硬件加速兼容性问题:某些自定义 View 在硬件加速下可能出现显示错误,关闭硬件加速后用软件绘制兜底。
  3. 调试需求:软件绘制流程简单,便于追踪绘制问题(如错位、颜色错误)。

总结:软件绘制的核心流程

  1. 准备画布:通过 Surface 获取与缓冲区关联的 Canvas(基于 Skia 的 SkCanvas)。

  2. 绘制内容:View 树遍历,调用 onDraw 方法,通过 Skia 库操作 SkBitmap 的像素数据。

  3. 提交显示:将绘制好的缓冲区提交到系统队列,等待合成到屏幕。

软件绘制就像 “手工画画”:先准备好画纸(SKBitmap)和画笔(SkPaint),在画布(SKCanvas)上按顺序绘制每个元素,最后将画好的作品交给展示系统(SurfaceFlinger)。虽然没有硬件加速的 “流水线” 高效,但清晰的流程是理解 Android 绘制原理的重要起点。通过掌握软件绘制,能更好地理解 View 的测量、布局与绘制的整体逻辑,为优化复杂 UI 性能打下基础。