WebView 的排版不由 Web 标准管,而由底层 OS 管。iOS 用 San Francisco 和 PingFang SC 绘制,字重支持完整的 100–900;Android 用 Roboto 和 Noto,旧版本只暴露 400/500/700,且 500 常被悄悄降级成 400。字体回退分叉——Android 上是 Noto Color Emoji、iOS 上是 Apple Color Emoji——而腾讯 X5 这类厂商内核会直接禁用非白名单字体回退。系统字体缩放会覆盖 CSS:Android 的 WebSettings.setTextZoom 与 iOS 的 UIContentSizeCategory 会默默把每个 px、rem、em 都乘以缩放比,撑破布局。结果是品牌排版在一个引擎上正确、在下一个上就崩——emoji 变豆腐块、字重塌陷、布局溢出。宿主自有 runtime 锁定排版:受控字体栈、封顶系统缩放、保底回退字体、一致的字重映射,页面无法再漂。
一份字体列表,两幅不同的画
同一个 font-family 声明,在每个平台上解析出的字形、hinting 与字重映射都不同。iOS 上的 PingFang SC 带着完整的 100–900 字重区间,而 Android 的系统 Roboto——尤其旧版本——只提供 400、500、700,且 500 常被忽略或降级成 400。在 iOS 上看起来干净利落的 font-weight: 500(中等),在 Android 上要么渲染成普通的 400,要么被描边 fallback 画得比预期更粗。列一份 -apple-system, Roboto, "PingFang SC" 并不能调和两者;引擎只会挑第一个它真正画得出来的名字,其余一律忽略。
回退在最要命的地方断裂
CJK 与 emoji 是回退链最容易崩的地方。基于 Chromium 的 Android WebView 在很多设备上默认仍是 Droid Sans Fallback,它不覆盖 Unicode 扩展区——新 emoji、生僻的 CJK 扩展字符——于是它们被画成豆腐块或问号。iOS 在较新的 Unicode 上表现更好,但若页面从不声明 UTF-8 或回退字体族,仍会坠回一个不支持的字体。厂商内核让它更糟:腾讯 X5 能直接禁用非白名单字体回退,于是连你加载的 Web 字体都可能不生效。品牌精心挑选的字体,好不好看全看设备碰巧装了什么。
系统缩放悄悄覆盖你的 CSS
这是安静的杀手。Android 上,系统设置注入一个全局缩放因子,被 WebSettings.setTextZoom 反映出来,WebView 在布局前把 px、rem、em 全乘以它。iOS 上,UIContentSizeCategory 把动态字体推进页面。这个覆盖是「强制」的:一个高度固定的容器,字一放大就溢出截断;一个精密的 Flex/Grid 布局,字一膨胀就互相挤压;响应式断点被破坏,因为页面被强行拖进桌面端布局逻辑。页面从没要求过,也无法单靠 CSS 退出。
runtime 解法
宿主自有 runtime 让排版跨引擎可确定。它注入受控字体栈,让每个平台只看到它真正能画的字体族;保底携带 Noto Sans CJK 与 Noto Color Emoji,使 emoji 与生僻字形永不变成豆腐;归一化 -webkit-font-smoothing 以统一抗锯齿;强制单一字重映射,让 500 在哪都意味着 500。它把系统字体缩放封顶在安全值,并预加载品牌字体。页面渲染得一致,因为做决定的不是设备,而是 runtime。
| 维度 | WebView | 宿主自有 runtime |
|---|---|---|
| 字重映射 | iOS 完整 100–900;Android 400/500/700、500 降级 | 单一强制字重映射 |
| 字体回退 | 分叉;X5 禁用非白名单 | 保底 Noto CJK + Color Emoji |
| emoji / 生僻字形 | 很多 Android 上是豆腐块 | 永远画出,不缺失 |
| 系统缩放 | 强制覆盖,撑破布局 | 封顶在安全值 |
| 抗锯齿 | 各引擎不一致 | 跨平台归一 |
| 品牌一致性 | 看设备运气 | 处处同渲染 |
本系列反复回到一个事实:WebView 继承了平台最弱的那些面,还额外加了一个「Web 形状」的面。排版是用户最先看到、也最快下判断的那个面——而在原生 WebView 里,掌控它的不是设计师,而是设备。runtime 把品牌重新交还给自己的字体。
参考来源
- PHP 中文网——移动端字体渲染差异:iOS -apple-system 与 Android Roboto 字形/hinting/字重映射不同;Android WebView 上 font-weight 500 无效;X5 禁用非白名单回退 — 字重与回退在各引擎如何分叉。
- TrueSight——WebView 排版「防御性」治理:Android textZoom 与 iOS UIContentSizeCategory 强制覆盖 CSS;setTextZoom(100) 锁定 — 系统缩放如何撑破布局,以及锁定的办法。
- CSDN 问答——WebView 内 emoji/生僻 CJK 渲染成豆腐块;低版本 Android 缺失字体回退与 HarfBuzz/Color Emoji — 字形为何消失,以及如何注入 Noto 字体。
- 稀土掘金——Hybrid 之 WebView 渲染原理:WebKit(iOS)与 Blink(Android)管线差异,含 font-smoothing 不一致 — 排版漂移背后的引擎级分叉。
- web.dev——Android 上的 Web:WebView 是 OS 渲染管线的一部分、且缺浏览器的统一控制 — 排版取决于设备的结构性原因。