查看原文
其他

Cocoa 文本系统

Tpphha 知识小集 2020-09-02

作者 | Tpphha,目前在美拍 iOS 小组,经常在 GitHub 闲逛,致力于在大前端方向发展,也希望能做出一款有人喜欢的产品。

文字排版

在开始文本系统介绍之前,我们先了解一下文字是怎么排版的,而要了解文字的排版就必须先有一些基本概念。

我这里只做简单地介绍,具体请参考:Typographical Concepts[1]

字符(Characters)与字形(Glyphs)

上图表示的是连字(Ligatures),连字由字符 "f" 以及字符 "l" 组成,它们组合后成为一个字形(Glyph)。
与此类似的还有 "é",它由 "e" 与 "´" 组合而成。

可以看到,字符与字形不一定是一一对应的关系,当然在一般情况下,它们可以看做是一一对应的。

字符编码

计算机通过编码表将字符存储为数字。在 Cocoa 平台的编码方案为 Unicode 标准。Unicode 标准为世界上每种现代书面语言中的每个字符提供了一个惟一的数字,其独立于所使用的平台、程序和编程语言。这个通用标准解决了一个长期存在的问题,即不同的计算机系统使用数百种相互冲突的编码方案。它还具有简化处理双向文本和上下文表单的功能。

字形结构

字体(Fonts)

上面介绍了字符与字形的关系,那么它们的关系具体又是什么呢?这就需要用到字体了。

字符加字体可以得到字形,在 Cocoa 中我们通过字体可以得到 CGGlyph,渲染的时候我们使用 CTFont 的方法传入 CGGlyph 就可以渲染出实际的文字。

文本系统架构

无论是 macOS 还是 iOS,苹果的文本系统的架构都是一样的,如上图所示。
在 Typesetter 以及 Glyph generator 之下是 CoreText,所以系统的整个文本系统是构建在 CoreText 之上。

在 iOS 平台,系统隐藏了 Typesetter、 Glyph generator。

整个系统遵循 MVC 的架构设计:

  • Model:NSTextStorageNSTextContainer

  • View:在 macOS 是 NSTextView,在 iOS 是 UITextView

  • Controller:NSLayoutManager

类职能简介

  • NSTextStorage保存富文本数据;

  • NSTextContainer提供布局区域;

  • TextView真实地展示文本;

  • NSLayoutManager来管理所有的布局以及缓存布局信息,其持有 NSGlyphGenerator NSTypesetter实例,其中 NSGlyphGenerator用来生成 Glyph,NSTypesetter进行具体的排版操作。

NSTypesetter 是一个抽象类,NSLayoutManager 默认使用 NSATSTypesetter(Apple Type Services (ATS))进行排版。

常见配置

一个 NSTextStorage可以配置多个 NSLayoutManager,一个 NSLayoutManager可以配置多个 NSTextContainer,每个 NSTextContainer可以关联一个 NSTextView

所以我们可以很方便地实现这些功能:

  • 电子书阅读器:一个 NSTextStorage,一个 NSLayoutManagerNSLayoutManager管理多个 NSTextContainer

  • 附带实时预览功能的 Markdown 编辑器:一个 NSTextStorage,多个 NSLayoutManager,每个 NSLayoutManager管理一个 NSTextContainer

CoreText

以上文本系统称之为 TextKit, 而整个 TextKit基于 CoreText构建。

目前流行文本框架如 TTTAttributedLabel[2]YYText[3]都是基于 CoreText进行开发,并且直接使用 CTFramesstter相关接口。

CTFramesstter内部使用 CTTypesetter进行文字排版,CTTypesetter可以生成 CTLineCTLine由多个 CTRun组成,而 CTRun由具有相同 attributes的文字组成。最终,多个 CTLine合成为 CTFrame

图片来源:blog.devtang.com

  1. 绝大部分场景我们首先都应该基于更高层的 TextKit 进行开发,尽量避免对底层 CoreText 的使用。并且需要注意的是,直接使用 CoreText  TextKit 进行渲染的效果是不一致的,这是由于 CoreText  TextKit 的 fix attributes 是不完全一致的,并且它们在排版细节可能也会有差异(依赖于 TextKit 的实现);

  2. 由于 TextKit 基于 CoreText,所以无需担心性能问题,并且其更易使用与扩展。

UILabel 的实现

通过 Instruments查看 UILabel的调用栈,我们知道其实际基于 TextKit实现。见下图:

