渲染引擎对比:Skia vs. Direct2D vs. Core Graphics

做界面移植,说白了就是跟三个渲染引擎打交道:Skia、Direct2D 和 Core Graphics。很多人觉得它们都是画图的,能有多大区别?嗯,我当年也这么想,直到在项目里被像素对齐坑了一把,才老老实实把这三个家伙的脾气摸了个透。

今天咱们就掰开揉碎,看看它们各自怎么干活,像素对齐又有什么门道。

三驾马车:各自的身世与定位

先简单认识一下这三位:

引擎 所属平台 渲染方式 典型应用
Skia 跨平台(Chrome、Android、Flutter) 软件/GPU 混合 浏览器、移动端 UI 框架
Direct2D Windows(Vista+) 硬件加速(GPU) Windows 桌面应用、游戏 UI
Core Graphics macOS / iOS 软件渲染为主 Apple 原生界面、PDF 渲染

你可能会问:为什么 Flutter 选 Skia 而不是 Direct2D?我个人习惯从「跨平台一致性」这个角度去理解。Skia 在 Android、iOS、Windows、macOS 上画出来的结果几乎一样,这对移植来说太重要了。

渲染管线:它们到底怎么画图?

这三个引擎的渲染管线有本质区别。我画了一张图,帮你快速建立整体认知:

三大渲染引擎管线对比 Skia 管线 Canvas API 调用 几何裁剪 / 矩阵变换 光栅化(软件/GPU) 像素输出 Direct2D 管线 ID2D1RenderTarget 调用 GPU 命令构建 硬件光栅化(Direct3D) 像素输出 Core Graphics 管线 CGContext 调用 Quartz 2D 引擎处理 软件光栅化(CPU) 像素输出 关键差异 • Skia:软件/GPU 混合,跨平台一致性最好,但 GPU 加速不如 Direct2D 彻底 • Direct2D:纯硬件加速,Windows 上性能最强,但离开 Windows 就废了 • Core Graphics:软件渲染为主,精度高,适合 PDF/打印,但 GPU 利用率低

像素对齐:移植中最容易翻车的坑

像素对齐,说白了就是「坐标落在像素网格的什么位置」。不同引擎的处理方式完全不同,我在这上面吃过不少亏。

Skia 的像素对齐策略

Skia 默认使用 半像素对齐。什么意思?你画一个 1px 宽的线,坐标在 (10, 10),Skia 会把它渲染到 (10.5, 10.5) 到 (11.5, 11.5) 之间。这样做的好处是线条看起来更平滑,坏处是——如果你不注意,整个界面会「糊」掉。

我的经验:在 Flutter 项目中,我遇到过按钮边缘模糊的问题。后来发现是坐标没做对齐处理。解决方案很简单:用 Offset.zero 或 Rect.fromLTWH 时,确保坐标是整数,然后调用 canvas.translate(0.5, 0.5) 做全局偏移。

Direct2D 的像素对齐策略

Direct2D 默认使用 整数像素对齐。它会把坐标直接映射到像素网格上。你画一条线在 (10, 10),它就真的在 (10, 10) 开始。这听起来很完美,对吧?但有个问题:抗锯齿。

Direct2D 的抗锯齿是「事后处理」的。它先画一个硬边缘,再用 alpha 混合去模糊。这导致一个现象:同样的 1px 线,在 Skia 和 Direct2D 上看起来粗细不一样。

注意:Direct2D 的 DrawLine 和 FillRectangle 在像素对齐上行为不同。画线时,如果坐标是整数,线宽是奇数,线条会「跨」在两个像素上,看起来变粗了。我曾经在 Windows 上移植一个 Skia 应用,所有 1px 边框都变成了 2px,排查了半天才发现是这个问题。

Core Graphics 的像素对齐策略

Core Graphics 走的是 浮点坐标 路线。它不关心像素网格,只关心数学坐标。渲染时,它会用高精度的子像素采样,然后通过抗锯齿「猜」出最终像素颜色。

这样做的好处是:旋转、缩放时质量极高。坏处是:性能开销大,而且在不同设备上可能看到不同的结果。

