字体 TTF 到 GPU 图像的完整渲染流程

从字体文件中的矢量轮廓,到屏幕上像素的完整链路。覆盖 Android Framework → HWUI → Skia → GPU 全栈。


全链路总览

┌─────────────────────────────────────────────────────────────────────────────────────┐
│  TTF/OTF 字体文件                                                                    │
│  ┌─────────────────────────────────────────────────────────────────────────────────┐│
│  │  存储内容: 矢量轮廓(贝塞尔曲线控制点) + 排版表(GSUB/GPOS/kern/cmap)             ││
│  │  比喻: 每个字符是一张 SVG (可缩放矢量图), 不是像素位图                           ││
│  └─────────────────────────────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────┬──────────────────────────────────────────┘
                                           │
          ┌────────────────────────────────┼────────────────────────────────┐
          │                                ▼                                │
          │  Phase 1: 文本排版 (Shaping)                                    │
          │  Unicode 字符 → Glyph IDs + 位置                               │
          │  [HarfBuzz + Minikin + ICU BiDi]                                │
          │  输出: (glyphID, x, y) 数组                                    │
          ├─────────────────────────────────────────────────────────────────┤
          │                                ▼                                │
          │  Phase 2: 录制 (Recording)                                      │
          │  Glyph 数组 → SkTextBlob → DisplayList                        │
          │  [UI Thread, HWUI RecordingCanvas]                              │
          │  输出: DisplayListData 中的 DrawTextBlob Op                     │
          ├─────────────────────────────────────────────────────────────────┤
          │                                ▼                                │
          │  Phase 3: 回放 + Op 创建 (Replay)                               │
          │  DisplayList → SubRun 分类 → AtlasTextOp                       │
          │  [RenderThread, Skia Ganesh Device]                             │
          │  输出: OpsTask 中排队的 AtlasTextOp                             │
          ├─────────────────────────────────────────────────────────────────┤
          │                                ▼                                │
          │  Phase 4: 字形光栅化 (Glyph Rasterization)                      │
          │  矢量轮廓 → 像素位图 → GPU Atlas 纹理                          │
          │  [RenderThread, FreeType + StrikeCache + GrDrawOpAtlas]         │
          │  输出: Atlas 纹理中的字形位图区域                               │
          ├─────────────────────────────────────────────────────────────────┤
          │                                ▼                                │
          │  Phase 5: 顶点填充 + GPU 绘制 (Vertex Fill + Draw)              │
          │  Atlas UV + 位置 → 四边形顶点 → GPU Draw Call                   │
          │  [RenderThread → GPU, Vulkan/GL]                                │
          │  输出: 屏幕上的像素                                             │
          └─────────────────────────────────────────────────────────────────┘

Phase 1: 文本排版 (Shaping)

目标

将 Unicode 字符串转换为字形 ID + 精确位置数组。这一步解决”用哪个字形、放在哪里”的问题。

调用链

TextView.onDraw(Canvas)                              [TextView.java:9314]
  → mLayout.draw(canvas)                             [Layout.java:793]
    → canvas.drawText(buf, start, end, x, y, paint)  [Layout.java:959]
      → BaseCanvas.nDrawText() [JNI]                 [Canvas.java:2386]
        → Canvas::drawText()                         [hwui/Canvas.cpp:89]
          → MinikinUtils::doLayout()                 [MinikinUtils.cpp:76]

Minikin + HarfBuzz 内部流程

MinikinUtils::doLayout(paint, bidiFlags, typeface, text, ...)
  │
  ├─ 1. ICU BiDi 分析
  │     → 确定文本方向 (LTR/RTL/混合)
  │     → 按方向分割成 BiDi Run
  │
  ├─ 2. Script Run 分割
  │     → 按 Unicode Script 分段 (Latin / CJK / Arabic / ...)
  │     → 每段独立进入 HarfBuzz
  │
  ├─ 3. 字体回退 (Font Fallback)
  │     → 从字体链中选择能覆盖该字符的字体
  │     → 例: "Hello你好" → Roboto(Hello) + NotoSansCJK(你好)
  │
  ├─ 4. HarfBuzz Shaping
  │     → 输入: Unicode 码点 + 字体
  │     → 查 cmap 表: 码点 → glyph ID
  │     → 查 GSUB 表: 字形替换 (连字、变体)
  │     → 查 GPOS 表: 字形定位 (字距调整、标记附着)
  │     → 输出: (glyphID, xOffset, yOffset, xAdvance) per glyph
  │
  └─ 5. 输出 minikin::Layout
        → nGlyphs(): 字形总数
        → getGlyphId(i): 第i个字形 ID
        → getX(i), getY(i): 相对基线位置
        → getFont(i): 使用的字体

