渲染引擎对比: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 的像素对齐策略
Skia 默认使用 半像素对齐。什么意思?你画一个 1px 宽的线,坐标在 (10, 10),Skia 会把它渲染到 (10.5, 10.5) 到 (11.5, 11.5) 之间。这样做的好处是线条看起来更平滑,坏处是——如果你不注意,整个界面会「糊」掉。
Offset.zero 或 Rect.fromLTWH 时,确保坐标是整数,然后调用 canvas.translate(0.5, 0.5) 做全局偏移。
Direct2D 的像素对齐策略
Direct2D 默认使用 整数像素对齐。它会把坐标直接映射到像素网格上。你画一条线在 (10, 10),它就真的在 (10, 10) 开始。这听起来很完美,对吧?但有个问题:抗锯齿。
Direct2D 的抗锯齿是「事后处理」的。它先画一个硬边缘,再用 alpha 混合去模糊。这导致一个现象:同样的 1px 线,在 Skia 和 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。
总结:移植时的选择策略
说了这么多,到底怎么选?我的建议是:
- 跨平台应用:用 Skia。一致性最好,坑最少。
- Windows 独占应用:用 Direct2D。性能最强,而且和 DirectWrite、DirectComposition 配合完美。
- Apple 生态应用:用 Core Graphics。虽然性能一般,但和系统集成度最高,文本渲染质量最好。
- 混合方案:如果必须同时支持三个平台,可以考虑用 Skia 做主要渲染,然后在 Windows 上用 Direct2D 做硬件加速回退,在 macOS 上用 Core Graphics 做高质量文本渲染。
嗯,像素对齐和渲染差异这块,说复杂也复杂,说简单也简单。你只要记住一点:不要相信任何引擎的「默认行为」。在移植时,先写一个像素对齐测试用例,把三个引擎的渲染结果截图对比,确认差异后再开始正式移植。这样能省下大量调试时间。