眾所周知,在iOS系統(tǒng)中买乃,UI相關(guān)操作和用戶的操作都是在主線程中進(jìn)行姻氨,大量的UI操作,會造成主線程阻塞为牍,影響應(yīng)用的流暢度和用戶體驗(yàn)哼绑,為了保證APP可以及時的響應(yīng)用戶的操作,所以一些UI的繪制工作碉咆,最好放到子線程抖韩,進(jìn)行異步繪制,減輕主線程的工作疫铜,因此YY作者寫出了一個異步繪制的工具YYAsyncLayer茂浮。
YYAsyncLayer 是 CALayer 的子類,當(dāng)它需要顯示內(nèi)容(比如調(diào)用了 [layer setNeedDisplay])時壳咕,它會向 delegate席揽,也就是 UIView 請求一個異步繪制的任務(wù)。在異步繪制時谓厘,Layer 會傳遞一個 BOOL(^isCancelled)() 這樣的 block幌羞,繪制代碼可以隨時調(diào)用該 block 判斷繪制任務(wù)是否已經(jīng)被取消。
當(dāng) TableView 快速滑動時竟稳,會有大量異步繪制任務(wù)提交到后臺線程去執(zhí)行属桦。但是有時滑動速度過快時,繪制任務(wù)還沒有完成就可能已經(jīng)被取消了他爸。如果這時仍然繼續(xù)繪制聂宾,就會造成大量的 CPU 資源浪費(fèi),甚至阻塞線程并造成后續(xù)的繪制任務(wù)遲遲無法完成诊笤。我的做法是盡量快速系谐、提前判斷當(dāng)前繪制任務(wù)是否已經(jīng)被取消;在繪制每一行文本前讨跟,我都會調(diào)用 isCancelled() 來進(jìn)行判斷纪他,保證被取消的任務(wù)能及時退出鄙煤,不至于影響后續(xù)操作。iOS 保持界面流暢的技巧
一止喷、YYAsyncLayer 文件類組成
YYAsyncLayer中主要有三個類:
YYTransaction:注冊一個通知馆类,在監(jiān)控runLoop睡眠和退出,來執(zhí)行任務(wù)回調(diào)弹谁,利用runloop空閑,執(zhí)行任務(wù)句喜。(如果你想知道為什么要在睡眠和退出的時候预愤,執(zhí)行任務(wù),你可以看下YY的這篇博客了解下深入理解RunLoop)
YYSentine:線程安全的計(jì)數(shù)器咳胃,通過判斷計(jì)數(shù)器的值是否相等植康,來判斷異步繪制任務(wù)是否被取消。
YYAsyncLayer:異步渲染的核心類展懈,是CALayer子類销睁,用來異步渲染layer內(nèi)容。
二存崖、YYAsyncLayer 源碼分析
1.YYTransaction源碼分析
YYTransaction繪制任務(wù)的機(jī)制是仿照CoreAnimation的繪制機(jī)制冻记,監(jiān)聽主線程RunLoop,在空閑階段插入繪制任務(wù)来惧,并將任務(wù)優(yōu)先級設(shè)置在CoreAnimation繪制完成之后冗栗,然后遍歷繪制任務(wù)集合進(jìn)行繪制工作并且清空集合,具體可以看源碼供搀。
/**
YYTransaction let you perform a selector once before current runloop sleep.
*/
@interface YYTransaction : NSObject
/**
Creates and returns a transaction with a specified target and selector.
@param target A specified target, the target is retained until runloop end.
@param selector A selector for target.
@return A new transaction, or nil if an error occurs.
*/
+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector;
/**
Commit the trancaction to main runloop.
@discussion It will perform the selector on the target once before main runloop's
current loop sleep. If the same transaction (same target and same selector) has
already commit to runloop in this loop, this method do nothing.
*/
- (void)commit;
@end
上面的YYTransaction.h文件中有兩個方法:
第一個方法是+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector隅居,根據(jù)傳入的target和selector來創(chuàng)建一個任務(wù)。
第二個方法是- (void)commit;葛虐,它用來在runloop睡眠的時候胎源,執(zhí)行傳入任務(wù),并且對于相同的任務(wù)屿脐,在runloop中只執(zhí)行一次涕蚤。
看到這里你應(yīng)該有兩個疑問,
第一是如何來實(shí)現(xiàn)在runLoop將要休眠的時候摄悯,來執(zhí)行傳進(jìn)來的任務(wù)赞季??奢驯?
第二是如何保證相同的任務(wù)只執(zhí)行一次申钩??瘪阁?
那么下面我們來看下源碼,分析上面的兩個問題??
// 注冊 Runloop Observer
static void YYTransactionSetup() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
transactionSet = [NSMutableSet new];
CFRunLoopRef runloop = CFRunLoopGetMain();
CFRunLoopObserverRef observer;
/**
創(chuàng)建一個RunLoop的觀察者
allocator:該參數(shù)為對象內(nèi)存分配器义黎,一般使用默認(rèn)的分配器kCFAllocatorDefault禾进。或者nil
activities:該參數(shù)配置觀察者監(jiān)聽Run Loop的哪種運(yùn)行狀態(tài)廉涕,這里我們監(jiān)聽beforeWaiting和exit狀態(tài)
repeats:CFRunLoopObserver是否循環(huán)調(diào)用泻云。
order:CFRunLoopObserver的優(yōu)先級,當(dāng)在Runloop同一運(yùn)行階段中有多個CFRunLoopObserver時狐蜕,根據(jù)這個來先后調(diào)用CFRunLoopObserver宠纯,0為最高優(yōu)先級別。正常情況下使用0层释。
callout:觀察者的回調(diào)函數(shù)婆瓜,在Core Foundation框架中用CFRunLoopObserverCallBack重定義了回調(diào)函數(shù)的閉包。
context:觀察者的上下文贡羔。 (類似與KVO傳遞的context廉白,可以傳遞信息,)因?yàn)檫@個函數(shù)創(chuàng)建ovserver的時候需要傳遞進(jìn)一個函數(shù)指針乖寒,而這個函數(shù)指針可能用在n多個oberver 可以當(dāng)做區(qū)分是哪個observer的狀機(jī)態(tài)猴蹂。(下面的通過block創(chuàng)建的observer一般是一對一的,一般也不需要Context宵统,)晕讲,還有一個例子類似與NSNOtificationCenter的 SEL和 Block方式
*/
observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopBeforeWaiting | kCFRunLoopExit,
true, // repeat
0xFFFFFF, // after CATransaction(2000000)
YYRunLoopObserverCallBack, NULL);
CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
});
}
//監(jiān)聽回調(diào)的方法
static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
if (transactionSet.count == 0) return;
NSSet *currentSet = transactionSet;
transactionSet = [NSMutableSet new];
[currentSet enumerateObjectsUsingBlock:^(YYTransaction *transaction, BOOL *stop) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[transaction.target performSelector:transaction.selector];
#pragma clang diagnostic pop
}];
}
上面的代碼就是runLoop將要休眠的時候,執(zhí)行任務(wù)的核心代碼马澈,
首先我們來看一下YYTransactionSetup方法中CFRunLoopObserverCreate函數(shù)的參數(shù)選绕笆 :
1. 任務(wù)執(zhí)行的時機(jī)
從上面源碼可以看出是在 kCFRunLoopBeforeWaiting | kCFRunLoopExit,也就是在將要睡眠和推出的時候來執(zhí)行痊班。
2.執(zhí)行的優(yōu)先級
從上面源碼可以看出是0xFFFFFF, // after CATransaction(2000000)勤婚,這是在CoreAnimation繪制完成之后之后執(zhí)行。
3.執(zhí)行的runLoop
從上面源碼可以看出是 CFRunLoopGetMain();也就是主線程的runLoop中執(zhí)行這個回調(diào)涤伐,這是因?yàn)閳?zhí)行的任務(wù)跟UI相關(guān)馒胆,必須要在主線程執(zhí)行。
然后我們看下回調(diào)方法凝果,是通過執(zhí)行 transactionSet 中的 transaction來執(zhí)行具體的方法祝迂,看到這個第一個問題“如何來實(shí)現(xiàn)在runLoop將要休眠的時候,執(zhí)行任務(wù)”已經(jīng)有答案了吧器净。
下面我們來看下“如何保證相同的任務(wù)只執(zhí)行一次型雳???”
- (void)commit {
if (!_target || !_selector) return;
YYTransactionSetup();
[transactionSet addObject:self];
}
- (NSUInteger)hash {
long v1 = (long)((void *)_selector);
long v2 = (long)_target;
return v1 ^ v2;
}
- (BOOL)isEqual:(id)object {
if (self == object) return YES;
if (![object isMemberOfClass:self.class]) return NO;
YYTransaction *other = object;
return other.selector == _selector && other.target == _target;
}
就是在commit的時候?qū)ransaction添加到transactionSet中纠俭,而我們知道NSMutableSet中不會出現(xiàn)相同的對象沿量,所以這就實(shí)現(xiàn)了相同的任務(wù)只執(zhí)行一次,同事由于NSMutableSet中是通過isEqual和hash來判斷對象是否相同的冤荆,所以將這兩個方法重寫,保證transactionSet中任務(wù)的唯一性钓简。
2.** YYAsyncLayer源碼分析**
YYAsyncLayer為了異步繪制而繼承CALayer的子類。通過使用CoreGraphic相關(guān)方法涌庭,在子線程中繪制內(nèi)容Context芥被,繪制完成后坐榆,回到主線程對layer.contents進(jìn)行直接顯示冗茸。
@interface YYAsyncLayer : CALayer
/// Whether the render code is executed in background. Default is YES.
@property BOOL displaysAsynchronously;
@end
@protocol YYAsyncLayerDelegate <NSObject>
@required
/// This method is called to return a new display task when the layer's contents need update.
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask;
@end
/**
A display task used by YYAsyncLayer to render the contents in background queue.
*/
@interface YYAsyncLayerDisplayTask : NSObject
@property (nullable, nonatomic, copy) void (^willDisplay)(CALayer *layer);
@property (nullable, nonatomic, copy) void (^display)(CGContextRef context, CGSize size, BOOL(^isCancelled)(void));
@property (nullable, nonatomic, copy) void (^didDisplay)(CALayer *layer, BOOL finished);
@end
YYAsyncLayer中主要有三部分:
1. YYAsyncLayerDelegate
YYAsyncLayerDelegate 的 newAsyncDisplayTask 是提供了 YYAsyncLayer 需要在后臺隊(duì)列繪制的內(nèi)容
2. YYAsyncLayerDisplayTask
display 在mainthread或者background thread調(diào)用 這要求 display 應(yīng)該是線程安全的
willdisplay 和 didDisplay 在 mainthread 調(diào)用。
3.YYAsyncLayer
YYAsyncLayer是通過創(chuàng)建異步創(chuàng)建圖像Context在其繪制夏漱,最后再主線程異步添加圖像從而實(shí)現(xiàn)的異步繪制。同時挂绰,在繪制過程中進(jìn)行了多次進(jìn)行取消判斷屎篱,以避免額外繪制.
YYAsyncLayer如何實(shí)現(xiàn)異步繪制和取消繪制功能:
1)異步繪制
通過 重寫display 方法,調(diào)用- (void)_displayAsync:(BOOL)async葵蒂,在后臺線程中調(diào)用task.display 進(jìn)行繪制交播,最終在主線程中將繪制圖片賦值給self.contents。
2)是否取消繪制
通過isCancelled來判斷是否取消繪制践付,主要是利用了局部變量被block捕獲后秦士,在block中value就不會改變,通過判斷block中的value和外部的value值是否相等永高,來判斷任務(wù)是否已經(jīng)取消隧土。
YYSentinel *sentinel = _sentinel;
int32_t value = sentinel.value;
BOOL (^isCancelled)() = ^BOOL() {
return value != sentinel.value;
};
3)如何創(chuàng)建隊(duì)列
通過[NSProcessInfo processInfo].activeProcessorCount控制隊(duì)列的最大數(shù)量和cpu的數(shù)量保持一致,因?yàn)榫€程的切換也是需要額外的開銷的命爬。所以線程不是越多曹傀,執(zhí)行效率越高。
三饲宛、YYAsyncLayer 問題總結(jié)
1. YYTransaction中皆愉,如何在runLoop將要休眠的時候,來執(zhí)行傳進(jìn)來的任務(wù)?
通過YYTransactionSetup方法中執(zhí)行CFRunLoopObserverCreate函數(shù)實(shí)現(xiàn)亥啦,具體的函數(shù)參數(shù)選取如下:
1) 任務(wù)執(zhí)行的時機(jī)
從上面源碼可以看出是在 kCFRunLoopBeforeWaiting | kCFRunLoopExit炭剪,也就是在將要睡眠和推出的時候來執(zhí)行。
2)執(zhí)行的優(yōu)先級
從上面源碼可以看出是0xFFFFFF, // after CATransaction(2000000)翔脱,這是在CoreAnimation繪制完成之后之后執(zhí)行奴拦。
3)執(zhí)行的runLoop
從上面源碼可以看出是 CFRunLoopGetMain();也就是主線程的runLoop中執(zhí)行這個回調(diào),這是因?yàn)閳?zhí)行的任務(wù)跟UI相關(guān)届吁,必須要在主線程執(zhí)行错妖。
2. YYTransaction如何保證相同的任務(wù)(transaction)只執(zhí)行一次?
通過在commit的時候?qū)ransaction添加到transactionSet中疚沐,而我們知道NSMutableSet中不會出現(xiàn)相同的對象暂氯,所以這就實(shí)現(xiàn)了相同的任務(wù)只執(zhí)行一次,同事由于NSMutableSet中是通過isEqual和hash來判斷對象是否相同的亮蛔,所以將這兩個方法重寫痴施,保證transactionSet中任務(wù)的唯一性。
3.YYAsyncLayer如何實(shí)現(xiàn)異步繪制和取消繪制功能
1)異步繪制
通過 重寫display 方法究流,調(diào)用- (void)_displayAsync:(BOOL)async辣吃,在后臺線程中調(diào)用task.display 進(jìn)行繪制,最終在主線程中將繪制圖片賦值給self.contents芬探。
2)是否取消繪制
通過isCancelled來判斷是否取消繪制神得,主要是利用了局部變量被block捕獲后,在block中value就不會改變偷仿,通過判斷block中的value和外部的value值是否相等哩簿,來判斷任務(wù)是否已經(jīng)取消。
YYSentinel *sentinel = _sentinel;
int32_t value = sentinel.value;
BOOL (^isCancelled)() = ^BOOL() {
return value != sentinel.value;
};
3)如何創(chuàng)建隊(duì)列
通過[NSProcessInfo processInfo].activeProcessorCount控制隊(duì)列的最大數(shù)量和cpu的數(shù)量保持一致酝静,因?yàn)榫€程的切換也是需要額外的開銷的节榜。所以線程不是越多,執(zhí)行效率越高全跨。
四亿遂、YYAsyncLayer 相關(guān)知識點(diǎn)總結(jié)
1.CFRunLoopObserverCreate函數(shù)使用,創(chuàng)建runLoop監(jiān)聽蛇数。
2.NSMutableSet使用,一直相同對象判斷條件hash和isEqual碌上。
3.OSAtomicIncrement32線程安全的自增計(jì)數(shù),每調(diào)用一次+1倚评。
4.GCD相關(guān)隊(duì)列天梧,dispatch_once等使用霞丧。
5.block捕獲局部變量,值不會改變特性蛹尝。
6.CoreGraphics相關(guān)繪制API的使用。
參考資料:
https://github.com/ibireme/YYAsyncLayer
https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/
https://blog.ibireme.com/2015/05/18/runloop/
https://juejin.im/post/5a0a52b5f265da43247ff4ad
http://www.reibang.com/p/58e7571d7806
iOS的異步繪制--YYAsyncLayer源碼分析