为什么 flutter 的字体在 iOS 系统上更「细」?

供稿来自:@张文森

现象

一切要从 UI 设计 的一个反馈说起:

当前 flutter 开发的社区页面,在 iOS 文字显示的特别细(书城页面也有相同的问题),相比原生渲染还原度不够,需要看一下。

对比原生 iOS 的其他页面,字体的字重看起来确实不一样,也确实如设计反馈一样,flutter 页面的字体比原生页面要「细」一些,以社区帖子为例:

查找原因

开始简单的以为是 flutter 代码的问题,和 UI 同学调试半天后,都没实现原生的效果。

由此确定应该不是项目本身代码的原因,伟大的福尔摩斯曾经说过,「排除一切不可能的,剩下的即使再不可能,那也是真相」。所以开始怀疑是 flutter sdk 的问题,看了一下官方的 issue,发现也有类似的反馈。不幸的是,官方好像曲解了这个issue的原意,所以以 「重复问题」的理由给关掉了,所以为何会有如此现象,我们就不得而知了。

iOS 系统上的字体

靠人靠天靠祖上,不算是好汉,为了找到问题的根因,我决定自己深入研究一下。众所周知,iOS 系统有一个非常有名的中文字体「苹方」,这个字体有多么有名呢?这么说吧,七猫的 UI设计师 给 鸿蒙App 的设计稿,中文字体用的也是苹方😂。
在iOS系统上,西文字体使用「SF Pro」,中文字体就使用「苹方」。

结合免费小说 App 中 flutter 页面 和 原生页面中的文字「2」的渲染,我们能够明显看出来,flutter中的「2」使用苹方字体渲染,而原生页面中的「2」使用的却是 SF Pro。

flutter 3.x版本的bug

产生这个差异的原因,是因为 flutter 3.x 版本的一个bug:在 fluter 中,文字部分的字重定义了 9 个枚举值,分别是FontWeight.w100、FontWeight.w200、FontWeight.w300...FontWeight.w900,我们分别使用这九种字重渲染“七猫 abcdx y z 9876543 2 1”,结果如下:

从图中我们不难看出,中文的 「七猫」相比较于 西文的 「abcd...3 2 1」部分,字重只体现出来了两种,而非预期的平滑渐变效果,即在 iOS 系统上,flutter 无法正常渲染多级字重。

这个bug产生的原因,其实和 flutter 关系也不大,究其根本,是Skia渲染引擎的锅,Skia的SkFontMgr_Mac::onMatchFamilyStyleCharacter函数在匹配中文字符(如PingFang SC)时,生成的CTFontDescriptor仅包含weight属性,未指定familyName。这导致macOS的CoreText框架无法正确关联字体家族,转而回退到系统默认字体,且忽略用户指定的权重(如w600),最终使用默认权重(w400)渲染。

为了解决该问题,我们在flutter代码中手动指定了字体的回退逻辑 :fontFamilyFallback: const ['PingFang SC']来临时解决该问题,所以字重展示恢复了正常,但随之而来的,就是「某些 iOS 系统」上,西文字符的展示会和原生有些许不同,比如 「2」展示成了苹方体的「2」。

iOS 系统上字体的差异性

同样的flutter代码,为什么某些 「iOS系统」上,「2」会显示成苹方体的「2」,而某些却不会呢?这个就要从苹果偷偷摸摸的对苹方字体的调整说起。苹果在 iOS 18上,启用了一套新的苹方体设计,所以现有的 iOS 系统,包含了两种苹方,我称之为 「苹方」 & 「新苹方」。区别有以下几点:

  • 版权的所有者不同
  • 新苹方的西文字符调整成了类 SF Pro 的设计

所以同样的flutter代码,在不同的iOS设备中,字体的回退表现也不一致。

此外,新苹方终于支持可变字体了,对比老的苹方(or SF Pro),我们能够明显看出对于字重支持的差异性:

不过嵌入到 iOS 系统中的新苹方,还是以不同字重的静态文件来支持不同字重中文的展示的。使用 Swift UI ,分别指定不同的字体显示同一段文本,我们来看看他们有何区别:

可以看到,字重和字体大小对比系统处理都有所不同,苹果对于 iOS 中苹方字体的支持,确实让人有些摸不着头脑。不过大概可以猜出,iOS 系统中,为了字体排版展示的效果,使用系统字体时,苹果一定对其有一些特殊的处理,这些处理导致了即使是原生页面展示同样的文本,指定PingFang和指定系统字体的显示效果截然不同以及 flutter 的字体比原生更「细」的现象。

苹果类似的设计细节处理其实不少,比如在输入框中,当你同时输入英文和中文时,iOS系统都会自动把中英文之间的间距拉开,不需要手动加空格。

所以,这也解释了为什么设计师都喜欢Apple,因为 Apple 尊重设计。

解决方案

与其说是解决方案,我更愿意称之为这篇文章的总结。由于iOS系统的黑盒机制,我们无法了解具体的文字排版逻辑,所以也就很难实现同样的效果。

  • 等待后续的flutter版本:可能后面 flutter 从Skia 切换到 Impeller后,这个问题就自然解决了
  • 也可能后面系统内置的苹方字体切换为动态字体后也就没问题了。我们能做的,确实不多。

参考