文章摘要
Skia 的 drawGlyphs 是字体渲染的核心环节,负责将 Minikin/Harfbuzz 布局后的字形转换为像素并绘制到画布。其主要流程包括:接收字形信息(glyph id、位置、字体),通过 SkCanvas::drawGlyphs 构造 SkGlyphRun,再由 SkDraw 进行栅格化(获取字形轮廓或位图,计算位置,最终绘制)。Skia 利用字形缓存(SkGlyphCache)优化性能,支持 CPU/GPU 渲染、抗锯齿等效果,并通过 SkFont/SkTypeface 与 Minikin 实现数据衔接。整个过程高效整合了文本布局与图形渲染的关键功能。
Skia 的 drawGlyphs
实现是 Skia 字体渲染管线的核心环节之一。它负责将 Minikin/Harfbuzz 布局好的字形(glyph id)、位置、字体等信息,最终栅格化并绘制到画布上。下面详细介绍 Skia drawGlyphs
的实现原理、关键流程、源码入口和与 Minikin 的衔接。
1. Skia 字体渲染基本流程
- 接收字形信息
Minikin/Harfbuzz 负责文本分段、字形选择、字形定位,输出 glyph id、字体、位置等信息。 - Skia 负责绘制
Skia 接收这些信息,通过drawGlyphs
或相关 API,将字形渲染到 SkCanvas 上。
2. drawGlyphs 的典型调用链
2.1 Java 层
Canvas.drawText()
、TextView
等最终会走到 native 层的SkCanvas::drawGlyphs
。
2.2 Native 层
MinikinUtils::doLayout()
调用 Minikin 布局,获得 glyph id、位置、字体。- 通过 JNI/NDK 传递到 Skia。
2.3 Skia 层
SkCanvas::drawGlyphs()
是核心入口。- 其参数包括:
- glyphs(字形 id 数组)
- count(字形数量)
- positions(每个字形的坐标)
- font(SkFont,封装 SkTypeface、字号、hinting、抗锯齿等)
- paint(SkPaint,封装颜色、样式等)
3. Skia drawGlyphs 实现细节
3.1 关键源码入口
3.2 主要流程
-
SkCanvas::drawGlyphs
void SkCanvas::drawGlyphs(int count, const uint16_t glyphs[], const SkPoint positions[], const SkFont& font, const SkPaint& paint) { // 构造 SkGlyphRun SkGlyphRun glyphRun(font, SkSpan<const uint16_t>(glyphs, count), SkSpan<const SkPoint>(positions, count)); // 构造 SkGlyphRunList SkGlyphRunList glyphRunList(glyphRun, ...); // 交给 SkDraw 处理 this->onDrawGlyphRunList(glyphRunList, paint); }
-
SkDraw::drawGlyphRunList
- 遍历每个 glyph,查找字体缓存(SkGlyphCache),获得字形轮廓或位图。
- 计算每个字形的实际绘制位置。
- 根据 paint 设置(抗锯齿、阴影、描边等)进行栅格化。
- 绘制到目标 surface(CPU bitmap 或 GPU 纹理)。
-
字形栅格化与缓存
- Skia 会缓存常用字形的位图(glyph cache),提升性能。
- 支持 subpixel positioning、LCD 渲染、hinting 等。
-
硬件加速
- 如果开启 GPU 加速,Skia 会将字形数据上传到 GPU,使用 OpenGL/Vulkan 渲染。
4. Skia drawGlyphs 与 Minikin 的衔接
- Minikin 负责文本到 glyph 的 mapping 和布局,Skia 只负责“画”。
- Minikin 通过 MinikinFontSkia 提供 SkTypeface 给 Skia。
- Skia 通过 SkFont/SkTypeface 访问字体文件,获得字形轮廓。
5. 伪代码流程
// 1. Minikin 布局
std::vector<uint16_t> glyphs;
std::vector<SkPoint> positions;
SkTypeface* typeface = ...; // MinikinFontSkia
// 2. Skia 绘制
SkFont font(typeface, textSize);
canvas->drawGlyphs(glyphs.size(), glyphs.data(), positions.data(), font, paint);
6. 相关源码片段
SkCanvas::drawGlyphs
void SkCanvas::drawGlyphs(int count, const uint16_t glyphs[], const SkPoint positions[],
const SkFont& font, const SkPaint& paint) {
// 构造 SkGlyphRun
SkGlyphRun glyphRun(font, SkSpan<const uint16_t>(glyphs, count), SkSpan<const SkPoint>(positions, count));
SkGlyphRunList glyphRunList(glyphRun, ...);
this->onDrawGlyphRunList(glyphRunList, paint);
}
SkDraw::drawGlyphRunList
void SkDraw::drawGlyphRunList(const SkGlyphRunList& glyphRunList, const SkPaint& paint) {
for (const SkGlyphRun& run : glyphRunList) {
// 查找字体缓存
// 获取字形轮廓或位图
// 计算位置
// 绘制到 surface
}
}
7. 总结
- Minikin 负责文本到 glyph 的 mapping、字体选择、布局。
- Skia 的
drawGlyphs
负责字形的栅格化和绘制,支持 CPU/GPU 渲染、缓存、抗锯齿等。 - 两者通过 SkTypeface/SkFont/MinikinFontSkia 进行衔接。
8. 参考资料
下面详细介绍 Skia 的字形栅格化(rasterization)、缓存机制(glyph cache)、GPU 渲染细节,并结合源码和实际流程说明。
1. Skia 字形栅格化(Glyph Rasterization)
1.1 基本流程
- 字形栅格化是指将字体的矢量轮廓(outline)转换为像素位图(bitmap)或距离场(SDF),以便在屏幕上显示。
- Skia 支持 TrueType、OpenType 等字体格式,能处理各种 hinting、抗锯齿、subpixel 等效果。
1.2 主要步骤
-
获取字形轮廓
- 通过
SkTypeface
读取字体文件,获得指定 glyph id 的矢量轮廓(如贝塞尔曲线)。 - 相关接口:
SkTypeface::getPath(glyphID, &SkPath)
- 通过
-
栅格化为位图
- Skia 使用自己的 rasterizer(如 SkScan)将矢量轮廓转换为像素。
- 支持灰度、LCD(子像素)、SDF(距离场)等多种格式。
- 可选 hinting、抗锯齿、subpixel positioning。
-
生成最终像素数据
- 结果是一个 bitmap 或 alpha mask,供后续绘制。
1.3 相关源码
2. Skia 字形缓存机制(Glyph Cache)
2.1 作用
- 字体渲染中,字形栅格化是高开销操作。Skia 通过字形缓存(glyph cache)避免重复栅格化,提高性能。
2.2 主要结构
- SkGlyphCache:每种字体、字号、hinting、渲染参数组合对应一个缓存。
- SkGlyph:缓存单个 glyph 的 bitmap、metrics、轮廓等。
2.3 工作流程
- 查找缓存
- 绘制前,Skia 先查找 SkGlyphCache 是否已有目标 glyph 的位图。
- 命中缓存
- 直接取出 bitmap,跳过栅格化。
- 未命中
- 通过 SkScalerContext 进行栅格化,并将结果写入 SkGlyphCache。
- 缓存管理
- Skia 会定期清理不常用的缓存,防止内存膨胀。
2.4 相关源码
3. Skia GPU 渲染细节
3.1 GPU 字体渲染的挑战
- 字形数据复杂,上传 GPU 成本高。
- 需要高效管理字形纹理,支持动态扩展、回收。
3.2 Skia 的 GPU 字体渲染方案
3.2.1 字形纹理图集(Atlas)
- Skia 将常用字形的 bitmap 打包到一个或多个大纹理(atlas)中。
- 每个 glyph 在 atlas 中有自己的位置和 UV 坐标。
3.2.2 渲染流程
- 字形栅格化
- 首次遇到 glyph 时,CPU 端栅格化为 bitmap。
- 上传到 GPU
- 将 bitmap 拷贝到 atlas 纹理的空闲区域。
- 记录 UV 坐标
- 记录 glyph 在 atlas 中的偏移和大小。
- 批量绘制
- 绘制时,使用一个四边形(quad)+ UV 坐标,批量渲染多个 glyph。
- 支持 OpenGL/Vulkan/Metal 等后端。
3.2.3 SDF/Path Rendering
- 对于大字号或特殊效果,Skia 也支持 SDF(Signed Distance Field)或直接用 GPU 路径渲染(如 SkiaPathRender)。
3.2.4 缓存与回收
- Atlas 空间有限,Skia 会根据 LRU 等策略回收不常用的 glyph 区域。
3.3 相关源码
4. 伪代码流程
// 1. 查找/创建 SkGlyphCache
SkGlyphCache* cache = SkStrikeCache::FindOrCreateStrike(font, paint);
// 2. 查找 glyph
SkGlyph* glyph = cache->getGlyph(glyphID);
// 3. 如果没有 bitmap,栅格化
if (!glyph->hasImage()) {
SkScalerContext* scaler = cache->getScalerContext();
scaler->getImage(glyph);
}
// 4. 如果是 GPU 渲染,上传到 atlas
if (useGPU) {
GrAtlas* atlas = getOrCreateAtlas();
atlas->addGlyph(glyph->image());
}
// 5. 绘制
drawGlyphQuad(atlas, glyph->uv(), position);
5. 总结
- 字形栅格化:Skia 支持多种字体格式和渲染模式,矢量转 bitmap。
- 缓存机制:SkGlyphCache/SkStrikeCache 避免重复栅格化,提升性能。
- GPU 渲染:通过 atlas 管理字形纹理,批量渲染,支持 SDF/Path 等高级效果。