YYAsyncLayer 原始碼解析

Junyiii發表於2017-11-14

YYAsyncLayer的示例YYAsyncLayer

示例中


[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];

- (void)contentsNeedUpdated {
    // do update
    [self.layer setNeedsDisplay];
}
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
    //..
}複製程式碼

1 YYTransaction

看看 YYTransaction , 根據名字 這應該是 處理事物相關的 類。
不得不說 這個註釋真好

/**
 YYTransaction let you perform a selector once before current runloop sleep.
 */
@interface YYTransaction : NSObject複製程式碼

可以看出 YYTransaction 是 用來將 selector 在 runloop sleep 前 提交到 runloop 中 處理的。
YYTransaction 儲存了 targetselector 用來在runloop observer callback 中執行對應方法

1.1 Commit

注意commit 中的註釋,如果 相同的 transaction 已經提交到 runloop 中了,這個方法什麼都不會做.

/**
 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 那麼 是不會存在兩個相同的元素,系統會自動刪掉一個元素

Objective-C 中 通過 isEqual: 方法 來測試和其他物件的想等性
通過重寫isEqual:hash 來支援根據 _selector,_target 判斷想等性.

- (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;
}複製程式碼

1.2 Observe RunLoop

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
    }];
}複製程式碼

2 YYAsyncLayer

/**
 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 需要在後臺佇列繪製的內容.

2.1 YYAsyncLayerDisplayTask

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 需要在後臺佇列繪製的內容.

2.1 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);複製程式碼

2.2 取消繪製任務

當 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;
};複製程式碼

3 如何使用

  1. YYAsyncLayerDelegate 的 - (YYAsyncLayerDisplayTask *)newAsyncDisplayTask 提供了繪製所需的task
  2. 在設定可以涉及到 檢視內容改變的 操作時,[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
  3. contentsNeedUpdated 的操作就是 [self.layer setNeedsDisplay]; 更新檢視

一些其他的閱讀在這個倉庫裡
github.com/JunyiXie/Op…

菜?一個,望大佬多指教

相關文章