前言
YYAsyncLayer是非同步繪製與顯示的工具。最初是從YYKitDemo中接觸到這個工具,為了保證列表滾動流暢,將檢視繪製、以及圖片解碼等任務放到後臺執行緒,在YYAsyncLayer之前還是想從YYKitDemo中效能優化說起,雖然些跑題了…
YYKitDemo
對於列表主要對兩個代理方法的優化,一個與繪製顯示有關,另一個與計算佈局有關:
1 2 |
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; |
常規邏輯可能覺得應該先呼叫tableView : cellForRowAtIndexPath :
返回UITableViewCell
物件,事實上呼叫順序是先返回UITableViewCell
的高度,是因為UITableView
繼承自UIScrollView
,滑動範圍由屬性contentSize
來確定,UITableView
的滑動範圍需要通過每一行的UITableViewCell
的高度計算確定,複雜cell如果在列表滾動過程中計算可能會造成一定程度的卡頓。
假設有20條資料,當前螢幕顯示5條,tableView : heightForRowAtIndexPath :
方法會先執行20次返回所有高度並計算出滑動範圍,tableView : cellForRowAtIndexPath :
執行5次返回當前螢幕顯示的cell個數。
從圖中簡單看下流程,從網路請求返回JSON資料,將Cell的高度以及內部檢視的佈局封裝為Layout物件,Cell顯示之前在非同步執行緒計算好所有佈局物件,並存入陣列,每次呼叫tableView: heightForRowAtIndexPath :
只需要從陣列中取出,可避免重複的佈局計算。同時在呼叫tableView: cellForRowAtIndexPath :
對Cell內部檢視非同步繪製佈局,以及圖片的非同步繪製解碼,這裡就要說到今天的主角YYAsyncLayer。
YYAsyncLayer
首先介紹裡面幾個類:
- YYAsyncLayer:繼承自CALayer,繪製、建立繪製執行緒的部分都在這個類。
- YYTransaction:用於建立RunloopObserver監聽MainRunloop的空閒時間,並將YYTranaction物件存放到集合中。
- YYSentinel:提供獲取當前值的
value
(只讀)屬性,以及- (int32_t)increase
自增加的方法返回一個新的value
值,用於判斷非同步繪製任務是否被取消的工具。
上圖是整體非同步繪製的實現思路,後面一步步說明。現在假設需要繪製Label,其實是繼承自UIView,重寫+ (Class)layerClass
,在需要重新繪製的地方呼叫下面方法,比如setter
,layoutSubviews
。
1 2 3 4 5 6 7 8 9 10 11 |
+ (Class)layerClass { return YYAsyncLayer.class; } - (void)setText:(NSString *)text { _text = text.copy; [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit]; } - (void)layoutSubviews { [super layoutSubviews]; [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit]; } |
YYTransaction有selector
、target
的屬性,selector
其實就是contentsNeedUpdated
方法,此時並不會立即在後臺執行緒去更新顯示,而是將YYTransaction物件本身提交儲存在transactionSet
的集合中,上圖中所示。
1 2 3 4 5 6 7 8 9 10 11 12 |
+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector{ if (!target || !selector) return nil; YYTransaction *t = [YYTransaction new]; t.target = target; t.selector = selector; return t; } - (void)commit { if (!_target || !_selector) return; YYTransactionSetup(); [transactionSet addObject:self]; } |
同時在YYTransaction.m中註冊一個RunloopObserver,監聽MainRunloop在kCFRunLoopCommonModes
(包含kCFRunLoopDefaultMode
、UITrackingRunLoopMode
)下的kCFRunLoopBeforeWaiting
和kCFRunLoopExit
的狀態,也就是說在一次Runloop空閒時去執行更新顯示的操作。
kCFRunLoopBeforeWaiting
:Runloop將要進入休眠。
kCFRunLoopExit
:即將退出本次Runloop。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static void YYTransactionSetup() { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ transactionSet = [NSMutableSet new]; CFRunLoopRef runloop = CFRunLoopGetMain(); CFRunLoopObserverRef observer; observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting | kCFRunLoopExit, true, // repeat 0xFFFFFF, // after CATransaction(2000000) YYRunLoopObserverCallBack, NULL); CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes); CFRelease(observer); }); } |
下面是RunloopObserver的回撥方法,從transactionSet取出transaction物件執行SEL的方法,分發到每一次Runloop執行,避免一次Runloop執行時間太長。
1 2 3 4 5 6 7 8 9 10 11 |
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 }]; } |
接下來是非同步繪製,這裡用了一個比較巧妙的方法處理,當使用GCD時提交大量併發任務到後臺執行緒導致執行緒被鎖住、休眠的情況,建立與程式當前啟用CPU數量(activeProcessorCount
)相同的序列佇列,並限制MAX_QUEUE_COUNT
,將佇列存放在陣列中。
YYAsyncLayer.m有一個方法YYAsyncLayerGetDisplayQueue
來獲取這個佇列用於繪製(這部分YYKit中有獨立的工具YYDispatchQueuePool)。建立佇列中有一個引數是告訴佇列執行任務的服務質量quality of service,在iOS8+之後相比之前系統有所不同。
- iOS8之前佇列優先順序:
DISPATCH_QUEUE_PRIORITY_HIGH 2
高優先順序
DISPATCH_QUEUE_PRIORITY_DEFAULT 0
預設優先順序
DISPATCH_QUEUE_PRIORITY_LOW (-2)
低優先順序
DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
後臺優先順序
- iOS8+之後:
QOS_CLASS_USER_INTERACTIVE 0x21
, 使用者互動(希望儘快完成,不要放太耗時操作)
QOS_CLASS_USER_INITIATED 0x19
, 使用者期望(不要放太耗時操作)
QOS_CLASS_DEFAULT 0x15
, 預設(用來重置對列使用的)
QOS_CLASS_UTILITY 0x11
, 實用工具(耗時操作,可以使用這個選項)
QOS_CLASS_BACKGROUND 0x09
, 後臺
QOS_CLASS_UNSPECIFIED 0x00
, 未指定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/// Global display queue, used for content rendering. static dispatch_queue_t YYAsyncLayerGetDisplayQueue() { #ifdef YYDispatchQueuePool_h return YYDispatchQueueGetForQOS(NSQualityOfServiceUserInitiated); #else #define MAX_QUEUE_COUNT 16 static int queueCount; static dispatch_queue_t queues[MAX_QUEUE_COUNT]; //存放佇列的陣列 static dispatch_once_t onceToken; static int32_t counter = 0; dispatch_once(&onceToken, ^{ //程式啟用的處理器數量 queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount; queueCount = queueCount MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount); if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) { for (NSUInteger i = 0; i |
接下來是關於繪製部分的程式碼,對外介面YYAsyncLayerDelegate
代理中提供- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask
方法用於回撥繪製的程式碼,以及是否非同步繪製的BOOl型別屬性displaysAsynchronously
,同時重寫CALayer的display
方法來呼叫繪製的方法- (void)_displayAsync:(BOOL)async
。
這裡有必要了解關於後臺的繪製任務何時會被取消,下面兩種情況需要取消,並呼叫了YYSentinel的increase
方法,使value
值增加(執行緒安全):
- 在檢視呼叫
setNeedsDisplay
時說明檢視的內容需要被更新,將當前的繪製任務取消,需要重新顯示。 - 以及檢視被釋放呼叫了
dealloc
方法。
在YYAsyncLayer.h中定義了YYAsyncLayerDisplayTask
類,有三個block屬性用於繪製的回撥操作,從命名可以看出分別是將要繪製,正在繪製,以及繪製完成的回撥,可以從block傳入的引數BOOL(^isCancelled)(void)
判斷當前繪製是否被取消。
1 2 3 |
@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); |
下面是部分- (void)_displayAsync:(BOOL)async
繪製的程式碼,主要是一些邏輯判斷以及繪製函式,在非同步執行之前通過YYAsyncLayerGetDisplayQueue
建立的佇列,這裡通過YYSentinel
判斷當前的value
是否等於之前的值,如果不相等,說明繪製任務被取消了,繪製過程會多次判斷是否取消,如果是則return,保證被取消的任務能及時退出,如果繪製完畢則設定圖片到layer.contents
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
if (async) { //非同步 if (task.willDisplay) task.willDisplay(self); YYSentinel *sentinel = _sentinel; int32_t value = sentinel.value; NSLog(@" --- %d ---", value); //判斷當前計數是否等於之前計數 BOOL (^isCancelled)() = ^BOOL() { return value != sentinel.value; }; CGSize size = self.bounds.size; BOOL opaque = self.opaque; CGFloat scale = self.contentsScale; CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL; if (size.width |
最後
關於具體使用可以看看程式的示例,這是從YYAsyncLayer中學到的一些技巧,自己還試著簡單實現一遍,專案中遇到的效能問題可也以依據這些思路去找到最合適的解決方案,挺想說一句閱讀原始碼是件比較要耐心的事,但確實可以收穫頗多。最近也有換環境工作的計劃,座標帝都,歡迎騷擾https://github.com/ShelinShelin
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!