View.getForceSDFT() 到字形光栅化完整链路分析
概述
View.setForceSDFT(true) 是 MIUI 自定义的渲染优化 API(Feature Tag: SF_BugFixUpstream_3),用于在 scale 动画期间强制 Skia 使用 SDF(Signed Distance Field)文字渲染模式,从而消除亚像素偏移导致的 glyph atlas cache miss。
本文档详细追踪从 Java View.getForceSDFT() 到 Skia 底层字形光栅化决策的完整代码链路。
两条并行路径
setForceSDFT 通过两条独立路径将标记传递到 Skia:
- 路径 A(Paint 路径):per-View draw 级别,通过
TextView.onDraw()→Paint.setForceSDFT()传递 - 路径 B(Canvas/Pipeline 路径):RenderThread 全局级别,通过
ViewRootImpl→CanvasContext→SkCanvas传递
两条路径在 Skia 的 TextBlobRedrawCoordinator::drawForMaskType() 汇合。
路径 A:Paint 路径(per-View draw 级别)
1. View 层设置与继承
文件:frameworks/base/core/java/android/view/View.java
// [View.java:20494-20513] MIUI ADD: SF_BugFixUpstream_3
private boolean mPreForceSDFT = false; // 从父级继承
private boolean mForceSDFT = false; // 自身设置
public void setForceSDFT(boolean forceSDFT) {
mForceSDFT = true;
}
public boolean getForceSDFT() {
return mForceSDFT || mPreForceSDFT; // 自身 OR 父级
}2. 子 View draw 时继承父级标记
文件:frameworks/base/core/java/android/view/View.java
// [View.java:27024-27026] 在 draw(Canvas, ViewGroup, long) 中
// MIUI ADD: SF_BugFixUpstream_3
if (parent != null) {
mPreForceSDFT = parent.getForceSDFT(); // ★ 子 View 从父级继承标记
}
// END SF_BugFixUpstream_3设计意义:对父 View(如 KeyguardBottomAreaView)调用一次 setForceSDFT(true),其所有子 View(包括 KeyguardIndicationTextView)在 draw 时自动继承,无需逐个设置。
3. TextView.onDraw 传递到 Paint
文件:frameworks/base/core/java/android/widget/TextView.java
// [TextView.java:9418] MIUI ADD: SF_BugFixUpstream_3
mTextPaint.setForceSDFT(getForceSDFT());
// END SF_BugFixUpstream_34. Paint JNI 到 SkPaint
文件:frameworks/base/graphics/java/android/graphics/Paint.java
// [Paint.java:2082-2083]
public void setForceSDFT(boolean forceSDFT) {
nSetForceSDFT(mNativePaint, forceSDFT);
}文件:frameworks/base/libs/hwui/jni/Paint.cpp
// [Paint.cpp:1196-1198]
static void setForceSDFT(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle, jboolean forceSDFT) {
auto* paint = reinterpret_cast<Paint*>(paintHandle);
paint->setForceSDFT(forceSDFT);
}文件:skia/include/core/SkPaint.h
// [SkPaint.h:344-345]
bool getForceSDFT() const { return fForceSDFT != 0; }
void setForceSDFT(bool force) { fForceSDFT = force ? 1 : 0; }路径 B:Canvas/Pipeline 路径(RenderThread 全局级别)
1. ViewRootImpl → HardwareRenderer
文件:frameworks/base/core/java/android/view/ViewRootImpl.java
// [ViewRootImpl.java:561-566] MIUI ADD: SF_BugFixUpstream_3
public void setForceSDFT(boolean forceSDFT) {
if (mAttachInfo.mThreadedRenderer != null) {
mAttachInfo.mThreadedRenderer.setForceSDFT(forceSDFT);
}
}2. JNI → RenderProxy → CanvasContext
文件:frameworks/base/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
// [android_graphics_HardwareRenderer.cpp:1305-1307]
static void android_view_ThreadedRenderer_setForceSDFT(JNIEnv* env, jobject clazz,
jlong proxyPtr, jboolean forceSDFT) {
reinterpret_cast<RenderProxy*>(proxyPtr)->setForceSDFT(forceSDFT);
}文件:frameworks/base/libs/hwui/renderthread/RenderProxy.cpp
// [RenderProxy.cpp:804-805]
void RenderProxy::setForceSDFT(bool forceSDFT) {
mRenderThread.queue().post([this, forceSDFT]() { mContext->setForceSDFT(forceSDFT); });
}文件:frameworks/base/libs/hwui/renderthread/CanvasContext.h
// [CanvasContext.h:305]
void setForceSDFT(bool forceSDFT) { mForceSDFT = forceSDFT; }3. 每帧绘制前注入到 Pipeline 和 Canvas
文件:frameworks/base/libs/hwui/renderthread/CanvasContext.cpp
// [CanvasContext.cpp:1349]
mRenderPipeline->setForceSDFT(mForceSDFT);文件:frameworks/base/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
// [SkiaVulkanPipeline.cpp:108] MIUI ADD: SF_BugFixUpstream_3
backBuffer->getCanvas()->setForceSDFT(getForceSDFT());文件:frameworks/base/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
// [SkiaOpenGLPipeline.cpp:177] MIUI ADD: SF_BugFixUpstream_3
surface->getCanvas()->setForceSDFT(getForceSDFT());文件:skia/include/core/SkCanvas.h
// [SkCanvas.h:2204-2205]
void setForceSDFT(bool forceSDFT) { fForceSDFT = forceSDFT; }
bool getForceSDFT() { return fForceSDFT; }路径汇合点:TextBlobRedrawCoordinator
文件:skia/src/text/gpu/TextBlobRedrawCoordinator.cpp
// [TextBlobRedrawCoordinator.cpp:45-53]
void TextBlobRedrawCoordinator::drawForMaskType(...) {
// MIUI MOD: SF_BugFixUpstream_3
SkPaint skPaint(paint);
if (canvas->getForceSDFT()) { // ← 路径 B 的标记
skPaint.setForceSDFT(canvas->getForceSDFT()); // 注入到 Paint
}
sk_sp<TextBlob> blob = this->findOrCreateBlob(viewMatrix, glyphRunList, skPaint, ...);
// END SF_BugFixUpstream_3
blob->draw(canvas, glyphRunList.origin(), paint, atlasDelegate);
}作用:确保即使路径 A 没有生效(例如非 TextView 的文字绘制),路径 B 仍然能将 forceSDFT 标记注入到 Skia 的文字渲染决策中。
核心决策点:SubRunControl::isSDFT()
文件:skia/src/text/gpu/SubRunControl.cpp
// [SubRunControl.cpp:70-88]
#if !defined(SK_DISABLE_SDF_TEXT)
bool SubRunControl::isSDFT(SkScalar approximateDeviceTextSize, const SkPaint& paint,
const SkMatrix& matrix) const {
// MIUI ADD: SF_BugFixUpstream_3
const bool forceSDFT = matrix.hasScale() && paint.getForceSDFT(); // ★ 核心条件
// END SF_BugFixUpstream_3
const bool wideStroke = paint.getStyle() == SkPaint::kStroke_Style &&
paint.getStrokeWidth() > 0;
return fAbleToUseSDFT &&
paint.getMaskFilter() == nullptr &&
(paint.getStyle() == SkPaint::kFill_Style || wideStroke) &&
0 < approximateDeviceTextSize &&
(fAbleToUsePerspectiveSDFT || !matrix.hasPerspective()) &&
// MIUI MOD: SF_BugFixUpstream_3
// 原始 AOSP:(fMinDistanceFieldFontSize <= approximateDeviceTextSize || matrix.hasPerspective()) &&
// approximateDeviceTextSize <= fMaxDistanceFieldFontSize;
(fMinDistanceFieldFontSize <= approximateDeviceTextSize || forceSDFT || matrix.hasPerspective()) &&
(approximateDeviceTextSize <= fMaxDistanceFieldFontSize || forceSDFT);
// END SF_BugFixUpstream_3
}
#endifMIUI 修改的核心效果:
| 条件 | AOSP 原始 | MIUI 修改后 |
|---|---|---|
| 字号 < minDistanceFieldFontSize | 不使用 SDF | forceSDFT=true 时强制使用 SDF |
| 字号 > maxDistanceFieldFontSize | 不使用 SDF | forceSDFT=true 时强制使用 SDF |
forceSDFT 生效条件 | - | matrix.hasScale() && paint.getForceSDFT() |
为什么要求 matrix.hasScale():只在有 scale 变换时才强制 SDF,因为只有 scale 变化才会导致亚像素偏移的 cache miss 问题。无 scale 时传统光栅化效果更好(更清晰)。
SDF vs DirectMask:GlyphID 构造差异
这是 forceSDFT 最终影响字形缓存命中的根本原因。
DirectMask 路径(默认,不使用 SDF)
文件:skia/src/text/gpu/SubRunContainer.cpp
// [SubRunContainer.cpp:1238-1239] prepare_for_direct_mask_drawing()
const SkPoint mappedPos = positionMatrixWithRounding.mapPoint(pos);
const SkPackedGlyphID packedID{glyphID, mappedPos, mask};
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// 包含 mappedPos 的亚像素信息!SkPackedGlyphID 构造函数(skia/src/core/SkGlyph.h:90-91):
SkPackedGlyphID(SkGlyphID glyphID, SkPoint pt, SkIPoint mask)
: fID{PackIDSkPoint(glyphID, pt, mask)} { }
// fID = (subPixelX << kSubPixelX) | (subPixelY << kSubPixelY) | (glyphID << kGlyphID)
// ↑ 2 bit ↑ 2 bit ↑ 16 bit结果:glyph_key = (glyphID, subpixelX, subpixelY) — 亚像素位置参与 key 计算。
SDFT 路径(forceSDFT=true 时)
文件:skia/src/text/gpu/SubRunContainer.cpp
// [SubRunContainer.cpp:1184] prepare_for_SDFT_drawing()
const SkPackedGlyphID packedID{glyphID};
// ↑↑↑↑↑↑↑↑
// 只有 glyphID!无亚像素信息!SkPackedGlyphID 构造函数(skia/src/core/SkGlyph.h:81-82):
constexpr explicit SkPackedGlyphID(SkGlyphID glyphID)
: fID{(uint32_t)glyphID << kGlyphID} { }
// fID = glyphID << 8,subPixelX = 0, subPixelY = 0结果:glyph_key = (glyphID) — 无亚像素位置,scale 变化不影响 key。
StrikeSpec 差异
SDFT 路径创建 strike 时使用 Identity Matrix:
文件:skia/src/text/gpu/SubRunContainer.cpp
// [SubRunContainer.cpp:1395-1396] make_sdft_strike_spec()
SkStrikeSpec strikeSpec = SkStrikeSpec::MakeMask(dfFont, dfPaint, surfaceProps, flags,
SkMatrix::I()); // ← 固定 Identity!意义:strike 的 key 不包含 view 的 scale matrix,因此 scale 从 1.0 → 0.999636 → 0.999272 变化时,所有帧使用同一个 strike cache。
为什么 SDF 不需要亚像素位置
┌─────────────────────────────────────────────────────────────┐
│ 传统 LCD/Grayscale 光栅化 │
│ │
│ 字形在特定亚像素位置预渲染为位图: │
│ 每个像素的覆盖率(antialiasing)取决于字形边缘与像素网格的关系 │
│ │
│ 位置 A (subpixel=0.27): ██░░ ← 边缘刚好在像素内 │
│ 位置 B (subpixel=0.33): █▓░░ ← 边缘跨越像素 │
│ │
│ → 不同位置 = 不同 antialiasing 结果 = 不同的位图 │
│ → 必须分别缓存 → key 必须包含亚像素位置 │
│ → scale 变化 → 位置变化 → key 不同 → CACHE MISS │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ SDF (Signed Distance Field) 渲染 │
│ │
│ 字形存储为「到字形边缘的有符号距离值」: │
│ 每个像素存储 distance(正=外部,负=内部) │
│ │
│ 同一个 SDF 纹理: [3][2][1][0][-1][-2] │
│ │
│ 位置 A: GPU shader 在阈值 0 处切割 → 生成清晰边缘 │
│ 位置 B: GPU shader 在阈值 0 处切割 → 同样清晰(亚像素插值) │
│ │
│ → 位置无关:任何坐标用同一个 SDF 纹理 │
│ → 只需缓存一份 → key 只需 glyphID │
│ → scale 变化 → 位置变化 → key 不变 → CACHE HIT │
└─────────────────────────────────────────────────────────────┘
完整链路图
┌─────────────────────────────────────────────────────────────────────┐
│ Java 层 │
│ │
│ NotificationUtil.setForceSDFT2(view, true) [反射调用] │
│ → View.setForceSDFT(true) [View.java:20502] │
│ │ │
│ ├─── mForceSDFT = true ────────────────── 路径 A ───────┐ │
│ │ │ │
│ │ 子 View draw() 时: │ │
│ │ mPreForceSDFT = parent.getForceSDFT() │ │
│ │ [View.java:27025] │ │
│ │ ↓ │ │
│ │ TextView.onDraw(): │ │
│ │ mTextPaint.setForceSDFT(getForceSDFT()) │ │
│ │ [TextView.java:9418] │ │
│ │ ↓ [JNI] │ │
│ │ SkPaint.fForceSDFT = 1 [SkPaint.h:345] │ │
│ │ │ │
│ └─── ViewRootImpl.setForceSDFT(true) ── 路径 B ───────┐ │ │
│ [ViewRootImpl.java:564] │ │ │
│ ↓ │ │ │
│ HardwareRenderer.nSetForceSDFT() │ │ │
│ ↓ [JNI → RenderThread queue] │ │ │
│ RenderProxy.setForceSDFT(true) │ │ │
│ [RenderProxy.cpp:804] │ │ │
│ ↓ │ │ │
│ CanvasContext.mForceSDFT = true │ │ │
│ [CanvasContext.h:305] │ │ │
└──────────────────────────────────────────────────────────────┼──┼───┘
│ │
┌──────────────────────────────────────────────────────────────▼──▼───┐
│ Native 层 (RenderThread, 每帧执行) │
│ │
│ CanvasContext::draw() │
│ → mRenderPipeline->setForceSDFT(mForceSDFT) │
│ [CanvasContext.cpp:1349] 路径 B │
│ → SkiaVulkanPipeline::draw() │
│ backBuffer->getCanvas()->setForceSDFT(true) │
│ [SkiaVulkanPipeline.cpp:108] │
│ │
│ ════════════════════════════════════════════════════════════════ │
│ Skia 文字渲染入口 │
│ ════════════════════════════════════════════════════════════════ │
│ │
│ TextBlobRedrawCoordinator::drawForMaskType() │
│ [TextBlobRedrawCoordinator.cpp:48-52] │
│ → if (canvas->getForceSDFT()) { ← 路径 B 汇入 │
│ skPaint.setForceSDFT(true); │
│ } │
│ → findOrCreateBlob(matrix, glyphRunList, skPaint, ...) │
│ ↑ paint 已带 forceSDFT │
│ (路径 A 或 路径 B) │
│ │
│ ════════════════════════════════════════════════════════════════ │
│ ★ 核心决策点 ★ │
│ ════════════════════════════════════════════════════════════════ │
│ │
│ SubRunContainer::MakeInAlloc() [SubRunContainer.cpp:1471]│
│ → subRunControl->isSDFT(size, runPaint, positionMatrix) │
│ │
│ SubRunControl::isSDFT() [SubRunControl.cpp:74] │
│ → forceSDFT = matrix.hasScale() && paint.getForceSDFT() │
│ → return ... && │
│ (minSize <= size || forceSDFT || perspective) && ← ★绕过★ │
│ (size <= maxSize || forceSDFT); ← ★绕过★ │
│ │
│ ════════════════ isSDFT() == true ═══════════════════════════════ │
│ │
│ make_sdft_strike_spec() [SubRunContainer.cpp:1395]│
│ → StrikeSpec::MakeMask(..., SkMatrix::I()) │
│ ↑ Identity! 不受 view scale 影响 │
│ │
│ prepare_for_SDFT_drawing() [SubRunContainer.cpp:1184]│
│ → packedID = SkPackedGlyphID{glyphID} │
│ ↑ ★ 无亚像素位置!★ │
│ → strike->digestFor(kSDFT, packedID) │
│ │
│ AtlasTextOp::onPrepareDraws() │
│ → hasGlyph(maskFormat, glyph.entry()) │
│ → key 不含亚像素 → scale 变化不影响 → ★ CACHE HIT ★ │
│ → 耗时: ~3μs (仅更新 draw token) │
│ │
│ ════════════════ isSDFT() == false (默认) ═══════════════════════ │
│ │
│ prepare_for_direct_mask_drawing() [SubRunContainer.cpp:1239]│
│ → mappedPos = positionMatrix.mapPoint(pos) │
│ → packedID = SkPackedGlyphID{glyphID, mappedPos, mask} │
│ ↑ ★ 包含亚像素位置!★ │
│ │
│ AtlasTextOp::onPrepareDraws() │
│ → hasGlyph(maskFormat, glyph.entry()) │
│ → key 含亚像素 → scale 变化 → 位置不同 → ★ CACHE MISS ★ │
│ → 重新光栅化 → 耗时: ~600μs │
└─────────────────────────────────────────────────────────────────────┘
关键源码行号速查表
| 层级 | 文件 | 行号 | 作用 |
|---|---|---|---|
| Java View | View.java | 20502 | setForceSDFT() 设置 mForceSDFT |
| Java View | View.java | 20510-20511 | getForceSDFT() 返回 mForceSDFT || mPreForceSDFT |
| Java View | View.java | 27025 | 子 View draw 时继承 mPreForceSDFT = parent.getForceSDFT() |
| Java TextView | TextView.java | 9418 | mTextPaint.setForceSDFT(getForceSDFT()) |
| Java Paint | Paint.java | 2082-2083 | nSetForceSDFT(mNativePaint, forceSDFT) |
| Java ViewRootImpl | ViewRootImpl.java | 561-564 | mAttachInfo.mThreadedRenderer.setForceSDFT() |
| JNI Paint | Paint.cpp | 1196-1198 | paint->setForceSDFT(forceSDFT) |
| JNI Renderer | android_graphics_HardwareRenderer.cpp | 1305-1307 | → RenderProxy.setForceSDFT() |
| Native RenderProxy | RenderProxy.cpp | 804-805 | post to RenderThread → CanvasContext |
| Native CanvasContext | CanvasContext.h | 305 | mForceSDFT = forceSDFT |
| Native CanvasContext | CanvasContext.cpp | 1349 | mRenderPipeline->setForceSDFT(mForceSDFT) |
| Native Pipeline | SkiaVulkanPipeline.cpp | 108 | canvas->setForceSDFT(getForceSDFT()) |
| Native Pipeline | SkiaOpenGLPipeline.cpp | 177 | surface->getCanvas()->setForceSDFT(getForceSDFT()) |
| Skia SkPaint | SkPaint.h | 344-345 | fForceSDFT 标记位 |
| Skia SkCanvas | SkCanvas.h | 2204-2205 | fForceSDFT 标记位 |
| Skia Coordinator | TextBlobRedrawCoordinator.cpp | 48-49 | canvas→paint 标记传递(路径 B 汇入) |
| ★ Skia 决策 | SubRunControl.cpp | 74 | forceSDFT = matrix.hasScale() && paint.getForceSDFT() |
| ★ Skia 决策 | SubRunControl.cpp | 86-87 | 绕过 min/maxDistanceFieldFontSize 限制 |
| ★ SDFT key | SubRunContainer.cpp | 1184 | SkPackedGlyphID{glyphID} — 无亚像素位置 |
| DirectMask key | SubRunContainer.cpp | 1239 | SkPackedGlyphID{glyphID, mappedPos, mask} — 含亚像素 |
| SDFT StrikeSpec | SubRunContainer.cpp | 1395-1396 | SkMatrix::I() — 不受 view scale 影响 |
| SkPackedGlyphID | SkGlyph.h | 81-82 | 仅 glyphID 构造:fID = glyphID << kGlyphID |
| SkPackedGlyphID | SkGlyph.h | 90-91 | 含位置构造:fID = PackIDSkPoint(glyphID, pt, mask) |
条件约束
forceSDFT 生效需要满足:
matrix.hasScale()— 当前变换矩阵包含 scale 分量(否则不需要 SDF 优化)paint.getForceSDFT()— 标记已通过路径 A 或 B 传递到 paintfAbleToUseSDFT— GPU 支持 SDF 渲染(现代设备均支持)paint.getMaskFilter() == nullptr— 无自定义 mask filterpaint.getStyle() == kFill_Style或 wide stroke — 填充或宽描边样式approximateDeviceTextSize > 0— 文字可见
不生效的情况:
matrix.hasScale() == false(纯平移/旋转,无缩放)→ 不需要 SDF,传统光栅化即可- 文字使用了自定义 MaskFilter → 不兼容 SDF
- DDRE 模式启用时(
strikeDeviceInfo.mUseDDRE == true)→ 走另一套渲染路径
性能对比
| 指标 | DirectMask(默认) | SDFT(forceSDFT=true) |
|---|---|---|
| glyph_key 组成 | (glyphID, subpixelX, subpixelY) | (glyphID) |
| scale 变化时 cache | MISS(每帧不同亚像素位) | HIT(key 不变) |
| 单次 onPrepare 耗时 | ~600μs(重新光栅化) | ~3μs(仅 token 更新) |
| 文字清晰度 | 最优(亚像素精确光栅化) | 略低(SDF 插值,SDFT2 已改善) |
| 适用场景 | 静态/无 scale 动画 | scale 动画期间 |
| 内存开销 | 每个亚像素位一份缓存 | 仅一份 SDF 纹理 |
使用示例
SystemUI 中的使用(已有)
文件:NotificationUtil.java:314
public static void setForceSDFT2(View view, boolean isForceSDFT) {
if (view == null) return;
try {
Class viewCl = Class.forName("android.view.View");
Method setForceSDFT = viewCl.getMethod("setForceSDFT", boolean.class);
setForceSDFT.invoke(view, isForceSDFT);
} catch (Exception e) {
Log.d(TAG, "ViewRootImpl setForceSDFT2 Method not found!");
}
}调用点:MiuiNotificationPanelAnimControllerImpl.kt 中的 ViewAnimatorListener
override fun beginAnimator() {
NotificationUtil.setForceSDFT2(view, true) // 动画开始:启用 SDF
}
override fun completeAnimator() {
NotificationUtil.setForceSDFT2(view, false) // 动画结束:恢复普通
}适用场景
对 任何在 scale 动画期间包含文字的 View 树 均可使用:
// 动画开始前
targetView.setForceSDFT(true) // 或通过反射
// 动画结束后
targetView.setForceSDFT(false)由于继承机制(mPreForceSDFT = parent.getForceSDFT()),只需对动画的根 View 设置一次,子树中所有 TextView 自动生效。