关键数据转换

输入: "Hello你好"  (Unicode 码点序列: U+0048 U+0065 U+006C U+006C U+006F U+4F60 U+597D)
                            ↓
输出: [(glyph:43, x:0, y:0, font:Roboto),
       (glyph:72, x:12.5, y:0, font:Roboto),
       (glyph:79, x:22.3, y:0, font:Roboto),
       (glyph:79, x:28.1, y:0, font:Roboto),
       (glyph:82, x:33.9, y:0, font:Roboto),
       (glyph:1247, x:44.0, y:0, font:NotoSansCJK),
       (glyph:2891, x:68.0, y:0, font:NotoSansCJK)]

Phase 2: 录制 (Recording)

目标

将排版结果封装为 SkTextBlob,记入 DisplayList,等待 RenderThread 回放。

调用链

Canvas::drawText() [排版完成后]
  → DrawTextFunctor::operator()(start, end)          [DrawTextFunctor.h:119]
    → canvas->drawGlyphs(glyphFunc, count, paint)    [SkiaCanvas.cpp:850]
      → SkTextBlobBuilder::allocRunPos(font, count)   构建 TextBlob
      → glyphFunc(buffer.glyphs, buffer.pos)          填充 glyph IDs + 位置
      → builder.make() → sk_sp<SkTextBlob>            冻结为不可变对象
      → mCanvas->drawTextBlob(blob, 0, 0, paint)
        → DisplayListData::drawTextBlob()            [RecordingCanvas.cpp:1786]
          → push<DrawTextBlob>(blob, x, y, paint)     写入 DisplayList

SkTextBlob 结构

SkTextBlob (不可变对象, 可跨帧复用)
├── Run 0: {SkFont(Roboto, 14sp), glyphs:[43,72,79,79,82], positions:[(0,0),(12.5,0),...]}
└── Run 1: {SkFont(NotoSansCJK, 14sp), glyphs:[1247,2891], positions:[(44,0),(68,0)]}

UI Thread 结束点

此时 UI Thread 的工作完成。文本以 DrawTextBlob Op 的形式冻结在 DisplayList 中,等待 RenderThread 处理。


Phase 3: 回放 + Op 创建 (Replay)

目标

RenderThread 回放 DisplayList,将 TextBlob 转换为 GPU Op (AtlasTextOp) 加入渲染队列。

调用链

DrawFrameTask::run()                                     [DrawFrameTask.cpp:123]
  → syncFrameState() → prepareTree()                     同步 DL 到 RenderThread
  → CanvasContext::draw()                                [CanvasContext.cpp:1137]
    → SkiaPipeline::renderFrame()                        [SkiaPipeline.cpp:379]
      → renderFrameImpl()                                [SkiaPipeline.cpp:464]
        → RenderNodeDrawable::drawContent()              回放 display list
          → DrawTextBlob::draw(SkCanvas* c)              回放 TextBlob Op
            → c->drawTextBlob(blob, x, y, paint)
              → SkCanvas::onDrawTextBlob()               [SkCanvas.cpp:2886]
                → blobToGlyphRunList()                   TextBlob → GlyphRunList
                → topDevice()->drawGlyphRunList()        派发到 GPU Device

Skia GPU Device 处理

ganesh::Device::drawGlyphRunList()                       [Device.cpp:1771]
  → SurfaceDrawContext::drawGlyphRunList()               [SurfaceDrawContext.cpp:375]
    → TextBlobRedrawCoordinator::drawGlyphRunList()
      │
      ├─ 缓存查找: findOrCreateBlob(positionMatrix, glyphRunList)
      │   → Key = (blob uniqueID, positionMatrix 2x2部分, paint属性)
      │   → 命中: 复用已有 SubRun (快速路径, ~5us)
      │   → 未命中: TextBlob::Make() → SubRunContainer::MakeInAlloc()
      │
      ├─ SubRun 类型选择 (SubRunControl::isSDFT):
      │   ├─ DirectMaskSubRun:   小字号, 位图方式, 精确像素对齐
      │   ├─ SDFTSubRun:         中等字号, SDF 方式, 容忍 scale 变化
      │   ├─ TransformMaskSubRun: 大字号, 固定大小光栅化 + GPU 变换
      │   └─ PathSubRun:          超大字号, 矢量路径渲染
      │
      └─ atlasDelegate → AtlasTextOp::Make()             创建 GPU Op
           → SurfaceDrawContext::addDrawOp(op)           加入 OpsTask

