YYAsyncLayer的示例YYAsyncLayerios
示例中 git
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
- (void)contentsNeedUpdated {
// do update
[self.layer setNeedsDisplay];
}
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
//..
}複製代碼
看看 YYTransaction , 根據名字 這應該是 處理事物相關的 類。
不得不說 這個註釋真好github
/**
YYTransaction let you perform a selector once before current runloop sleep.
*/
@interface YYTransaction : NSObject複製代碼
能夠看出 YYTransaction 是 用來將 selector 在 runloop sleep 前 提交到 runloop 中 處理的。
YYTransaction 存儲了 target
和 selector
用來在runloop observer callback 中執行對應方法安全
注意commit 中的註釋,若是 相同的 transaction 已經提交到 runloop 中了,這個方法什麼都不會作.bash
/**
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;複製代碼
這是怎麼實現的呢?異步
- (void)commit {
if (!_target || !_selector) return;
// 在Commit 中 作 單例的初始化 很好 隱藏了不少細節,使用着經過 簡單的調用便可 添加 transcation
YYTransactionSetup();
[transactionSet addObject:self];
}複製代碼
能夠注意到transactionSet 既然是Set 那麼 是不會存在兩個相同的元素,系統會自動刪掉一個元素async
在 Objective-C
中 經過 isEqual:
方法 來測試和其餘對象的想等性
經過重寫isEqual:
和hash
來支持根據 _selector
,_target
判斷想等性.oop
- (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;
}複製代碼
YYTransaction 經過觀察Runloop的waiting或Exit狀態 ,經過回調,執行 transactionSet 中的 transaction 測試
// 註冊 Runloop Observer
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);
});
}
// Runloop Observer Callback
static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
if (transactionSet.count == 0) return;
NSSet *currentSet = transactionSet;
// 更新 trasactionSet 保證 callback 執行後,對象不會被持有
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
}];
}複製代碼
/**
The YYAsyncLayer class is a subclass of CALayer used for render contents asynchronously.
@discussion When the layer need update it's contents, it will ask the delegate for a async display task to render the contents in a background queue. */ @interface YYAsyncLayer : CALayer複製代碼
能夠看到 YYAsyncLayerDelegate 的 newAsyncDisplayTask 是提供了 YYAsyncLayer 須要在後臺隊列繪製的內容. ui
YYAsyncLayerDisplayTask 有以下的屬性
@property (nullable, nonatomic, copy) void (^willDisplay)(CALayer *layer);- display
@property (nullable, nonatomic, copy) void (^display)(CGContextRef context, CGSize size, BOOL(^isCancelled)(void));
@property (nullable, nonatomic, copy) void (^didDisplay)(CALayer *layer, BOOL finished);複製代碼
display 在mainthread或者background thread調用 這要求 display 應該是線程安全的
willdisplay 和 didDisplay 在 mainthread 調用。
newAsyncDisplayTask 是提供了 YYAsyncLayer 須要在後臺隊列繪製的內容.
經過 重寫display 方法,異步繪製 self.contents
- (void)display {
super.contents = super.contents;
[self _displayAsync:_displaysAsynchronously];
}複製代碼
- (void)_displayAsync:(BOOL)async
中在後臺隊列執行 task.display 的 block 進行繪製任務,最後在主線程中 將繪製結果的圖片賦值給 contents.
self.contents = (__bridge id)(image.CGImage);複製代碼
當 TableView 快速滑動時,會有大量異步繪製任務提交到後臺線程去執行。可是有時滑動速度過快時,繪製任務尚未完成就可能已經被取消了。若是這時仍然繼續繪製,就會形成大量的 CPU 資源浪費,甚至阻塞線程並形成後續的繪製任務遲遲沒法完成。iOS 保持界面流暢的技巧
- (void)setNeedsDisplay {
[self _cancelAsyncDisplay];
[super setNeedsDisplay];
}
- (void)_cancelAsyncDisplay {
[_sentinel increase];
}複製代碼
這個跟 tableview 的 cell 重用有關
因爲 cell 重用,那麼 當重用的cell繪製新的內容時,就會調用setNeedDisplay 方法.
這是能夠再次取消上一次的後臺繪製任務,在進行新的繪製.
利用 YYSentinel 來完成任務的取消
/**
YYSentinel is a thread safe incrementing counter.
It may be used in some multi-threaded situation.
*/
@interface YYSentinel : NSObject複製代碼
value 用來保存任務剛開始時 sentinel.value
int32_t value = sentinel.value;複製代碼
若是任務執行過程當中 發現snetinel.value 和 保存的value,則就是認爲任務以及取消了
BOOL (^isCancelled)() = ^BOOL() {
return value != sentinel.value;
};複製代碼
一些其餘的閱讀在這個倉庫裏
github.com/JunyiXie/Op…
菜🐔一個,望大佬多指教