YYAsyncLayer源代碼解析

前言

本文的中文注釋代碼demo更新在我的github上。

在研究iOS UI性能優化上,異步繪制一直是一個離不開的話題。最近在研究Facebook的開源框架AsyncDisplayKit的時候,找到了YYKit作者所實現的YYAsyncLayer。從這個項目了解異步繪制的方法。

項目結構

YYAsyncLayer項目較為簡單,一共就三個文件:

  • YYSentinel:線程安全的計數器。
  • YYTransaction:注冊runloop調用。
  • YYAsyncLayer:異步繪制的CALayer子類。

其中YYTransaction涉及了runloop的內容,具體runloop的了解,可以從作者的另一篇文章深入理解RunLoop了解。

以下將分別介紹下面3個文件。

源代碼

YYSentinel

YYSentinel使用原子性操作函數,進行計數。

/**
 線程安全的計數器
 */
@interface YYSentinel : NSObject
/**
 當前計數
 */
@property (readonly) int32_t value;
/**
 原子性增加值

 @return 新值
 */
- (int32_t)increase;

@end

#import <libkern/OSAtomic.h>
@implementation YYSentinel {
    int32_t _value;
}

- (int32_t)value {
    return _value;
}

- (int32_t)increase {
    //使用OSAtomic增加值
    return OSAtomicIncrement32(&_value);
}

@end

YYTransaction

YYTransaction的邏輯也并不復雜:將target和相應selector存入一個set中(重寫hash與isEqual用于set判斷),并且在runloop中注冊kCFRunLoopBeforeWaiting與kCFRunLoopExit事件,將優先級定義為0,即在Core Animation執行完畢后,執行相應的display方法,去模擬Core Animation的繪制機制,進行相應異步繪制的方法。
YYTransaction.h聲明

@interface YYTransaction : NSObject

/**
 創建和返回一個transaction通過一個定義的target和selector

 @param target   執行target,target會在runloop結束前被retain
 @param selector target的selector

 @return 1個新的transaction,或者有錯誤時返回nil
 */
+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector;

/**
 加入transaction到runloop
 */
- (void)commit;

@end

YYTransaction.m實現

@interface YYTransaction()
@property (nonatomic, strong) id target;
@property (nonatomic, assign) SEL selector;
@end

static NSMutableSet *transactionSet = nil;

//runloop循環的回調
static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (transactionSet.count == 0) return;
    NSSet *currentSet = transactionSet;
//獲取完上一次需要執行的方法后,將所有方法清空
    transactionSet = [NSMutableSet new];
    //遍歷set。執行里面的selector
    [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
    }];
}

static void YYTransactionSetup() {
    static dispatch_once_t onceToken;
    //gcd只運行一次
    dispatch_once(&onceToken, ^{
        transactionSet = [NSMutableSet new];
        CFRunLoopRef runloop = CFRunLoopGetMain();
        CFRunLoopObserverRef observer;
        
        //注冊runloop監聽,在等待與退出前進行
        observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                           kCFRunLoopBeforeWaiting | kCFRunLoopExit,
                                           true,      // repeat
                                           0xFFFFFF,  // after CATransaction(2000000)
                                           YYRunLoopObserverCallBack, NULL);
        //將監聽加在所有mode上
        CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
        CFRelease(observer);
    });
}


@implementation YYTransaction


+ (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;
    //初始化runloop監聽
    YYTransactionSetup();
    //添加行為到set中
    [transactionSet addObject:self];
}

//hash值返回
- (NSUInteger)hash {
    long v1 = (long)((void *)_selector);
    long v2 = (long)_target;
    return v1 ^ v2;
}
//isEqual返回
- (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;
}

@end

YYAsyncLayer

YYAsyncLayer為了異步繪制而繼承CALayer的子類。通過使用Core Graphic相關方法,在子線程中繪制內容Context,繪制完成后,回到主線程對layer.contents進行直接顯示。控制了渲染線程的數量以及通過原子計數YYSentinel控制了取消異步渲染的內容。通過delegate回調,可以使得不同的delegate對象在block中繪制需要的內容。
YYAsyncLayer.h聲明

/**
 YYAsyncLayer是異步渲染的CALayer子類
 */
@interface YYAsyncLayer : CALayer
//是否異步渲染
@property BOOL displaysAsynchronously;
@end

/**
 YYAsyncLayer's的delegate協議,一般是uiview。必須實現這個方法
 */
@protocol YYAsyncLayerDelegate <NSObject>
@required
//當layer的contents需要更新的時候,返回一個新的展示任務
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask;
@end