特性 Skia Direct2D Core Graphics
坐标系统 半像素偏移 整数像素对齐 浮点坐标
抗锯齿方式 内置子像素 事后 alpha 混合 高精度子像素
1px 线效果 清晰但略淡 可能变粗 最平滑
旋转/缩放质量 中等 好(硬件加速) 最好
性能 中等 高 低(CPU 密集)

渲染差异:同一个界面,三个样子

你想想看,同样的一个圆角矩形,在三个引擎上画出来,颜色、边缘、阴影都可能不一样。我整理了几个最常见的差异点:

1. 颜色空间差异

Skia 默认使用 sRGB,Direct2D 使用 scRGB,Core Graphics 使用 Generic RGB。同一个 #FF0000,在三个引擎上显示出来的红色饱和度不同。移植时一定要做颜色空间转换。

2. 渐变渲染差异

线性渐变在 Skia 和 Direct2D 上表现接近,但 Core Graphics 的渐变是「非线性」的——它默认做了伽马校正。你从红到蓝渐变,在 Core Graphics 上中间色会偏暗。

避坑指南:我曾经在 macOS 上开发一个设计工具,用户从 Windows 导入的渐变文件,在 Mac 上显示完全不一样。后来发现是 Core Graphics 的 CGGradient 默认启用了伽马校正,而 Skia 没有。解决方案是在 Skia 端手动做伽马校正,或者用 SkShader::MakeLinearGradient 时传入 SkGradientShader::kInterpolateColorsInPremul 标志。

3. 文本渲染差异

文本渲染是差异最大的地方。Skia 使用 FreeType 或 DirectWrite 后端,Direct2D 使用 DirectWrite,Core Graphics 使用 Core Text。同一个字体、字号、字重,渲染出来的字形宽度、行高、基线位置都可能不同。

我建议你在移植时,不要依赖引擎的默认文本布局,而是用统一的文本布局引擎(比如 HarfBuzz)做字形整形,然后再交给各引擎渲染。

性能调优:让三个引擎跑得更快

性能调优,说白了就是「减少绘制调用,减少状态切换」。但每个引擎的优化侧重点不同:

  • Skia:减少 save/restore 次数,尽量用 SkCanvas::clipRect 代替 clipPath。我习惯把多个绘制操作合并到一个 SkPicture 中,然后一次回放。
  • Direct2D:减少 BeginDraw/EndDraw 之间的状态切换。把相同笔刷、相同变换的绘制操作放在一起。另外,尽量用 ID2D1BitmapBrush 代替 ID2D1SolidColorBrush 的频繁创建。
  • Core Graphics:避免频繁创建 CGContext。尽量复用 CGLayer 做离屏渲染。另外,CGContextDrawImage 的性能比 CGContextDrawPDFPage 好很多。

一个小技巧:在移植时,先在一个引擎上把性能优化到极致,然后再移植到其他引擎。因为性能瓶颈往往在「绘制逻辑」本身,而不是引擎差异。我通常先在 Skia 上做原型,因为它的调试工具最完善,然后再适配到 Direct2D 和 Core Graphics。

总结:移植时的选择策略

说了这么多,到底怎么选?我的建议是:

  1. 跨平台应用:用 Skia。一致性最好,坑最少。
  2. Windows 独占应用:用 Direct2D。性能最强,而且和 DirectWrite、DirectComposition 配合完美。
  3. Apple 生态应用:用 Core Graphics。虽然性能一般,但和系统集成度最高,文本渲染质量最好。
  4. 混合方案:如果必须同时支持三个平台,可以考虑用 Skia 做主要渲染,然后在 Windows 上用 Direct2D 做硬件加速回退,在 macOS 上用 Core Graphics 做高质量文本渲染。

嗯,像素对齐和渲染差异这块,说复杂也复杂,说简单也简单。你只要记住一点:不要相信任何引擎的「默认行为」。在移植时,先写一个像素对齐测试用例,把三个引擎的渲染结果截图对比,确认差异后再开始正式移植。这样能省下大量调试时间。