來自:知識小集(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:
NSTextStorage
、NSTextContainer
; - 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
,一個NSLayoutManager
,NSLayoutManager
管理多個NSTextContainer
; - 附帶實時預覽功能的 Markdown 編輯器:一個
NSTextStorage
,多個NSLayoutManager
,每個NSLayoutManager
管理一個NSTextContainer
。
CoreText
以上文本系統稱之為 TextKit
, 而整個 TextKit
基於 CoreText
構建。
目前流行文字框架如 TTTAttributedLabel[2]、YYText[3]都是基於 CoreText
進行開發,並且直接使用 CTFramesstter
相關介面。
CTFramesstter
內部使用 CTTypesetter
進行文字排版,CTTypesetter
可以生成 CTLine
,CTLine
由多個 CTRun
組成,而 CTRun
由具有相同 attributes
的文字組成。最終,多個 CTLine
合成為 CTFrame
。
圖片來源:blog.devtang.com
- 絕大部分場景我們首先都應該基於更高層的
TextKit
進行開發,儘量避免對底層CoreText
的使用。並且需要註意的是,直接使用CoreText
與TextKit
進行渲染的效果是不一致的,這是由於CoreText
與TextKit
的 fix attributes 是不完全一致的,並且它們在排版細節可能也會有差異(依賴於TextKit
的實現);- 由於
TextKit
基於CoreText
,所以無需擔心效能問題,並且其更易使用與擴充套件。
UILabel 的實現
透過 Instruments
檢視 UILabel
的呼叫棧,我們知道其實際基於 TextKit
實現。見下圖:
可以看到 UILabel
首先會呼叫 -[NSConcreteMutableAttributedString fixAttributesInRange:]
,然後使用 _NSStringDawingEngine
進行文字大小計算以及渲染。
並且可以發現,我們常用的文字大小計算方法 -[NSAttributedString(NSExtendedStringDrawing) boundingRectWithSize:options:context:]
也是基於 TextKit
實現。
FixAttributes
TextKit
在進行文字排版之前,都會先對 NSTextStorage
執行 fixAttribtesInRange:
方法。而這個方法可能是非常耗時的,所以有時候也會造成 TextKit
效能不好的假象。
那麼為什麼需要進行這步操作呢?我們觀察到 fixAttribtesInRange:
方法實際執行了另外 3 個方法,分別是:
fixFontAttributeInRange:
fixParagraphStyleAttributeInRane:
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]。
總結
- 介紹了文字排版的基礎:字元、字形、字型,字元 + 字型 -> 字形;
- 介紹了 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
朋友會在“發現-看一看”看到你“在看”的內容