SubRun 类型决策

字号 (approximateDeviceTextSize) :
  │
  ├─ < 32px (且无 forceSDFT): DirectMask
  │     → 以实际 CTM scale 光栅化, 像素完美
  │     → 缺点: scale 变化 = 缓存失效
  │
  ├─ 32px ~ 162px (或 forceSDFT=true): SDFT (Signed Distance Field Text)
  │     → 以固定桶字号(32/72/162px)光栅化
  │     → GPU shader 插值缩放
  │     → 优点: 容忍 scale 变化, 缓存稳定
  │
  ├─ > 162px: TransformMask
  │     → 以固定大小光栅化, GPU 做缩放变换
  │
  └─ > 阈值: PathSubRun
        → 直接发送矢量路径到 GPU

Phase 4: 字形光栅化 (Glyph Rasterization)

目标

将 TTF 中的矢量轮廓转换为像素位图,上传到 GPU Atlas 纹理。这是性能瓶颈所在。

触发时机

在 GPU flush 阶段,OpsTask::execute() 调用各 Op 的 prepare:

OpsTask::execute()
  → AtlasTextOp::onPrepareDraws(target)                 [AtlasTextOp.cpp:481]
    → 遍历每个 geometry (每段文字)
      → GlyphData::regenerateAtlas()                     核心光栅化入口

光栅化详细流程

AtlasTextOp::onPrepareDraws(GrMeshDrawTarget* target)
  │
  ├─ 1. 获取 Atlas 管理器
  │     GrAtlasManager* atlasManager = target->atlasManager()
  │     const GrSurfaceProxyView* views = atlasManager->getViews(maskFormat)
  │
  ├─ 2. 遍历每个字形
  │     for (auto& geo : geometries) {
  │       GlyphData::regenerateAtlas(subRunCursor, end, atlasManager, ...)
  │       │
  │       ├─ 2a. 在 Atlas 中查找该字形
  │       │     GrAtlasLocator locator = glyph.atlasLocator()
  │       │     if (atlasManager->hasGlyph(locator)) → 命中, 跳过光栅化
  │       │
  │       ├─ 2b. Atlas 未命中 → 触发光栅化
  │       │     SkStrike* strike = strikeCache->findOrCreateStrike(descriptor)
  │       │     │
  │       │     │  ┌─── Strike 查找/创建 ───────────────────────────────┐
  │       │     │  │ Key = SkDescriptor (由 SkScalerContextRec 序列化)    │
  │       │     │  │ 包含: typeface + textSize + fPost2x2 + flags + ...  │
  │       │     │  │                                                      │
  │       │     │  │ sk_relax(scale) → 量化到 1/1024 精度 → 进入 key      │
  │       │     │  │ fontVariationSettings → 进入 typeface fingerprint    │
  │       │     │  └──────────────────────────────────────────────────────┘
  │       │     │
  │       │     SkGlyph* glyph = strike->getGlyph(packedGlyphID)
  │       │     if (glyph->image() == nullptr) {
  │       │       // ★★★ 调用 FreeType 光栅化 ★★★
  │       │       strike->generateImage(glyph)
  │       │     }
  │       │
  │       └─ 2c. 上传到 Atlas 纹理
  │             atlasManager->addGlyphToAtlas(glyph, ...)
  │               → GrDrawOpAtlas::addToAtlas(width, height, imageData, locator)
  │                 → Plot::addSubImage() → 写入 CPU staging buffer
  │                 → 标记 Plot dirty → 后续 flush 时上传到 GPU
  │     }
  │
  └─ 3. 填充顶点数据 (Phase 5 的输入)

FreeType 光栅化内部

