第一章:Go字体渲染链路全景概览
Go 语言本身不内置图形渲染引擎,其标准库(如 image/draw、golang.org/x/image/font)仅提供底层字形数据处理能力,真正的字体渲染依赖于外部生态协同完成。理解这一链路对构建高质量文本渲染应用(如命令行富文本工具、嵌入式 UI 或 PDF 生成器)至关重要。
字体数据加载与解析
Go 程序通常通过 golang.org/x/image/font/opentype 包加载 .ttf 或 .otf 文件。该包将字体二进制流解析为 *opentype.Font 实例,提取字形轮廓(glyph outlines)、度量信息(ascent/descent/advance width)及 Unicode 映射表。示例代码如下:
fontBytes, _ := os.ReadFile("NotoSansCJK-Regular.ttc") // 支持 TTC 集合
font, err := opentype.Parse(fontBytes)
if err != nil {
log.Fatal(err) // 解析失败可能因校验和错误或格式不支持
}
文本布局与字形映射
给定字符串与字体大小后,需调用 font.Metrics() 获取基础度量,并借助 golang.org/x/image/font/basicfont 或自定义 font.Face 实现将 Unicode 码点映射为字形 ID(glyph ID)。注意:Go 不自动处理 OpenType 特性(如 ligatures、kerning),需手动启用 opentype.WithHinting(opentype.HintingFull) 或集成 golang.org/x/image/font/sfnt 进行高级特性解析。
光栅化与绘制输出
字形轮廓由 font.GlyphBuf 描述,最终需光栅化为位图。标准库不提供光栅器,常见实践包括:
- 使用
golang.org/x/image/font/inconsolata提供的预光栅化位图(仅限固定字体) - 调用外部 C 库(如 FreeType)通过 cgo 封装实现动态光栅化
- 借助
github.com/freddierice/go-freetype等社区封装包完成抗锯齿渲染
| 组件层级 | 代表包/方案 | 是否支持子像素渲染 | 是否支持可变字体 |
|---|---|---|---|
| 字体解析 | x/image/font/opentype |
否 | 有限支持 |
| 文本布局 | x/text/unicode/norm + 自定义逻辑 |
否 | 否 |
| 光栅化 | FreeType(cgo)或 WebAssembly | 是 | 是 |
整个链路呈现“数据加载 → 布局计算 → 几何光栅化 → 像素合成”的线性依赖关系,任一环节缺失都将导致文本显示异常(如方块、错位或空白)。
第二章:freetype绑定层的Go接口与C ABI契约分析
2.1 Go struct到FT_Face的内存布局对齐验证(含unsafe.Sizeof与dlv memory read实测)
内存对齐关键约束
FreeType 的 FT_Face 是 C 结构体,其字段按 8 字节自然对齐;Go 中对应 struct 必须严格匹配偏移与大小,否则 C.FT_New_Face 调用将触发段错误。
验证代码示例
type Face struct {
NumGlyphs uint32 // offset: 0
_ [4]byte // 填充至 8-byte 对齐
Flags uint64 // offset: 8 → 必须对齐到 8
}
fmt.Printf("Size: %d, Align: %d\n", unsafe.Sizeof(Face{}), unsafe.Alignof(Face{}.Flags))
// 输出:Size: 16, Align: 8
unsafe.Sizeof确认总长为 16 字节;Alignof验证uint64字段起始地址必为 8 的倍数。若省略[4]byte填充,Flags将落于 offset 4,违反 C ABI。
dlv 实测对比表
| 字段 | Go struct offset | FT_Face (C) offset | 是否一致 |
|---|---|---|---|
num_glyphs |
0 | 0 | ✅ |
flags |
8 | 8 | ✅ |
内存读取验证流程
graph TD
A[dlv attach] --> B[mem read -fmt uint32 -len 1 0x7f...a0]
B --> C[比对 Go struct.NumGlyphs]
C --> D[一致则对齐正确]
2.2 font.Face.Options中FontSize字段的语义定义与单位转换约定溯源
FontSize 字段在 font.Face.Options 中表示逻辑字号(logical point size),而非像素值。其语义源自 PostScript 传统:1 point = 1/72 inch,与设备无关。
单位转换核心逻辑
// FontSize=12 表示 12pt → 需结合DPI转换为设备像素
px := float64(opts.FontSize) * dpi / 72.0 // dpi 通常为96(Web)或144(HiDPI)
opts.FontSize是浮点数,支持亚像素精度(如12.5)- 转换依赖运行时
dpi上下文,不固化为固定像素
关键约定表
| 环境 | 默认 DPI | 典型 FontSize→px 映射(12pt) |
|---|---|---|
| Web (CSS) | 96 | 16 px |
| Windows GDI | 96 | 16 px |
| macOS Core Text | 72 | 12 px(因1pt=1px) |
渲染流程示意
graph TD
A[FontSize=12] --> B[Scale by DPI/72]
B --> C[Apply font transform matrix]
C --> D[Rasterize at target resolution]
2.3 CGO调用栈中float64→FT_F26Dot6的定点数转换逻辑逆向(反汇编+寄存器追踪)
核心转换公式
FT_F26Dot6 是 FreeType 中 26.6 定点格式(26位整数 + 6位小数),等价于 int32(float64 * 64)。CGO 调用时,Go 的 float64 经 cgo 传入 C 函数前,由 runtime 插入隐式转换。
关键汇编片段(amd64)
// go:linkname _Cfunc_ft_set_char_size
// RAX ← float64 参数(来自 Go stack)
cvtsd2siq %rax, %rax // float64 → int64(截断)
salq $6, %rax // 左移6位:等效 ×64
此处
cvtsd2siq执行向零截断(not rounding),故1.999999→1→64,而非127;需注意 Go 层应提前math.Round()若需四舍五入。
寄存器流转摘要
| 阶段 | 寄存器 | 值类型 | 说明 |
|---|---|---|---|
| Go 调用入口 | RAX | float64 bit | IEEE754 双精度位模式 |
| 转换后 | RAX | int32 | 已左移6位的 FT_F26Dot6 |
graph TD
A[float64 in Go] --> B[cgo stub: cvtsd2siq]
B --> C[salq $6]
C --> D[FT_F26Dot6 int32]
2.4 FT_Set_Char_Size调用前的scale因子计算路径(从pt→px→1/64像素单位全程跟踪)
字体尺寸转换需严格遵循排版精度要求。核心路径为:pt → px → 1/64 pixel(即FreeType内部使用的FT_F26Dot6定点单位)。
单位换算链路
- 1 pt = 1/72 inch
- 1 inch = DPI 像素(如
dpi = 96)→1 pt = dpi / 72 px - 1 px = 64 × 1/64 px → 故
1 pt = (dpi / 72) × 64(单位:1/64 px)
关键计算代码
// 假设 target_pt = 12.0f, dpi = 96
float pixels_per_pt = dpi / 72.0f; // ≈ 1.333... px/pt
float pixels = target_pt * pixels_per_pt; // 12 × 1.333... = 16.0 px
FT_F26Dot6 size_26dot6 = (FT_F26Dot6)(pixels * 64.0f); // 16.0 × 64 = 1024 (0x400)
该转换将浮点pt值无损映射至FreeType所需的26.6定点格式,确保FT_Set_Char_Size输入的width与height参数满足亚像素对齐要求。
转换关系表(dpi=96)
| pt | px | 1/64 px (FT_F26Dot6) |
|---|---|---|
| 6 | 8.0 | 512 |
| 12 | 16.0 | 1024 |
| 18 | 24.0 | 1536 |
graph TD
A[pt 输入] --> B[dpi/72 → px/pt]
B --> C[pt × px_per_pt → px]
C --> D[px × 64 → FT_F26Dot6]
D --> E[传入 FT_Set_Char_Size]
2.5 runtime·cgocall上下文中的栈帧污染防护机制与字体参数传递完整性校验
Go 运行时在 cgocall 调用 C 函数时,需严格隔离 Go 栈与 C 栈,防止跨语言调用引发的栈帧污染。
栈帧隔离策略
- Go 协程栈由 runtime 动态管理,C 调用前自动切换至固定大小的
g0系统栈; - 所有 Go 指针在进入 C 前经
cgoCheckPointer静态/动态双重校验; - 字体参数(如
fontFace,fontSize,hintingMode)统一打包为C.struct_font_params并通过unsafe.Pointer传入,禁止直接传递 Go 结构体字段地址。
参数完整性校验流程
// C-side validation stub (called pre-render)
int validate_font_params(const struct font_params *p) {
if (!p || p->size <= 0 || p->size > 256) return -1; // fontSize bounds
if (p->hinting < 0 || p->hinting > 3) return -2; // enum range
return (p->family && strlen(p->family) < 128) ? 0 : -3; // null-terminated + length guard
}
该函数在 CGO 入口强制校验:size 防溢出、hinting 防越界、family 防空指针与缓冲区溢出,确保字体配置语义安全。
| 校验项 | 触发条件 | 处理动作 |
|---|---|---|
fontSize |
≤0 或 >256 pt | 拒绝渲染,返回错误 |
hintingMode |
非 0–3 枚举值 | 截断为默认值 1 |
family |
NULL 或长度 ≥128 字节 | 返回 -3 错误码 |
graph TD
A[cgocall entry] --> B[Switch to g0 stack]
B --> C[Copy & validate font_params]
C --> D{validate_font_params() == 0?}
D -->|Yes| E[Proceed to C render]
D -->|No| F[panic with cgo: invalid font config]
第三章:光栅化尺寸生成的核心数学模型
3.1 DPI感知下的em-size→device-pixel映射公式推导与Go runtime/draw实现交叉验证
DPI感知的核心在于将逻辑尺寸(如 em)按系统DPI缩放为设备像素。标准映射公式为:
$$ \text{device_pixels} = \text{em_size} \times \frac{\text{DPI}}{96} \times \text{font_size_pt} \times \frac{1}{72} \times 96 $$
简化后得关键关系:
px = em × (DPI / 96) × pt × (96 / 72) → 实际Go中采用更直接的 px = em × (DPI / 72)(因1pt ≡ 1/72 inch,1em ≈ font-size in pt)。
Go draw.Text 的实际映射路径
// src/image/draw/text.go 中关键片段(简化)
func (d *Drawer) DrawText(text string, face font.Face, dst image.Image, pt fixed.Point26_6) {
// face.Metrics().Height 是 fixed.Int26_6,已内含DPI感知的像素高度
// runtime 调用 font.Face 实现时,会通过 golang.org/x/image/font/basicfont 等桥接系统DPI
}
该调用链最终委托至 golang.org/x/image/font/sfnt,其 Face.Metrics() 返回值已根据 face.DPI() 动态缩放——验证了 em → px 映射在字体光栅化前已完成DPI校准。
关键参数对照表
| 符号 | 含义 | Go 对应字段 |
|---|---|---|
em |
逻辑单位(相对字体大小) | face.Metrics().Height(归一化为1em基准) |
DPI |
当前显示设备DPI | face.DPI()(由 font.Face 实现动态提供) |
px |
输出设备像素 | fixed.Point26_6 坐标系中的整数像素位置 |
映射一致性验证流程
graph TD
A[em-size输入] --> B{Go font.Face}
B --> C[face.DPI()]
B --> D[face.Metrics().Height]
C & D --> E[fixed.Int26_6 pixel calculation]
E --> F[draw.Drawer 渲染到 device-pixel buffer]
3.2 freetype的26.6定点数精度边界测试:小字号(
FreeType 使用 26.6 定点数(26位整数 + 6位小数)表示字体度量,其最小可分辨单位为 $1/64 \approx 0.015625$。当字号低于 8pt(约 10.67px @72dpi),字形轮廓缩放后坐标常落入多个相邻 26.6 值映射到同一像素的“精度坍塌区”。
可视化复现核心逻辑
// 将 pt 转为 26.6 定点数(假设 72dpi,1pt = 1px)
func ptTo266(pt float64) int32 {
px := pt * 72.0 / 72.0 // 归一化至像素单位
return int32(px * 64) // ×64 → 26.6 格式
}
该函数将 7.9pt 映射为 505(即 505/64 ≈ 7.890625),而 7.905pt 同样映射为 506/64 ≈ 7.90625 —— 两者在渲染时可能被栅格化器视为相同位置,引发字形偏移或笔画粘连。
关键误差阈值对比
| 字号(pt) | 理论像素值 | 26.6 表示值 | 实际解析值(pt) | 绝对误差(pt) |
|---|---|---|---|---|
| 6.0 | 6.0 | 384 | 6.0 | 0.0 |
| 6.015 | 6.015 | 385 | 6.015625 | 0.000625 |
精度坍塌示意(mermaid)
graph TD
A[6.01pt 输入] --> B[×64 → 384.64]
B --> C[舍入为 385]
C --> D[÷64 → 6.015625pt]
D --> E[像素级偏移 ≥0.5px?]
3.3 字体矩阵(FT_Matrix)在size缩放中的隐式参与:从Go端设置到FT_Load_Glyph的链式影响
FT_Matrix 并非显式传入 FT_Load_Glyph,却在 FT_Set_Transform 与 FT_Set_Char_Size 调用后,悄然介入 glyph metrics 计算与轮廓变换。
矩阵生效的隐式时序
FT_Set_Char_Size设置 nominal size → 触发 internalface->size->metrics重算FT_Set_Transform(face, &matrix, &delta)注册矩阵 → 存入face->internal->transform_matrixFT_Load_Glyph执行时自动应用该矩阵(若非 identity),再进行 hinting 与栅格化
Go 绑定中的关键调用链
// ft.go 中典型流程(简化)
face.SetCharSize(12*64, 0, 72, 72) // 单位:26.6 fixed
face.SetTransform(&FT.Matrix{xx: 0x10000, yy: 0x10000}, nil) // 单位矩阵,显式设为 identity
face.LoadGlyph('A', ft.LoadDefault)
此处
SetTransform若省略,transform_matrix保持零值(即 identity);但一旦设置非单位矩阵(如缩放/倾斜),FT_Load_Glyph内部会将 glyph 轮廓顶点左乘该矩阵,早于 hinting 阶段,直接影响glyph->outline.points坐标系。
链式影响关键节点
| 阶段 | 是否受 FT_Matrix 影响 | 说明 |
|---|---|---|
FT_Set_Char_Size |
否 | 仅更新 size->metrics,不触碰 transform |
FT_Load_Glyph(轮廓加载) |
是 | 对原始 outline 点执行仿射变换 |
FT_Render_Glyph |
否 | 仅栅格化已变换后的 outline |
graph TD
A[FT_Set_Char_Size] --> B[FT_Set_Transform]
B --> C[FT_Load_Glyph]
C --> D[Apply FT_Matrix to outline.points]
D --> E[Hinting on transformed coordinates]
E --> F[Rasterization]
第四章:汇编级动态验证与调试实践
4.1 使用delve+GDB双调试器捕获FT_Load_Glyph入口时的%rdi(FT_Face)与%rsi(charcode)寄存器状态
调试环境协同策略
Delve(Go 进程主控)负责断点注入与符号解析,GDB(底层寄存器级)接管系统调用前的 ABI 级寄存器快照。二者通过 ptrace 共享同一进程上下文。
寄存器捕获关键步骤
- 在
FT_Load_Glyph符号地址处设置硬件断点(hbreak *0xADDR) - 使用
info registers rdi rsi即时读取调用约定传参值 - 验证
rdi指向有效FT_Face结构(检查rdi->num_glyphs != 0)
GDB 命令示例与分析
(gdb) b FT_Load_Glyph
(gdb) r
(gdb) info registers rdi rsi
rdi 0x5555557a8c20 93824995262496 # FT_Face handle
rsi 0x41 65 # 'A' charcode (UTF-32)
该输出表明:rdi 持有 FreeType 库分配的字体实例指针;rsi 为 Unicode 码点(此处为 ASCII 字母 A),符合 System V AMD64 ABI 规范。
| 寄存器 | 含义 | 类型 | 验证方式 |
|---|---|---|---|
%rdi |
FT_Face |
struct ptr | x/1gx $rdi 查首字段 |
%rsi |
charcode |
uint32_t | p/x $rsi 显示十六进制 |
4.2 在libfreetype.so.6中定位FT_Size_Request和FT_Size_Metrics结构体填充指令流(objdump -d反汇编精读)
关键函数入口识别
使用 objdump -d libfreetype.so.6 | grep -A5 "FT_New_Size" 定位尺寸初始化逻辑,发现 FT_New_Size 调用 ft_size_init,后者调用 ft_size_request。
核心结构体填充指令段
mov DWORD PTR [rax+0x0], 0x1 # req->width ← 1 (units: FT_UInt16)
mov DWORD PTR [rax+0x4], 0x2 # req->height ← 2
mov DWORD PTR [rax+0x8], 0x3 # req->horiResolution
mov DWORD PTR [rax+0xc], 0x4 # req->vertResolution
rax 指向 FT_Size_Request 实例;偏移量严格对应结构体字段顺序(width, height, horiResolution, vertResolution),验证 ABI 布局与头文件 freetype/fttypes.h 一致。
结构体字段映射表
| 偏移 | 字段名 | 类型 | 含义 |
|---|---|---|---|
| 0x0 | width | FT_UInt16 | 请求宽度(1/64像素) |
| 0x4 | height | FT_UInt16 | 请求高度 |
| 0x8 | horiResolution | FT_UInt16 | 水平DPI |
| 0xc | vertResolution | FT_UInt16 | 垂直DPI |
指令流依赖关系
graph TD
A[FT_New_Size] --> B[ft_size_init]
B --> C[ft_size_request]
C --> D[填充 FT_Size_Request]
D --> E[调用 driver->request_size]
4.3 Go runtime.mcall切换前后字体参数在栈上的生命周期快照对比(基于stack trace与memory dump)
mcall 是 Go 运行时中用于 M 级别(OS 线程)上下文切换的核心函数,不涉及 G 调度,但会保存/恢复当前 goroutine 的栈帧。所谓“字体参数”实为误称——Go runtime 中无字体概念;此处应指 runtime.g 结构体中与调度上下文强相关的字段(如 g.sched.pc, g.sched.sp, g.sched.g),常被误读为“前端渲染参数”。
栈帧快照关键字段
g.sched.sp: 切换前保存的栈顶指针(指向旧栈帧)g.sched.pc: 下一条待执行指令地址(mcall返回后跳转目标)g.sched.g: 当前 goroutine 指针(用于恢复 g0 ↔ user g 切换)
内存布局差异对比
| 状态 | g.sched.sp 值(示例) |
栈底可见参数(dump offset -0x20) | 是否仍可达 |
|---|---|---|---|
mcall 前 |
0xc00007e7a0 |
pc=0x105c8a0(user func) |
是(活跃栈) |
mcall 后 |
0xc00007e6f0 |
pc=0x102a3b0(goexit stub) |
否(原栈帧已压入 g0 栈) |
// runtime/asm_amd64.s 截取 mcall 入口
TEXT runtime·mcall(SB), NOSPLIT, $0-0
MOVQ SP, g_scheduled_sp(BX) // 保存当前 SP → g.sched.sp
LEAQ fn+0(FP), AX // fn 是回调函数指针(如 goexit)
MOVQ AX, g_scheduled_pc(BX) // 保存返回 PC
MOVQ g0, BX // 切换到 g0 栈
MOVQ g0_m(BX), BP // 加载 m 结构
JMP schedule // 进入调度循环(不返回 user g 栈)
逻辑分析:
mcall不修改g.sched.g,但将当前g的sp/pc快照写入其sched字段,并立即切换至g0栈执行。原 user goroutine 栈帧未被回收,但因SP已更新且无活跃引用,在后续gc扫描中被标记为不可达——这正是栈上“字体参数”(实为调度元数据)生命周期终止的精确内存信号。
graph TD
A[User Goroutine 栈] -->|mcall 调用前| B[g.sched.sp → user栈顶]
B --> C[参数如 pc/sp 仍驻留栈上]
C --> D[mcall 执行]
D --> E[保存 sp/pc → g.sched]
E --> F[SP 切换至 g0 栈]
F --> G[原栈帧失去根可达性]
4.4 利用perf record -e instructions:u采集字体加载热点,关联Go源码行号与x86-64指令周期损耗
instructions:u 事件精准捕获用户态每条x86-64指令执行(含分支、访存、ALU),是定位字体解析/渲染路径中微秒级瓶颈的黄金指标。
# 在字体加载关键流程中运行(如 go run main.go)
perf record -e instructions:u -g -F 99 --call-graph dwarf \
--no-submodules -- ./font-loader --font-path=NotoSansCJK.ttc
-F 99 避免采样过载;--call-graph dwarf 保留Go内联函数与行号映射;--no-submodules 过滤第三方字体库无关调用栈。
关键约束与验证步骤
- Go需编译时禁用优化:
go build -gcflags="-N -l" - 确保内核支持
perf_event_paranoid ≤ 1 - 使用
perf script -F +srcline输出带main.go:127的符号化栈
| 字段 | 示例值 | 说明 |
|---|---|---|
instructions:u |
1,248,302 | 用户态总指令数 |
cycles:u |
3,891,445 | 对应周期数(IPC≈0.32) |
L1-dcache-load-misses:u |
42,109 | 缓存未命中显著影响字体字形缓存 |
graph TD
A[font.LoadFace] --> B[parseTTCHeader]
B --> C[decompressCFFTable]
C --> D[interp.CFFCharString]
D --> E[render.GlyphRaster]
E --> F[cache.PutGlyphBitmap]
高IPC偏差点(如interp.CFFCharString中循环解码分支)将暴露为instructions:u密集但cycles:u异常攀升区域。
第五章:工程化建议与跨平台一致性陷阱总结
构建流程标准化的落地实践
在某中大型电商客户端项目中,iOS 和 Android 团队长期使用各自独立的 CI 脚本(iOS 用 Fastlane + GitHub Actions,Android 用 Gradle Wrapper + Jenkins),导致同一版本号下构建产物 ABI 兼容性不一致。我们推动统一采用 Nx + Turborepo 管理多平台构建流水线,强制约定 build:all 命令必须同时触发 iOS(Xcode 15.3+)、Android(AGP 8.4+)、Web(Vite 5.2+)三端构建,并通过 checksum 校验生成物哈希值。以下为关键校验逻辑片段:
# post-build-integrity-check.sh
for platform in ios android web; do
sha256sum "dist/$platform/bundle.js" | cut -d' ' -f1 >> .build_hashes
done
sha256sum .build_hashes | head -c16 > .build_fingerprint
环境变量注入的隐式差异
不同平台对环境变量大小写、前缀处理存在根本差异:React Native iOS 会自动过滤 RN_ 前缀变量,而 Android Gradle 插件默认将 BUILD_TYPE=release 转换为全大写 BUILD_TYPE,但 Webpack DefinePlugin 则严格区分 process.env.NODE_ENV 大小写。某次灰度发布中,因 API_BASE_URL 在 Android 中被误设为 api_base_url(下划线格式),导致 12% 的安卓用户请求 404。解决方案是引入中间层配置文件 env.config.ts,所有平台启动时统一读取 JSON5 格式配置并做标准化转换:
| 平台 | 原始变量名 | 实际生效名 | 是否需转义 |
|---|---|---|---|
| iOS | API_BASE_URL | API_BASE_URL | 否 |
| Android | api_base_url | API_BASE_URL | 是 |
| Web | ApiBaseUrl | API_BASE_URL | 是 |
字体渲染一致性破防现场
某金融类 App 在 iPadOS 17.4 上出现按钮文字截断,而相同代码在 macOS Safari 17.4 中显示正常。经排查发现:iOS WebKit 对 font-feature-settings: "ss01" 的支持存在渲染延迟,且 text-rendering: optimizeLegibility 在 iOS 17+ 中会强制启用字体子像素抗锯齿,导致行高计算偏差。最终采用 CSS 容器查询 + 特征检测组合方案:
@container (min-width: 768px) {
.btn {
line-height: calc(1em + 2px);
}
}
@supports (font-feature-settings: "ss01") and (-webkit-appearance: none) {
.btn {
text-rendering: auto;
}
}
网络请求拦截的平台鸿沟
使用 MSW(Mock Service Worker)进行前端联调时,Android WebView 95+ 支持 navigator.serviceWorker.ready,但 iOS WKWebView 16.4 仍拒绝注册非 HTTPS 上下文的 Service Worker。团队被迫构建双模 mock 系统:开发环境启用 MSW,测试环境降级为基于 fetch 拦截的轻量 mock 层(mock-fetch.ts),并通过构建时条件编译控制:
flowchart TD
A[启动应用] --> B{process.env.NODE_ENV === 'development'?}
B -->|是| C[加载 msw-worker.js]
B -->|否| D[注入 mock-fetch.ts]
C --> E[Service Worker 注册]
D --> F[全局 fetch 代理]
本地存储容量策略失控
某教育 App 在 iOS 16.6 上因 localStorage 写入超 10MB 触发 QuotaExceededError,而 Android Chrome 120 允许至 25MB。我们不再依赖单一存储机制,改为分层策略:高频小数据走 IndexedDB(封装为 IDBStorage 类),用户偏好存入 AsyncStorage(React Native),敏感凭证交由 Keychain/Keystore。关键决策逻辑通过运行时探测确定:
const storageStrategy = await detectStorageCapacity();
// 返回 { type: 'indexeddb', limit: 12_000_000 } 或 { type: 'asyncstorage', limit: 8_000_000 } 