/**
 YYAsyncLayer在后臺渲染contents的顯示任務類
 */
@interface YYAsyncLayerDisplayTask : NSObject

/**
 這個block會在異步渲染開始的前調用,只在主線程調用。
 */
@property (nullable, nonatomic, copy) void (^willDisplay)(CALayer *layer);

/**
 這個block會調用去顯示layer的內容
 */
@property (nullable, nonatomic, copy) void (^display)(CGContextRef context, CGSize size, BOOL(^isCancelled)(void));

/**
 這個block會在異步渲染結束后調用,只在主線程調用。
 */
@property (nullable, nonatomic, copy) void (^didDisplay)(CALayer *layer, BOOL finished);

@end

YYAsyncLayer.m實現

/// Global display queue, used for content rendering.
//全局顯示線程,給content渲染用
static dispatch_queue_t YYAsyncLayerGetDisplayQueue() {
    //如果存在YYDispatchQueuePool
#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, ^{
        //處理器數量,最多創建16個serial線程
        queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
        queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount;
        if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
            for (NSUInteger i = 0; i < queueCount; i++) {
                dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
                queues[i] = dispatch_queue_create("com.ibireme.yykit.render", attr);
            }
        } else {
            for (NSUInteger i = 0; i < queueCount; i++) {
                queues[i] = dispatch_queue_create("com.ibireme.yykit.render", DISPATCH_QUEUE_SERIAL);
                dispatch_set_target_queue(queues[i], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
            }
        }
    });
    //循環獲取相應的線程
    int32_t cur = OSAtomicIncrement32(&counter);
    if (cur < 0) cur = -cur;
    return queues[(cur) % queueCount];
#undef MAX_QUEUE_COUNT
#endif
}

//釋放線程
static dispatch_queue_t YYAsyncLayerGetReleaseQueue() {
#ifdef YYDispatchQueuePool_h
    return YYDispatchQueueGetForQOS(NSQualityOfServiceDefault);
#else
    return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
#endif
}


@implementation YYAsyncLayerDisplayTask
@end


@implementation YYAsyncLayer {
    //計數,用于取消異步繪制
    YYSentinel *_sentinel;
}

#pragma mark - Override

+ (id)defaultValueForKey:(NSString *)key {
    if ([key isEqualToString:@"displaysAsynchronously"]) {
        return @(YES);
    } else {
        return [super defaultValueForKey:key];
    }
}

- (instancetype)init {
    self = [super init];
    static CGFloat scale; //global
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        scale = [UIScreen mainScreen].scale;
    });
    self.contentsScale = scale;
    _sentinel = [YYSentinel new];
    _displaysAsynchronously = YES;
    return self;
}


- (void)dealloc {
    [_sentinel increase];
}

//需要重新渲染的時候,取消原來沒有完成的異步渲染
- (void)setNeedsDisplay {
    [self _cancelAsyncDisplay];
    [super setNeedsDisplay];
}


/**
 重寫展示方法,設置contents內容
 */
- (void)display {
    super.contents = super.contents;
    [self _displayAsync:_displaysAsynchronously];
}

#pragma mark - Private