SkScalerContext_FreeType::generateImage(glyph)
  │
  ├─ 1. 加载字形轮廓
  │     FT_Load_Glyph(face, glyphID, loadFlags)
  │     → 从 TTF 文件读取贝塞尔曲线控制点
  │     → 应用 hinting (网格适配)
  │     → 应用 变换矩阵 (fPost2x2)
  │
  ├─ 2. 光栅化为位图
  │     FT_Render_Glyph(glyph, renderMode)
  │     │
  │     │  renderMode 选择:
  │     │  ├─ FT_RENDER_MODE_NORMAL → A8 灰度 (最常用)
  │     │  ├─ FT_RENDER_MODE_LCD    → LCD 子像素 (水平 3x 过采样)
  │     │  ├─ FT_RENDER_MODE_MONO   → 1-bit 黑白
  │     │  └─ FT_RENDER_MODE_SDF    → SDF 距离场
  │     │
  │     └─ FreeType 内部:
  │          → 将贝塞尔曲线分解为边 (decompose outline)
  │          → 扫描线填充 (scanline rasterizer)
  │          → 应用 Gamma 校正
  │          → 输出: width × height 的像素数组
  │
  └─ 3. 后处理
        → Skia 应用 MaskFilter (如模糊)
        → Skia 应用 PreBlend (Gamma 预混合)
        → 写入 glyph->fImage buffer

光栅化耗时模型

单个字形光栅化耗时:
  ├─ Latin 字母 (简单轮廓, ~20个控制点):  ~5-15 us
  ├─ CJK 汉字 (复杂轮廓, ~100-300个控制点): ~50-150 us
  ├─ Emoji (彩色位图, 需解码 PNG/CBDT):     ~100-300 us
  └─ 可变字体 (需实时插值轮廓):              ~80-200 us

批量光栅化示例:
  "充电中" (3个CJK) → 3 × 150us ≈ 450us
  通知正文 (50个混合字符) → ~600us
  锁屏时钟 "12:34" (5个数字) → 5 × 30us ≈ 150us

Phase 5: Atlas 纹理管理 + 顶点填充 + GPU 绘制

5.1 Glyph Atlas 纹理结构

GrDrawOpAtlas (字形纹理图集)
│
├── Page 0: GPU Texture (2048 × 1024, A8 格式)
│   ├── Plot[0] (512×256) ─── 内部用 RectanizerSkyline 打包字形
│   │   ├── 'A' (12×14 px) @ (0, 0)
│   │   ├── 'B' (11×14 px) @ (12, 0)
│   │   ├── '你' (24×24 px) @ (23, 0)
│   │   └── ... (按需填充)
│   ├── Plot[1] (512×256)
│   │   └── ...
│   └── Plot[N]
│
├── Page 1: (按需激活)
├── Page 2: (按需激活)
└── Page 3: (最多 4 页)

特点:
- 最多 4 页 × 2048×1024 = 8MB (A8格式)
- 每页分为多个 Plot, Plot 是驱逐的最小单位
- 使用 LRU Token 追踪 Plot 使用时间
- Atlas 满时: 驱逐最久未用的 Plot → 回收空间

5.2 字形上传到 Atlas

GrDrawOpAtlas::addToAtlas(width, height, imageData, &locator)
  │
  ├─ 尝试 1: 在已有 Page 的 Plot 中找空间
  │     for (page : activePages) {
  │       if (page.uploadToPage(width, height, image, &locator)) → 成功
  │     }
  │
  ├─ 尝试 2: 激活新 Page (如果 < 4 页)
  │     activateNewPage(resourceProvider) → 分配 GPU 纹理内存
  │
  ├─ 尝试 3: 驱逐 LRU Plot (如果已达 4 页)
  │     找到 lastUseToken 最小的 Plot
  │     processEvictionAndResetRects(plot) → 清空该 Plot
  │     重新上传到回收的 Plot 空间
  │
  └─ 失败: "Atlas full" → 触发 flush 并重试

上传方式:
  → CPU staging buffer 中拼装
  → flush 时批量上传到 GPU (Texture upload slice in trace)
  → Vulkan: vkCmdCopyBufferToImage
  → OpenGL: glTexSubImage2D

5.3 顶点数据填充

每个字形 → 一个四边形 (4 顶点 + 6 索引)