可以看到 UILabel首先会调用 -[NSConcreteMutableAttributedString fixAttributesInRange:],然后使用 _NSStringDawingEngine进行文本大小计算以及渲染。

并且可以发现,我们常用的文本大小计算方法 -[NSAttributedString(NSExtendedStringDrawing) boundingRectWithSize:options:context:]也是基于 TextKit实现。

FixAttributes

TextKit在进行文本排版之前,都会先对 NSTextStorage执行 fixAttribtesInRange:方法。而这个方法可能是非常耗时的,所以有时候也会造成 TextKit性能不好的假象。

那么为什么需要进行这步操作呢?我们观察到 fixAttribtesInRange:方法实际执行了另外 3 个方法,分别是:

  1. fixFontAttributeInRange:

  2. fixParagraphStyleAttributeInRane:

  3. fixGlyphInfoAttributeInRange:

结合文档 fixAttribtesInRange 方法介绍[4],我们知道,其只要是为了修复一些不正常 attributes,例如:

  • 文字设置了不正确的字体,例如不能为汉字和阿拉伯字符分配Times-Roman字体,修复后会为它设置适合的字体;

  • 为非 NSAttachmentCharacter添加了 NSAttachmentAttribute,修复后会删除掉错误的 NSAttachmentAttribute

  • etc.


请注意:TextKit fallback 到其他的字体,系统会为 NSTextStorage 添加 key 为 NSOriginalFont,value 为原始字体的 attribute ,但是排版依然会使用原始字体进行排版,也就是说文本计算的大小依然是使用原始字体计算。

TextKit 踩坑

  • UILabel当只有一行时候如果设置了 linespacing,linespacing 仍然会生效,这种场景其实我们是不希望有多余的 linespacing;

  • UILabel没有使用 FontLeading进行排版;

  • 不能自定义截断文本(TruncationToken),系统内部默认截断文本为三个点:UTF16Char ellipsis = 0x2026,不过能参考 Texture[5]实现自定义截断;

  • 直接使用 TextKit,当 NSTextContainer设置了 maxNumberOfLines 文本产生截断的时候,同 UILabel,最后一行会有多余的 linespacing,解决方案参考:Neat[6]

总结

  1. 介绍了文字排版的基础:字符、字形、字体,字符 + 字体 -> 字形;

  2. 介绍了 Cocoa 文本系统 TextKit的架构,系统遵循 MVC 的架构:

  • Model:`NSTextStorage` 保存富文本数据,`NSTextContainer` 提供布局区域;

  • View:在 macOS 是 `NSTextView`,在 iOS 是 `UITextView`,负责真实地展示文本;

  • Controller:`NSLayoutManager`,负责文本布局的管理。

  • 介绍了 TextKit的底层技术支持:CoreText,它是先进的布局文本和处理字体的底层技术;

  • 介绍了 UILabel,其内部实现基于 TextKit提供的高性能、高质量排版引擎;

  • 介绍了为什么需要 FixAttributes

  • 介绍了使用 TextKit的一些踩坑经历以及其对应的解决方案。

  • 参考文档

    • [1]https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TypoFeatures/TextSystemFeatures.html#//apple_ref/doc/uid/TP40009459-CH6-64585

    • [2]https://github.com/TTTAttributedLabel/TTTAttributedLabel

    • [3]https://github.com/ibireme/YYText

    • [4]https://developer.apple.com/documentation/foundation/nsmutableattributedstring/1533823-fixattributesinrange

    • [5]https://github.com/texturegroup/texture

    • [6]https://github.com/leavez/Neat

    • [7]https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/CoreText_Programming/Introduction/Introduction.html#//apple_ref/doc/uid/TP40005533-CH1-SW1

    • [8]https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextAttributes/TextAttributes.html#//apple_ref/doc/uid/10000088-SW1

    • [9]https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Introduction/Introduction.html#//apple_ref/doc/uid/TP40009459

    • [10]https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextLayout/TextLayout.html#//apple_ref/doc/uid/10000158-SW1

    • [11]https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/Introduction/Introduction.html#//apple_ref/doc/uid/TP40009542-CH1-SW1

    推荐阅读

    BlockHook with Struct

    端内外融合拉新,用户增长 -- 相关技术方案选型分析

    AppHost:大前端融合下的 Hybrid 开发解决方案

    在看就点点吧 

      您可能也对以下帖子感兴趣

      文章有问题?点此查看未经处理的缓存