- (void)_displayAsync:(BOOL)async {
    //獲取delegate對象,這邊默認是CALayer的delegate,持有它的uiview
    __strong id<YYAsyncLayerDelegate> delegate = self.delegate;
    //delegate的初始化方法
    YYAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];
    //沒有展示block,就直接調用其他兩個block返回
    if (!task.display) {
        if (task.willDisplay) task.willDisplay(self);
        self.contents = nil;
        if (task.didDisplay) task.didDisplay(self, YES);
        return;
    }
    
    //異步
    if (async) {
        //先調用willdisplay
        if (task.willDisplay) task.willDisplay(self);
        //獲取計數
        YYSentinel *sentinel = _sentinel;
        int32_t value = sentinel.value;
        //用計數判斷是否已經取消
        BOOL (^isCancelled)() = ^BOOL() {
            return value != sentinel.value;
        };
        CGSize size = self.bounds.size;
        BOOL opaque = self.opaque;
        CGFloat scale = self.contentsScale;
        //長寬<1,直接清除contents內容
        if (size.width < 1 || size.height < 1) {
            //獲取contents內容
            CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
            //清除內容
            self.contents = nil;
            //如果是圖片就release圖片
            if (image) {
                dispatch_async(YYAsyncLayerGetReleaseQueue(), ^{
                    CFRelease(image);
                });
            }
            //已經展示完成block,finish為yes
            if (task.didDisplay) task.didDisplay(self, YES);
            return;
        }
        
        //異步線程調用
        dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{
            //是否取消
            if (isCancelled()) return;
            //創建Core Graphic bitmap context
            UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
            CGContextRef context = UIGraphicsGetCurrentContext();
            //返回context進行展示
            task.display(context, size, isCancelled);
            //如果取消,停止渲染
            if (isCancelled()) {
                //結束context,并且展示完成block,finish為no
                UIGraphicsEndImageContext();
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (task.didDisplay) task.didDisplay(self, NO);
                });
                return;
            }
            //獲取當前畫布
            UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
            //結束context
            UIGraphicsEndImageContext();
            //如果取消停止渲染
            if (isCancelled()) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (task.didDisplay) task.didDisplay(self, NO);
                });
                return;
            }
            //返回主線程
            dispatch_async(dispatch_get_main_queue(), ^{
                //如果取消,停止渲染
                if (isCancelled()) {
                    if (task.didDisplay) task.didDisplay(self, NO);
                } else {
                    //主線程設置contents內容進行展示
                    self.contents = (__bridge id)(image.CGImage);
                    //已經展示完成block,finish為yes
                    if (task.didDisplay) task.didDisplay(self, YES);
                }
            });
        });
    } else {
        //同步展示,直接increase,停止異步展示
        [_sentinel increase];
        if (task.willDisplay) task.willDisplay(self);
        //直接創建Core Graphic bitmap context
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);
        CGContextRef context = UIGraphicsGetCurrentContext();
        task.display(context, self.bounds.size, ^{return NO;});
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        //進行展示
        self.contents = (__bridge id)(image.CGImage);
        if (task.didDisplay) task.didDisplay(self, YES);
    }
}

- (void)_cancelAsyncDisplay {
    [_sentinel increase];
}

@end

用法

這里舉作者在github里寫的簡單例子:
在設置內容與layoutSubviews內添加YYTransaction,并且實現了- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask方法去實現相應的三個block,這樣,一個異步繪制的YYLabel就完成了。

@interface YYLabel : UIView
@property NSString *text;
@property UIFont *font;
@end

@implementation YYLabel

- (void)setText:(NSString *)text {
    _text = text.copy;
    [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)setFont:(UIFont *)font {
    _font = font;
    [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)layoutSubviews {
    [super layoutSubviews];
    [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)contentsNeedUpdated {
    // do update
    [self.layer setNeedsDisplay];
}

#pragma mark - YYAsyncLayer

+ (Class)layerClass {
    return YYAsyncLayer.class;
}

- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {

    // capture current state to display task
    NSString *text = _text;
    UIFont *font = _font;

    YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
    task.willDisplay = ^(CALayer *layer) {
        //...
    };

    task.display = ^(CGContextRef context, CGSize size, BOOL(^isCancelled)(void)) {
        if (isCancelled()) return;
        NSArray *lines = CreateCTLines(text, font, size.width);
        if (isCancelled()) return;

        for (int i = 0; i < lines.count; i++) {
            CTLineRef line = line[i];
            CGContextSetTextPosition(context, 0, i * font.pointSize * 1.5);
            CTLineDraw(line, context);
            if (isCancelled()) return;
        }
    };

    task.didDisplay = ^(CALayer *layer, BOOL finished) {
        if (finished) {
            // finished
        } else {
            // cancelled
        }
    };

    return task;
}
@end

總結

YYAsyncLayer內使用YYTransaction在 RunLoop 中注冊了一個 Observer,監視的事件和 Core Animation 一樣,但優先級比 CA 要低。當 RunLoop 進入休眠前、CA 處理完事件后,YYTransaction 就會執行該 loop 內提交的所有任務。
在YYAsyncLayer中,通過重寫CALayer顯示display方法,向delegate請求一個異步繪制的任務,并且在子線程中繪制Core Graphic對象,最后再回到主線程中設置layer.contents內容。

附上作者的部分解讀:

YYAsyncLayer 是 CALayer 的子類,當它需要顯示內容(比如調用了 [layer setNeedDisplay])時,它會向 delegate,也就是 UIView 請求一個異步繪制的任務。在異步繪制時,Layer 會傳遞一個 BOOL(^isCancelled)() 這樣的 block,繪制代碼可以隨時調用該 block 判斷繪制任務是否已經被取消。

參考資料

本文CSDN地址
1.iOS 保持界面流暢的技巧
2.深入理解RunLoop

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容