Mask2DVertex 结构 (最常用, 16 bytes/顶点):
┌──────────────────────────────────────────┐
│  devicePos (float x, y)    │ 8 bytes     │  屏幕上的位置
│  color (uint32_t RGBA)     │ 4 bytes     │  文字颜色
│  atlasPos (uint16_t u, v)  │ 4 bytes     │  Atlas 纹理中的 UV 坐标
└──────────────────────────────────────────┘

四个顶点覆盖字形的矩形区域:
  LT ────── RT        UV 从 Atlas 中采样对应字形位图
  │  glyph  │
  LB ────── RB

填充过程:
  VertexFiller::fillVertexData(vertices, offset, count, ...)
    → 遍历每个字形
    → 计算设备空间四角坐标 (应用 CreationMatrix → DrawMatrix 变换)
    → 从 GrAtlasLocator 获取 UV 坐标 (包含 Page 索引编码)
    → 写入顶点 buffer

5.4 GPU Draw Call 提交

AtlasTextOp::onExecute(GrOpFlushState* flushState)       [AtlasTextOp.cpp:654]
  │
  ├─ 1. 创建 Pipeline
  │     GrPipeline = {BlendMode, RenderTarget, Stencil, ...}
  │
  ├─ 2. 绑定 GeometryProcessor
  │     ├─ GrBitmapTextGeoProc:       位图文本 (DirectMask)
  │     │   → Vertex Shader: passthrough position + UV
  │     │   → Fragment Shader: texture2D(atlas, uv) * color
  │     │
  │     └─ GrDistanceFieldA8TextGeoProc: SDF 文本
  │         → Fragment Shader:
  │            float distance = texture2D(atlas, uv).a
  │            float alpha = smoothstep(0.5 - edge, 0.5 + edge, distance)
  │            fragColor = vec4(color.rgb, color.a * alpha)
  │
  ├─ 3. 绑定 Atlas 纹理
  │     flushState->bindTextures(gp, atlas[pageIndex], pipeline)
  │     (如果字形跨多页, 需要多次 draw call)
  │
  └─ 4. 提交 Indexed Draw
        flushState->drawIndexed(indexCount, baseVertex, ...)
        → Vulkan: vkCmdDrawIndexed(indexCount, 1, firstIndex, vertexOffset, 0)
        → 6 indices per glyph × N glyphs = 一次批量绘制

完整数据流转图

TTF 文件中的 "你" (U+4F60)
    │
    │  [Phase 1: 排版]
    │  HarfBuzz 查 cmap: U+4F60 → glyphID=1247
    │  HarfBuzz 查 GPOS: advance=24, xOffset=0
    ▼
(glyphID=1247, x=44.0, y=0, font=NotoSansCJK)
    │
    │  [Phase 2: 录制]
    │  写入 SkTextBlob Run buffer
    │  TextBlob → DrawTextBlob Op → DisplayList
    ▼
DisplayList[Op: DrawTextBlob(blob, 0, 0, paint)]
    │
    │  [Phase 3: 回放]
    │  RenderThread replay → drawGlyphRunList
    │  SubRunControl 判定: DirectMask (字号 < 32px)
    │  TextBlobRedrawCoordinator: cache miss → 创建新 SubRun
    │  AtlasTextOp::Make() → 加入 OpsTask
    ▼
OpsTask[AtlasTextOp(subRun=[glyphID=1247,...], matrix, paint)]
    │
    │  [Phase 4: 光栅化]
    │  StrikeCache 查找: Descriptor(NotoSansCJK, 14sp, matrix=[1,0,0,1]) → miss
    │  创建 SkScalerContext_FreeType
    │  FT_Load_Glyph(face, 1247, flags) → 加载贝塞尔轮廓
    │  FT_Render_Glyph → 扫描线光栅化 → 24×24 A8 位图
    │  atlasManager->addGlyphToAtlas → 写入 Page0/Plot2 @ (100, 50)
    ▼
Atlas 纹理 Page 0 中 (100, 50) 位置有 24×24 的 "你" 灰度位图
    │
    │  [Phase 5: 顶点 + 绘制]
    │  填充 4 个 Mask2DVertex:
    │    LT: pos=(44, -24), uv=(100, 50)
    │    LB: pos=(44, 0),   uv=(100, 74)
    │    RT: pos=(68, -24), uv=(124, 50)
    │    RB: pos=(68, 0),   uv=(124, 74)
    │  
    │  GPU Draw: vkCmdDrawIndexed
    │    → Vertex Shader: 变换到 NDC 坐标
    │    → Fragment Shader: sample(atlas, uv) * textColor
    ▼
