YYText 源碼剖析:CoreText 與異步繪制

系列文章:
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é)清笨。

一月杉、框架總覽

YYText GitHub

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ì)有偏差):

結(jié)構(gòu)圖1
結(jié)構(gòu)圖2

二菱父、CoreText 相關(guān)工具類

1、YYTextRunDelegate

在富文本中插入 key 為kCTRunDelegateAttributeNameCTRunDelegateRef實(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è)富文本臣镣,可以拿到CTLineRefCTRunRef以及一些結(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é)果可能是這樣的:


foot1head2之間會(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)整CTRunDelegateRefascent 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è)非常巧妙且令人興奮的技巧”萍启。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末总珠,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子勘纯,更是在濱河造成了極大的恐慌姚淆,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件屡律,死亡現(xiàn)場(chǎng)離奇詭異腌逢,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)超埋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)搏讶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人霍殴,你說(shuō)我怎么就攤上這事媒惕。” “怎么了来庭?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵妒蔚,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng)肴盏,這世上最難降的妖魔是什么科盛? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮菜皂,結(jié)果婚禮上贞绵,老公的妹妹穿的比我還像新娘。我一直安慰自己恍飘,他們只是感情好榨崩,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著章母,像睡著了一般母蛛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上乳怎,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天彩郊,我揣著相機(jī)與錄音,去河邊找鬼舞肆。 笑死焦辅,一個(gè)胖子當(dāng)著我的面吹牛博杖,可吹牛的內(nèi)容都是我干的椿胯。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼剃根,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼哩盲!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起狈醉,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤廉油,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后苗傅,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體抒线,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年渣慕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嘶炭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡逊桦,死狀恐怖眨猎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情强经,我是刑警寧澤睡陪,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響兰迫,放射性物質(zhì)發(fā)生泄漏信殊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一逮矛、第九天 我趴在偏房一處隱蔽的房頂上張望鸡号。 院中可真熱鬧,春花似錦须鼎、人聲如沸鲸伴。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)汞窗。三九已至,卻和暖如春赡译,著一層夾襖步出監(jiān)牢的瞬間仲吏,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工蝌焚, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留裹唆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓只洒,卻偏偏與公主長(zhǎng)得像许帐,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子毕谴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • blog.csdn.net CoreText實(shí)現(xiàn)圖文混排 - 博客頻道 CoreText實(shí)現(xiàn)圖文混排 也好久沒(méi)來(lái)寫(xiě)...
    K_Gopher閱讀 594評(píng)論 0 0
  • 蘋(píng)果文檔 https://developer.apple.com/documentation/coretext C...
    陽(yáng)明先生_x閱讀 421評(píng)論 0 4
  • 系列文章: CoreText實(shí)現(xiàn)圖文混排 CoreText實(shí)現(xiàn)圖文混排之點(diǎn)擊事件 CoreText實(shí)現(xiàn)圖文混排之文...
    老司機(jī)Wicky閱讀 40,067評(píng)論 221 432
  • 最近在網(wǎng)上看了一些大牛的文章成畦,自己也試著寫(xiě)了一下,感覺(jué)圖文混排真的很強(qiáng)大涝开。 廢話不多說(shuō)循帐,開(kāi)始整 先上效果圖跟代碼,...
    AllureJM閱讀 977評(píng)論 0 1
  • CoreText 是用于處理文字和字體的底層技術(shù)舀武。它直接和 Core Graphics(又被稱為 Quartz)打...
    SpursGo閱讀 1,697評(píng)論 0 2