系列文章:
YYText 源碼剖析:CoreText 與異步繪制
YYAsyncLayer 源碼剖析:異步繪制
YYCache 源碼剖析:一覽亮點(diǎn)
YYModel 源碼剖析:關(guān)注性能
YYImage 源碼剖析:圖片處理技巧
YYWebImage 源碼剖析:線程處理與緩存策略
前言
YYText 是業(yè)界知名富文本框架,基于 CoreText 做了大量基礎(chǔ)設(shè)施并且實(shí)現(xiàn)了兩個(gè)上層視圖組件:YYLabel 和 YYTextView。同其它 YYKit 組件一樣隙笆,YYText 在性能方面表現(xiàn)優(yōu)異,且功能出奇的強(qiáng)大溢豆,可以說(shuō)是業(yè)界巔峰之作。
提起 YYText,都知道它的核心優(yōu)化點(diǎn):異步繪制恭金,然而這只是冰山一角坟瓢,YYText 中最為復(fù)雜和篇幅最多的是基于 CoreText 的各種計(jì)算勇边,不得不說(shuō),源碼中大量的計(jì)算很容易讓人眼花繚亂折联。
若想深入理解 YYText 或者看懂本文粒褒,必須要了解 CoreText 基礎(chǔ)知識(shí)并且有足夠的耐心〕狭框架代碼量非常大奕坟,本文主要講解框架基于 CoreText 的底層基礎(chǔ)部分,不會(huì)過(guò)多的講解 YYLabel 和 YYTextView 的細(xì)節(jié)清笨。
一月杉、框架總覽
iOS UI 組件大都必須在主線程繪制,當(dāng)繪制壓力過(guò)大會(huì)造成界面卡頓抠艾,得益于多線程技術(shù)沙合,我們可以在異步線程繪制圖形從而減輕主線程壓力。
YYText 核心思路:在異步線程創(chuàng)建圖形上下文,然后利用 CoreText 繪制富文本首懈,利用 CoreGraphics 繪制圖片绊率、陰影、邊框等究履,最后將繪制完成的位圖放到主線程顯示滤否。
步驟看起來(lái)很簡(jiǎn)單,源碼中涉及到 CoreText 和 CoreGraphics 的繪制時(shí)需要大量的代碼來(lái)計(jì)算位置最仑,這也是本文的重點(diǎn)之一藐俺。為了簡(jiǎn)潔易懂,筆者會(huì)略過(guò)一些技術(shù)細(xì)節(jié)泥彤,比如縱向文本布局邏輯欲芹,一些奇怪的 BUG 修復(fù)代碼。
希望讀者朋友優(yōu)先了解 CoreText 基礎(chǔ) (CoreText 官方介紹)吟吝,這里放上兩個(gè)結(jié)構(gòu)圖便于理解(圖會(huì)有偏差):
二菱父、CoreText 相關(guān)工具類
1、YYTextRunDelegate
在富文本中插入 key 為kCTRunDelegateAttributeName
的CTRunDelegateRef
實(shí)例可以定制一段區(qū)域的大小剑逃,通常使用這個(gè)方式來(lái)預(yù)留出一段空白浙宜,后面可以填充圖片來(lái)達(dá)到圖文混排的效果。而創(chuàng)建CTRunDelegateRef
需要一系列的函數(shù)名蛹磺,使用繁瑣粟瞬,框架使用一個(gè)類來(lái)封裝以減小使用成本:
@interface YYTextRunDelegate : NSObject <NSCopying, NSCoding>
...
@property (nonatomic) CGFloat ascent;
@property (nonatomic) CGFloat descent;
@property (nonatomic) CGFloat width;
@end
static void DeallocCallback(void *ref) {
YYTextRunDelegate *self = (__bridge_transfer YYTextRunDelegate *)(ref);
self = nil; // release
}
static CGFloat GetAscentCallback(void *ref) {
YYTextRunDelegate *self = (__bridge YYTextRunDelegate *)(ref);
return self.ascent;
}
...
@implementation YYTextRunDelegate
- (CTRunDelegateRef)CTRunDelegate CF_RETURNS_RETAINED {
CTRunDelegateCallbacks callbacks;
callbacks.dealloc = DeallocCallback;
callbacks.getAscent = GetAscentCallback;
...
return CTRunDelegateCreate(&callbacks, (__bridge_retained void *)(self.copy));
}
...
使用CTRunDelegateCreate()
創(chuàng)建一個(gè)CTRunDelegateRef
,同時(shí)使用__bridge_retained
轉(zhuǎn)移內(nèi)存管理萤捆,持有一個(gè)YYTextRunDelegate
對(duì)象裙品。在該類中有數(shù)個(gè)靜態(tài)函數(shù)作為回調(diào),比如當(dāng)回調(diào)GetAscentCallback()
函數(shù)時(shí)俗或,將持有對(duì)象的ascent
屬性作為返回值市怎。
注意一:這樣做似乎存在內(nèi)存管理問(wèn)題,CTRunDelegateRef
實(shí)例持有的YYTextRunDelegate
對(duì)象如何釋放蕴侣?
答案就在CTRunDelegateRef
釋放時(shí)會(huì)走的DeallocCallback()
回調(diào)中,將內(nèi)存管理權(quán)限轉(zhuǎn)移給一個(gè)YYTextRunDelegate
局部變量自動(dòng)管理內(nèi)存臭觉。
注意二:可以看到CTRunDelegateCreate(&callbacks, (__bridge_retained void *)(self.copy))
代碼對(duì)self
做了一個(gè)copy
操作 (該類的 copy 為深拷貝) 昆雀,這樣做是為了什么呢?
可能第一反應(yīng)是想到CTRunDelegateRef
持有self
的副本是為了避免循環(huán)引用蝠筑,然而該方法并沒(méi)有讓self
持有CTRunDelegateCreate()
后的實(shí)例狞膘,所以也不存在循環(huán)引用問(wèn)題。
實(shí)際上這里應(yīng)該只是創(chuàng)建一個(gè)副本什乙,當(dāng)該方法返回后保證配置數(shù)據(jù)的安全性 (避免被外部意外更改)挽封。
2、YYTextLine
創(chuàng)建一個(gè)富文本臣镣,可以拿到CTLineRef
和CTRunRef
以及一些結(jié)構(gòu)數(shù)據(jù) (比如ascent descent
等)辅愿,CTRunRef
包含的數(shù)據(jù)內(nèi)容并不是很多智亮,所以框架沒(méi)有專門(mén)做一個(gè)類來(lái)包裝它。使用YYTextLine
來(lái)包裝CTLineRef
計(jì)算保存一些數(shù)據(jù)便于后面的計(jì)算点待,比如使用CTLineGetTypographicBounds(...);
方法來(lái)拿到ascent descent leading
等阔蛉。
計(jì)算 line 位置和大小
_bounds = CGRectMake(_position.x, _position.y - _ascent, _lineWidth, _ascent + _descent);
_bounds.origin.x += _firstGlyphPos;
_position
是指 line 的origin
點(diǎn)位于context
上下文的坐標(biāo)轉(zhuǎn)換為UIKit
坐標(biāo)系的值,那么結(jié)合上面的結(jié)構(gòu)圖2分析:_position.y - _ascent
就是 line 的最小y
值癞埠,_ascent + _descent
就是 line 高度(沒(méi)有算上行間距 leading)状原。
這里最小x
值加了一個(gè)_firstGlyphPos
,它是當(dāng)前 line 第一個(gè) run 相對(duì)于 line 的偏移苗踪,通過(guò)CTRunGetPositions(...);
算出颠区,可能有一種場(chǎng)景,line 的origin
位置與第一個(gè) run 的位置有偏移(筆者并沒(méi)有模擬出這種情況)通铲。
找出所有的占位 run
實(shí)際上這就是找出之前說(shuō)的CTRunDelegateRef
毕莱,框架每一個(gè)CTRunDelegateRef
都對(duì)應(yīng)了一個(gè)YYTextAttachment
,它表示一個(gè)附件(圖片测暗、UIView央串、CALayer),具體實(shí)現(xiàn)后面會(huì)單獨(dú)講碗啄。這里只需要知道基本原理就是用CTRunDelegateRef
占位质和,用YYTextAttachment
填充。
當(dāng)遍歷 line 里面的 run 時(shí)稚字,若該 run 包含了YYTextAttachment
說(shuō)明這是占位 run饲宿,那么至關(guān)重要的一步是計(jì)算這個(gè) run 的位置和大小(便于后面將附件填充到正確位置)胆描。
runPosition.x += _position.x;
runPosition.y = _position.y - runPosition.y;
runTypoBounds = CGRectMake(runPosition.x, runPosition.y - ascent, runWidth, ascent + descent);
_position
上面已經(jīng)說(shuō)明了意義瘫想,runPosition
是當(dāng)前 run 相對(duì)于當(dāng)前 line origin
的偏移,那么runPosition.x + _position.x
表示了 run 相對(duì)于圖形上下文的x
方向位置昌讲,后面同理国夜。
最終,將這個(gè)YYTextAttachment
附件對(duì)象和 run 位置大小信息緩存起來(lái)(后面會(huì)專門(mén)分析實(shí)現(xiàn)邏輯)短绸。
3车吹、YYTextContainer
創(chuàng)建CTFrameRef
使用CTFramesetterCreateFrame(...)
方法,這個(gè)方法需要一個(gè)CGPathRef
參數(shù)醋闭,為了使用簡(jiǎn)便窄驹,框架抽象了一個(gè)YYTextContainer
類重點(diǎn)屬性如下:
@property CGSize size;
@property UIEdgeInsets insets;
@property (nullable, copy) UIBezierPath *path;
@property (nullable, copy) NSArray<UIBezierPath *> *exclusionPaths;
使用者可以簡(jiǎn)單的使用CGSize
來(lái)制定富文本的大小,也可以用內(nèi)存自動(dòng)管理功能強(qiáng)大的UIBezierPath
來(lái)制定路徑证逻,同時(shí)包含一個(gè)exclusionPaths
排除路徑乐埠。
┌─────────────────────────────┐ <------- container
│ │
│ asdfasdfasdfasdfasdfa <------------ container insets
│ asdfasdfa asdfasdfa │
│ asdfas asdasd │
│ asdfa <----------------------- container exclusion path
│ asdfas adfasd │
│ asdfasdfa asdfasdfa │
│ asdfasdfasdfasdfasdfa │
│ │
└─────────────────────────────┘
CoreText 是支持鏤空效果的,就是由這個(gè) exclusion path 控制。該類的屬性訪問(wèn)都是線程安全的丈咐,還做了一些精致的容錯(cuò)瑞眼。
三、YYTextLayout 核心計(jì)算類
YYTextLayout
包含了布局一個(gè)富文本幾乎所有的信息扯罐,同時(shí)還將眾多的繪制相關(guān) C 代碼放在了這個(gè)文件里面负拟,所以這個(gè)文件非常龐大。我們先不管這些繪制代碼歹河,YYTextLayout
主要的作用是計(jì)算各種數(shù)據(jù)掩浙,為后面的繪制做準(zhǔn)備。
核心計(jì)算在+ (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range;
初始化方法中秸歧,這個(gè)方法為后面的各種查詢計(jì)算打下了數(shù)據(jù)基礎(chǔ)厨姚,接下來(lái)就分析一下這個(gè)超過(guò) 500 行的初始化方法做了些什么。
1键菱、計(jì)算繪制路徑和路徑的位置矩形
基于YYTextContainer
對(duì)象計(jì)算得到CGPathRef
是主要邏輯谬墙,為了避免矩陣屬性出現(xiàn)負(fù)值,使用CGRectStandardize(...)
來(lái)矯正经备。由于 UIKit 和 CoreText 坐標(biāo)系的差別拭抬,最終得到的矩陣要先做一個(gè)坐標(biāo)系翻轉(zhuǎn):
rect = CGRectApplyAffineTransform(rect, CGAffineTransformMakeScale(1, -1));
cgPath = CGPathCreateWithRect(rect, NULL);
或者
CGAffineTransform trans = CGAffineTransformMakeScale(1, -1);
CGMutablePathRef transPath = CGPathCreateMutableCopyByTransformingPath(path, &trans);
它們道理是一樣的,都是沿著 x 軸翻轉(zhuǎn)坐標(biāo)系 180°侵蒙,可能有人有疑問(wèn)造虎,UIKit 轉(zhuǎn)換為 CoreText 坐標(biāo)系不是除了翻轉(zhuǎn) 180°,還要移動(dòng)一個(gè)繪制區(qū)域高度么纷闺?確實(shí)這里少做了一個(gè)操作算凿,那是因?yàn)榭蚣苁鞘褂?code>CTRunDraw(...)遍歷繪制 run,在繪制 run 之前會(huì)用CGContextSetTextPosition(...)
指定位置(這個(gè)位置是 line 相對(duì)于繪制區(qū)域計(jì)算的)犁功,所以這個(gè)地方的 y 坐標(biāo)是否正確已經(jīng)沒(méi)有意義了氓轰。
繪制路徑的矩形大小位置pathBox
的計(jì)算:
比如這種情況,
pathBox = (CGRect){50, 50, 100, 100}
浸卦,可想而知pathBox
指的就是真正繪制區(qū)域相對(duì)于繪制上下文的位置和大小署鸡,這個(gè)數(shù)據(jù)非常有用,意味著后面計(jì)算 line 和 run 的位置時(shí)限嫌,都要加上 cgPathBox.origin
偏移靴庆,才能真正表示 line 和 run 相對(duì)于繪制上下文的位置(比如 line 的origin
是相對(duì)于繪制區(qū)域的一個(gè)點(diǎn),而不是相對(duì)于繪制上下文)萤皂。
2撒穷、初始化 CTFramesetterRef 和 CTFrameRef
這一步很簡(jiǎn)單匣椰,利用兩個(gè)函數(shù)就搞定:CTFramesetterCreateWithAttributedString(...) CTFramesetterCreateFrame(...)
裆熙。值得注意的是框架支持了幾個(gè) CTFrameRef 的屬性,比如kCTFramePathWidthAttributeName
,這些屬性同樣是通過(guò)YYTextContainer
配置的入录。
3蛤奥、計(jì)算 line 總 frame 和行數(shù)
前面已經(jīng)創(chuàng)建了一個(gè)富文本CTFrameRef
,那么這里只需要遍歷所有的 line 做計(jì)算僚稿,可以看到如下代碼獲取每一個(gè) line 的位置大蟹睬拧:
// CoreText coordinate system
CGPoint ctLineOrigin = lineOrigins[i];
// UIKit coordinate system
CGPoint position;
position.x = cgPathBox.origin.x + ctLineOrigin.x;
position.y = cgPathBox.size.height + cgPathBox.origin.y - ctLineOrigin.y;
YYTextLine *line = [YYTextLine lineWithCTLine:ctLine position:position vertical:isVerticalForm];
CGRect rect = line.bounds;
lineOrigins
是通過(guò)CTFrameGetLineOrigins(...)
得到的,所以需要轉(zhuǎn)換為 UIKit 坐標(biāo)系方便計(jì)算蚀同∶骞簦可以看到轉(zhuǎn)換時(shí)做了一個(gè)cgPathBox.origin
的偏移,這就是之前計(jì)算的實(shí)際繪制矩形的偏移蠢络,以此得到的position
就是相對(duì)于圖形上下文的點(diǎn)了衰猛,然后利用這個(gè)點(diǎn)初始化YYTextLine
,前面講了YYTextLine
的內(nèi)部實(shí)現(xiàn)刹孔,這里就直接得到了當(dāng)前 line 的位置和大蟹仁 :rect
。
然后髓霞,利用CGRectUnion(...)
函數(shù)將每一個(gè) line 的rect
合并起來(lái)卦睹,得到一個(gè)包含所有 line 的最小位置矩形textBoundingRect
。
計(jì)算 line 的行數(shù)
并不是一個(gè) line 就占有一行方库,當(dāng)有排除路徑時(shí)结序,一行可能有兩個(gè) line:所以,需要計(jì)算每個(gè) line 所在的行薪捍,便于為后續(xù)的很多計(jì)算提供基礎(chǔ)笼痹,比如最大行限制。
當(dāng)當(dāng)前 line 的高度大于 last line 的高度時(shí)酪穿,若當(dāng)前 line 的 y0 在 baseline 以上凳干,y1 在 baseline 以下,就說(shuō)明沒(méi)有換行被济。
當(dāng)當(dāng)前 line 的高度小于 last line 的高度時(shí)救赐,若 last line 的 y0 在 baseline 以上,y1 在 baseline 以下只磷,就說(shuō)明沒(méi)有換行经磅。
4、獲取行上下邊界數(shù)組
typedef struct {
CGFloat head;
CGFloat foot;
} YYRowEdge;
聲明了一個(gè)YYRowEdge *lineRowsEdge = NULL;
數(shù)組钮追,YYRowEdge
表示每一行的上下邊界预厌。計(jì)算邏輯大致是這樣的:
遍歷所有 line,當(dāng)當(dāng)前 line 和 last line 為同一行時(shí)元媚,取 line 和 last line 共同的最大上下邊界:
lastHead = MIN(lastHead, rect.origin.y);
lastFoot = MAX(lastFoot, rect.origin.y + rect.size.height);
當(dāng)當(dāng)前 line 和 last line 為不同行時(shí)轧叽,取當(dāng)前 line 的上下邊界:
lastHead = rect.origin.y;
lastFoot = lastHead + rect.size.height;
最終的結(jié)果可能是這樣的:
foot1
和head2
之間會(huì)存在一個(gè)間隙苗沧,這個(gè)間隙就是行間距,框架的處理是將這個(gè)間隙均分:
5炭晒、計(jì)算繪制區(qū)域總大小
上面已經(jīng)計(jì)算了繪制路徑的位置矩形pathBox
待逞,這只是實(shí)際繪制區(qū)域的大小,業(yè)務(wù)中若設(shè)置了YYTextContainer
的線寬或者邊距网严,那么實(shí)際業(yè)務(wù)需要的繪制區(qū)域總大小會(huì)更大:
圖中藍(lán)色填充區(qū)域即為實(shí)際繪制區(qū)域pathBox
识樱,繪制區(qū)域總大小應(yīng)該是藍(lán)色邊框所覆蓋的范圍(請(qǐng)忽略線與線之間的小縫隙)。借助CGRectInset(...) UIEdgeInsetsInsetRect(...)
等函數(shù)能輕易的計(jì)算出來(lái)震束,同樣的需要用CGRectStandardize(...)
糾正負(fù)值怜庸。
6、line 截?cái)?/h3>
當(dāng)富文本超過(guò)限制時(shí)垢村,可能需要對(duì)最后一行可顯示的行末尾做一個(gè)省略號(hào):aaaa...
休雌。
首先有一個(gè)NSAttributedString *truncationToken;
,這個(gè) token 可以自定義肝断,框架也有默認(rèn)的杈曲,就是一個(gè)...
省略號(hào),然后將這個(gè)truncationToken
拼接到最后一個(gè)line
:
NSMutableAttributedString *lastLineText = [text attributedSubstringFromRange:lastLine.range].mutableCopy;
[lastLineText appendAttributedString:truncationToken];
當(dāng)然胸懈,這樣lastLineText
肯定會(huì)超過(guò)繪制區(qū)域的范圍担扑,所以要使用系統(tǒng)提供的方法CTLineCreateTruncatedLine(...)
來(lái)創(chuàng)建自動(dòng)計(jì)算的截?cái)?line,該方法返回一個(gè)CTLineRef
趣钱,這里轉(zhuǎn)換為YYTextLine
并且作為YYTextLayout
的一個(gè)屬性truncatedLine
涌献。
這也就意味著,YYText 的截?cái)嗫偸窃诟晃谋咀詈蟮氖子校抑挥幸粋€(gè)燕垃。
7、緩存各種 BOOL 值
遍歷富文本對(duì)象井联,緩存一系列的 BOOL 值:
void (^block)(NSDictionary *attrs, NSRange range, BOOL *stop) = ^(NSDictionary *attrs, NSRange range, BOOL *stop) {
if (attrs[YYTextHighlightAttributeName]) layout.containsHighlight = YES;
if (attrs[YYTextBlockBorderAttributeName]) layout.needDrawBlockBorder = YES;
if (attrs[YYTextBackgroundBorderAttributeName]) layout.needDrawBackgroundBorder = YES;
if (attrs[YYTextShadowAttributeName] || attrs[NSShadowAttributeName]) layout.needDrawShadow = YES;
...
};
[layout.text enumerateAttributesInRange:visibleRange options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block];
可以猜測(cè)卜壕,YYTextBlockBorderAttributeName
等就是 YYText 定制的富文本屬性,在初始化YYTextLayout
時(shí)就將富文本中是否包含自定義 key 緩存起來(lái)烙常。
想象一下轴捎,若此處不使用這些 BOOL 值,那么在繪制的時(shí)候框架也需要去遍歷查找是否有自定義的 key蚕脏,若有再執(zhí)行自定義的繪制邏輯侦副。也就是說(shuō),這個(gè)遍歷是必須要做的驼鞭,要么在初始化時(shí)做秦驯,要么是繪制的時(shí)候做。
按照框架的設(shè)定挣棕,初始化YYTextLayout
和繪制都可以在主線程也可以在異步繪制執(zhí)行译隘,所以這里的目的主要不是為了將這個(gè)遍歷邏輯放入異步線程葱蝗,而是為了緩存。
初始化YYTextLayout
時(shí)緩存這些 BOOL 值過(guò)后细燎,二次繪制就不需要再遍歷了,以此達(dá)到優(yōu)化性能的目的皂甘。
8玻驻、合并所有的附件
前面有講到,YYTextLine
初始化時(shí)會(huì)將所有的附件及其相關(guān)位置信息裝到數(shù)組里面偿枕,那么這里遍歷所有的 line 將附件相關(guān)數(shù)組合并到一起璧瞬,那么之后的繪制就不需要再去遍歷 line 獲取附件了。
9渐夸、小結(jié)
除開(kāi)YYTextLayout
初始化方法嗤锉,還有在#pragma mark - Query
標(biāo)記下的一系列查詢方法,這些查詢方法都是基于上面的初始化計(jì)算數(shù)據(jù)墓塌。至于#pragma mark - Draw
標(biāo)記下的繪制相關(guān)方法后面再說(shuō)瘟忱。
YYTextLayout
初始化方法非常的長(zhǎng),筆者試圖將這個(gè)方法分解一下苫幢,發(fā)現(xiàn)這樣會(huì)更復(fù)雜访诱。原因是這個(gè)初始化方法里面包含了眾多的需要手動(dòng)管理的內(nèi)存,比如CGPathRef CTFramesetterRef CTFrameRef
等韩肝。
可能有人會(huì)說(shuō)触菜,哪個(gè)地方需要引用計(jì)數(shù)減一,手動(dòng)release
不就行了哀峻?
但是實(shí)際情況更加復(fù)雜涡相,因?yàn)檎麄€(gè)初始化過(guò)程隨時(shí)可能會(huì)被中斷。比如calloc(...)
開(kāi)辟內(nèi)存可能會(huì)失敗剩蟀,CGPathCreateMutableCopy(...)
創(chuàng)建路徑可能會(huì)失敗催蝗,所以,在任何情況失敗需要中斷初始化時(shí)育特,大概會(huì)如下寫(xiě):
if (failed) {
CFRelease(...);
free(...);
...
return nil;
}
而且這個(gè)地方你必須要將前面所有手動(dòng)管理的內(nèi)存釋放掉生逸,當(dāng)這個(gè)代碼過(guò)多的時(shí)候,可能會(huì)讓你瘋掉且预。
所以作者用了一個(gè)很巧的方法槽袄,使用goto
:
fail:
if (cgPath) CFRelease(cgPath);
if (lineOrigins) free(lineOrigins);
...
return nil;
那么,當(dāng)某個(gè)環(huán)節(jié)失敗時(shí)锋谐,直接這么寫(xiě):
if (failed) {
goto fail;
}
這個(gè)場(chǎng)景下遍尺,goto
的使用確實(shí)非常適合。
四涮拗、自定義富文本屬性
我們知道乾戏,NSMutableAttributedString
對(duì)象使用addAttribute:value:range:
等一系列方法可以添加富文本效果迂苛,這些效果有三個(gè)要素:名字 (key)、值 (value)鼓择、范圍三幻。YYText 也拓展了一些自己的名字 (YYTextAttribute 文件):
UIKIT_EXTERN NSString *const YYTextAttachmentAttributeName;
UIKIT_EXTERN NSString *const YYTextHighlightAttributeName;
...
當(dāng)然為這些 key 都創(chuàng)建了對(duì)應(yīng)的 value (類),比如YYTextHighlightAttributeName
對(duì)應(yīng)YYTextHighlight
呐能。但是這些自定義的 key CoreText 是識(shí)別不了的念搬,那么框架內(nèi)部是如何處理的呢?
NSDictionary *attrs = (id)CTRunGetAttributes(run);
id anyValue = attrs[anyKey];
if (anyValue) { ... }
很簡(jiǎn)單摆出,實(shí)際上就是遍歷富文本朗徊,通過(guò)上面這段代碼就能找到某個(gè) run 是否包含自定義的 key,然后做相應(yīng)的繪制邏輯偎漫。
1爷恳、圖文混排實(shí)現(xiàn)
YYText 大部分的自定義屬性都算是“裝飾”文本,所以只需要繪制的時(shí)候判斷有沒(méi)有包含對(duì)應(yīng)的 key象踊,若包含就做相應(yīng)的繪制邏輯温亲。但是有一個(gè)自定義屬性比較特殊:
YYTextAttachmentAttributeName : YYTextAttachment
因?yàn)檫@個(gè)是添加一個(gè)附件 (UIImage、UIView杯矩、CALayer)铸豁,所以需要一個(gè)空位,那么設(shè)置這個(gè)自定義屬性的時(shí)候還需要設(shè)置一個(gè)CTRunDelegateRef
:
NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:YYTextAttachmentToken];
YYTextAttachment *attach = [YYTextAttachment new];
attach.content = content; // UIImage菊碟、UIView节芥、CALayer
...
[atr yy_setTextAttachment:attach range:NSMakeRange(0, atr.length)];
YYTextRunDelegate *delegate = [YYTextRunDelegate new];
...
CTRunDelegateRef delegateRef = delegate.CTRunDelegate;
[atr yy_setRunDelegate:delegateRef range:NSMakeRange(0, atr.length)];
(1) 對(duì)齊方式
圖文混排添加圖片時(shí),業(yè)務(wù)中往往有很多對(duì)齊方式逆害,如何來(lái)對(duì)齊通過(guò)調(diào)整CTRunDelegateRef
的ascent descent
來(lái)控制头镊,框架對(duì)其方式有三種:居上,居下魄幕,居中相艇。
讓占位 run 的ascent
始終等于文本的ascent
(若占位 run 太矮則貼著 baseline) 。
讓占位 run 的descent
始終等于文本的descent
(若占位 run 太矮則貼著 baseline) 纯陨。
居中的計(jì)算相對(duì)復(fù)雜坛芽,需要讓占位 run 的中點(diǎn)和文本的中點(diǎn)對(duì)齊 (如圖),那么圖中yOffset + (占位 run 的 height) * 0.5
就等于占位 run 的ascent
(若占位 run 太矮則貼著 baseline) 翼抠。
當(dāng)然咙轩,上面圖中的圖片可以為UIView CALayer
。到目前為止阴颖,占位 run 的位置已經(jīng)確定了活喊,接下來(lái)就需要把 UIImage UIView CALayer
繪制到相應(yīng)的空位上了。
(2) 繪制附件
繪制的邏輯在YYTextLayout
下的方法YYTextDrawAttachment(...)
量愧,對(duì)于UIImage
圖片的附件钾菊,還能設(shè)置UIViewContentMode
帅矗,會(huì)根據(jù)一開(kāi)始設(shè)置的占位 run 的大小做圖片填充變化,然后調(diào)用 CoreGraphics API 繪制圖片:
CGImageRef ref = image.CGImage;
if (ref) {
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0, CGRectGetMaxY(rect) + CGRectGetMinY(rect));
CGContextScaleCTM(context, 1, -1);
CGContextDrawImage(context, rect, ref);
CGContextRestoreGState(context);
}
若附件的類型是UIView CALayer
煞烫,那分別就需要額外的傳入父視圖浑此、父 layer:targetView targetLayer
,然后的操作就是簡(jiǎn)單的將UIView
添加到targetView
上或者將CALayer
添加到targetLayer
上滞详。
2凛俱、點(diǎn)擊高亮實(shí)現(xiàn)
YYTextHighlightAttributeName : YYTextHighlight
YYTextHighlight
包含了單擊和長(zhǎng)按的回調(diào),還包括一些屬性配置茵宪。在YYLabel
中,通過(guò)下列方法來(lái)寫(xiě)觸發(fā)邏輯:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
涉及到判斷點(diǎn)擊的CGPoint
點(diǎn)對(duì)應(yīng)富文本中的具體位置瘦棋,所以有很多復(fù)雜的計(jì)算稀火,這里不展開(kāi)了。
當(dāng)找到了應(yīng)該觸發(fā)的YYTextHighlight
赌朋,更換具體的YYTextLine
為高亮狀態(tài)的YYTextLine
凰狞,然后重繪。當(dāng)手松開(kāi)時(shí)沛慢,切換會(huì)常態(tài)下的YYTextLine
赡若。
這就是點(diǎn)擊高亮的實(shí)現(xiàn)原理,實(shí)際上就是替換YYTextLine
更新布局团甲。
五逾冬、異步繪制
上面介紹了幾種特殊的自定義富文本屬性,對(duì)于其它的自定義屬性躺苦,基本上都是使用 CoreGraphics API 繪制身腻,比如邊框、陰影等匹厘,當(dāng)然 CoreText 自帶有很多效果嘀趟,YYText 做了一些改良和拓展。
可以看到繪制方法都會(huì)帶有一個(gè)是否取消的 Block愈诚,比如static void YYTextDrawShadow(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void));
她按。這個(gè)cancel
就是用來(lái)判斷是否需要取消本次繪制,這樣就能在一次繪制的任意位置中斷炕柔,及時(shí)的取消無(wú)用的繪制任務(wù)以提高效率酌泰。
YYText 富文本可以異步繪制,也可以在主線程繪制匕累,創(chuàng)建布局類及其相關(guān)計(jì)算可以在任意線程宫莱,可以根據(jù)業(yè)務(wù)需求選擇適合的策略。
具體實(shí)現(xiàn)有些復(fù)雜哩罪,所以關(guān)于異步繪制的具體原理可以看筆者專門(mén)的一篇博客:
YYAsyncLayer 源碼剖析:異步繪制
YYAsyncLayer 就是從 YYText 里面提取出來(lái)的組件授霸,核心就是一個(gè)支持異步繪制的CALayer
子類巡验,相信看完 YYAsyncLayer 的解析會(huì)對(duì)異步繪制有較深的認(rèn)識(shí)。
后語(yǔ)
YYText 確實(shí)過(guò)于重量碘耳,本文只是對(duì)基礎(chǔ)部分取重點(diǎn)做了解析显设,除此之外還有非常多的計(jì)算和邏輯,感興趣可以自行研究辛辨。
從代碼質(zhì)量來(lái)看捕捂,YYText 幾乎無(wú)可挑剔,細(xì)節(jié)處理非常棒斗搞,邏輯代碼很精煉指攒,筆者嘗試過(guò)重寫(xiě)部分邏輯代碼,發(fā)現(xiàn)優(yōu)化半天又回到了源碼的寫(xiě)法 ??僻焚,不得不佩服作者的功底允悦。
至此,筆者已經(jīng)閱讀了 YYKit 大部分源碼虑啤,曾多次被作者的代碼技巧所折服隙弛,幾乎每一句代碼都經(jīng)得起推敲,筆者也更加深刻的理解了性能優(yōu)化狞山,明白了優(yōu)化要從細(xì)節(jié)做起全闷。
突然想起了筆者和一位好友的笑梗。每逢佳時(shí):
“這確實(shí)是一個(gè)非常巧妙且令人興奮的技巧”萍启。