屏幕上 (44, baseline-24) ~ (68, baseline) 区域显示 "你" 的像素

缓存层级与失效条件

┌─────────────────────────────────────────────────────────────────────┐
│  Level 1: Java MeasuredText / StaticLayout                           │
│  缓存内容: 排版结果 (glyph IDs + positions)                         │
│  失效条件: setText() / setTextSize() / 字体变化                     │
│  命中时:   跳过 HarfBuzz shaping                                    │
├─────────────────────────────────────────────────────────────────────┤
│  Level 2: DisplayList (SkTextBlob)                                   │
│  缓存内容: 冻结的 TextBlob Op                                       │
│  失效条件: View.invalidate() → mRecreateDisplayList=true            │
│  命中时:   跳过整个 Phase 2 (不重录 DL)                             │
├─────────────────────────────────────────────────────────────────────┤
│  Level 3: TextBlobRedrawCoordinator (Slug)                           │
│  缓存内容: SubRun (已分类的字形组)                                   │
│  失效条件: positionMatrix 2x2 部分变化 (精确浮点比较!)               │
│  命中时:   跳过 SubRun 重建                                          │
├─────────────────────────────────────────────────────────────────────┤
│  Level 4: StrikeCache (字形缓存)                                     │
│  缓存内容: 已光栅化的字形位图                                        │
│  失效条件: Strike Descriptor 变化 (textSize/fPost2x2/typeface/...)  │
│            → sk_relax(scale) 跳变 (1/1024 精度)                     │
│            → fontVariationSettings 变化                              │
│  命中时:   跳过 FreeType 光栅化 (最大节省)                           │
├─────────────────────────────────────────────────────────────────────┤
│  Level 5: GrDrawOpAtlas (GPU 纹理)                                   │
│  缓存内容: 字形位图在 GPU 纹理中的位置                               │
│  失效条件: Atlas generation 变化 (Plot 被驱逐)                       │
│  命中时:   只需填充顶点, 无 CPU→GPU 上传                             │
└─────────────────────────────────────────────────────────────────────┘

性能差距:
  全部命中: AtlasTextOp ~3-5us (仅顶点填充 + draw call)
  Level 4 miss: AtlasTextOp ~100-600us (FreeType 光栅化 + Atlas 上传)
  Level 3 miss: 额外 ~20-50us (SubRun 重建)
  Level 2 miss: 额外 ~50-200us (DL 重录 + 排版)

性能关键路径总结

最佳情况 (所有缓存命中):
  drawTextBlob replay → Slug cache hit → Atlas hit → 填充顶点 → draw
  耗时: ~3-5 us

最差情况 (首次渲染 / 动画中 scale 每帧变化):
  invalidate → DL 重建 → drawText → Minikin shaping → TextBlob 构建
  → Slug miss → SubRun 重建 → Strike miss → FreeType 逐字形光栅化
  → Atlas 上传 → 顶点填充 → draw
  耗时: 600-3000 us (取决于字形数量和复杂度)

缓存的跨 TextView 复用能力

各层复用范围

层级是否跨 TextView缓存归属原因
Level 1: MeasuredTextPer-TextView每个 TextView 持有自己的 mLayout,排版结果绑定到具体文本内容
Level 2: DisplayListPer-View (RenderNode)每个 View 有独立 RenderNode + DisplayList,TextBlob 录制在各自 DL 中
Level 3: Slug/SubRunPer-TextBlobKey 含 TextBlob.uniqueID,不同 TextView 产生不同 blob,ID 不同
Level 4: StrikeCache全局 (进程级)Key = (typeface + textSize + fPost2x2 + flags),与哪个 TextView 无关
Level 5: GrDrawOpAtlas全局 (进程级)Atlas 是共享 GPU 纹理,按 (Strike + glyphID) 索引,任何 View 可读取

跨 View 复用的条件

Level 4 (StrikeCache) 和 Level 5 (Atlas) 是全局共享的。只要满足以下条件,不同 TextView 的相同字形只需光栅化一次

  1. 同一 Typeface (同一字体文件)
  2. 同一 TextSize (像素级相同)
  3. 同一 fPost2x2 矩阵 (sk_relax(scaleX/Y) 量化后相同)
  4. 同一渲染 flags (hinting, subpixel, edging 等)

