歡迎光臨
每天分享高質量文章

Cocoa 文本系統

來自:知識小集(ID:iOS-Tips)

作者 | Tpphha,目前在美拍 iOS 小組,經常在 GitHub 閑逛,致力於在大前端方向發展,也希望能做出一款有人喜歡的產品。

鏈接:https://juejin.im/post/5cceef41e51d4514df42072b?from=singlemessage&isappinstalled;=0

文字排版

在開始文本系統介紹之前,我們先瞭解一下文字是怎麼排版的,而要瞭解文字的排版就必須先有一些基本概念。

我這裡只做簡單地介紹,具體請參考: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
赞(0)

分享創造快樂