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 把品牌重新交还给自己的字体。

参考来源