实际示例

TextView A: "你好世界" (NotoSansCJK, 14sp, matrix=[1,0,0,1])
  → Strike key = (NotoSansCJK, 14sp, fPost2x2=[1,0,0,1])
  → 光栅化 "你" "好" "世" "界" → 写入 Atlas

TextView B: "你好朋友" (NotoSansCJK, 14sp, matrix=[1,0,0,1])  ← 同字体同字号同 matrix
  → Strike key 相同 → Strike 命中!
  → "你" "好" → Atlas 命中 (直接复用 A 光栅化的结果, 0 开销)
  → "朋" "友" → Atlas miss → 只需光栅化这 2 个新字形

典型场景分析

场景 1: 通知列表 (多个 TextView 显示不同通知内容)
  → 字体/字号/matrix 相同 → Strike 共享
  → 第一条通知光栅化了常用汉字后, 后续通知的相同字形直接 Atlas hit
  → 实际耗时: 第1条 ~600us, 第2条 ~200us, 第3条 ~50us (递减)

场景 2: 同一帧中锁屏时钟 "12:34" + 状态栏时钟 "12:34"
  → 如果字体/字号不同 → 不同 Strike → 各自独立光栅化
  → 如果字体/字号相同 → 共享 Strike → 后者 0 光栅化开销

场景 3: 父容器有 scale=0.97 → 子 TextView 们共享同一个 CTM
  → 所有子 TextView 的 fPost2x2 相同 → 落入同一个 Strike → 字形互相复用
  → 但与 scale=1.0 时的 Strike 不同 → 那一套缓存用不上

Trace 中递减趋势的解释

100帧 AtlasTextOp 数据:
  Frame 1: 1056us  ← 首帧, 大量字形首次光栅化
  Frame 5: 800us   ← 部分字形已被其他 TextView 光栅化过, 复用
  Frame 20: 600us  ← 常用字形大多已缓存
  Frame 50: 300us  ← 只剩少量新字形
  Frame 80: 50us   ← 几乎全部 Atlas hit

原因: 同一帧或相邻帧中, 多个 TextView 共享 StrikeCache + Atlas
     → 先渲染的 View 为后渲染的 View "预热" 了缓存

一句话总结:Level 1-3 是 per-View 的私有缓存,Level 4-5 是进程级全局共享的。只要字体属性一致,不同 TextView 的相同字形只需光栅化一次,后续全部走 Atlas hit 快速路径。


源码文件索引

阶段文件关键函数/行号
排版hwui/hwui/Canvas.cppdrawText() L89
排版hwui/hwui/MinikinUtils.cppdoLayout() L76
排版hwui/hwui/DrawTextFunctor.hoperator() L119
录制hwui/SkiaCanvas.cppdrawGlyphs() L850
录制hwui/RecordingCanvas.cppdrawTextBlob() L1786
同步hwui/renderthread/DrawFrameTask.cpprun() L123
回放hwui/pipeline/skia/SkiaPipeline.cpprenderFrameImpl() L464
SubRunskia/src/text/gpu/SubRunContainer.cppMakeInAlloc() L1402
SubRunskia/src/text/gpu/SubRunControl.cppisSDFT() L71
Blob 缓存skia/src/text/gpu/TextBlobRedrawCoordinator.cppfindOrCreateBlob() L58
Op 创建skia/src/gpu/ganesh/ops/AtlasTextOp.cppMake() L161
光栅化skia/src/gpu/ganesh/ops/AtlasTextOp.cpponPrepareDraws() L481
Strikeskia/src/core/SkStrikeCache.cppfindOrCreateStrike()
Strike Keyskia/src/core/SkScalerContext.cppMakeRecAndEffects() L1085
sk_relaxskia/src/core/SkScalerContext.cppsk_relax() L1046
FreeTypeskia/src/ports/SkScalerContext_FreeType.cppgenerateImage()
Atlasskia/src/gpu/ganesh/GrDrawOpAtlas.cppaddToAtlas()
顶点skia/src/text/gpu/VertexFiller.cppfillVertexData()
GPU 绘制skia/src/gpu/ganesh/ops/AtlasTextOp.cpponExecute() L654