YYAsyncLayer 學習


title: YYAsyncLayer 學習
date: 2017-11-23 13:27:19
tags: 第三方框架學習


YYAsyncLayer 學習

簡介

YYAsyncLayer是用于圖層異步繪制的一個組件,將耗時操作(如文本布局計算)放在RunLoop空閑時去做,進而減少卡頓.

組件內(nèi)容

YYAsyncLayer主要有3個類.

1, YYTransaction,負責將 YYAsyncLayer委托的繪制任務在RunLoop空閑時執(zhí)行.

2, YYSentine, 是一個線程安全的計數(shù)器,在進行隊列分配和任務取消時作為參考使用

3, YYAsyncLayer, 將其替換為View的Layer類,實現(xiàn)異步繪制

YYTransaction

+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector;方法創(chuàng)建委托對象.

- (void)commit;方法將委托對象存儲在一個全局Set中,在空閑時回調(diào).

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);
    });
}

空閑回調(diào)block

// 因為該對象還要被存放至集合中,當子類實現(xiàn)了isEqual方法時,則同時也要實現(xiàn) hash方法.

- (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;
}

在這里重載 isEqual方法,確保不會將具有相同target和selector的委托對象放入Set中

static NSMutableSet *transactionSet = nil;

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
    }];
}

創(chuàng)建唯一的 主線程 RunLoop觀察者,在RunLoop進入kCFRunLoopBeforeWaiting 或 退出時 將委托方法調(diào)用.

YYSentine

YYSentine的實現(xiàn)比較簡單,主要是對 OSAtomicIncrement32() 函數(shù)的封裝, 改函數(shù)為一個線程安全的計數(shù)器, 它會會保證在 數(shù)自增后再對其訪問, 在這個框架里他是用來 作為繪制任務是否被取消的參照物的~

#import "YYSentinel.h"
#import <libkern/OSAtomic.h>

@implementation YYSentinel {
    int32_t _value;
}

- (int32_t)value {
    return _value;
}

- (int32_t)increase {
    return OSAtomicIncrement32(&_value);
}
@end

YYAsyncLayer

隊列準備

#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 = 運行該進程的系統(tǒng)的處于激活狀態(tài)的處理器數(shù)量,
        queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
        //確保 0<queueCount<16
        queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount;

        //創(chuàng)建指定數(shù)量的 串行隊列 存放在隊列數(shù)組中
        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));
            }
        }
    });

    // 此為線程安全的自增計數(shù),每調(diào)用一次,+1
    int32_t cur = OSAtomicIncrement32(&counter);

    NSLog(@"cur:%d counter:%d",cur,counter);

    //返回合適的隊列
    if (cur < 0) cur = -cur;
    return queues[(cur) % queueCount];
#undef MAX_QUEUE_COUNT
#endif
}

準備若干的串行隊列~,將繪制任務分給不同的串行隊列, 這里之所以 隊列數(shù) 和 處理器數(shù) 匹配. 不創(chuàng)建過多無效隊列.

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

低優(yōu)先級的全局隊列作為 對象的釋放隊列,

代理方法

YYAsyncLayer的代理方法需要返回一個 DisplayTask對象, 任務對象中包括3個block.分別為willDisplay , display , didDisplay.在繪制的不同階段執(zhí)行

初始化

- (instancetype)init {
    self = [super init];
    static CGFloat scale; //global
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        scale = [UIScreen mainScreen].scale;
    });
    self.contentsScale = scale;
    //默認異步,每個圖層都配置一個計數(shù)器
    _sentinel = [YYSentinel new];
    _displaysAsynchronously = YES;
    return self;
}

//dealloc時 取消繪制
- (void)dealloc {
    [_sentinel increase];
}

//在再次繪制時,取消上次繪制任務
- (void)setNeedsDisplay {
    [self _cancelAsyncDisplay];
    [super setNeedsDisplay];
}
- (void)display {
    //這個我看不懂~為啥要再賦值一遍
    super.contents = super.contents;
    [self _displayAsync:_displaysAsynchronously];
}

繪制方法

沒有繪制任務

    if (!task.display) {
        if (task.willDisplay) task.willDisplay(self);
        self.contents = nil;
        if (task.didDisplay) task.didDisplay(self, YES);
        ///執(zhí)行完其他非空block后 返回
        return;
    }

異步繪制

      if (task.willDisplay) task.willDisplay(self);
        YYSentinel *sentinel = _sentinel;

        int32_t value = sentinel.value;

        //判斷是否要取消的block, 在圖層的dealloc方法,取消繪制方法中 和 同步繪制方法中 進行線程安全的自增操作. 在調(diào)用該block時 若block截取的變量value與對象中value中的值不一致時,則表明當前任務以被取消
        BOOL (^isCancelled)(void) = ^BOOL() {
            return value != sentinel.value;
        };

        CGSize size = self.bounds.size;
        BOOL opaque = self.opaque;
        CGFloat scale = self.contentsScale;

        // 當圖層寬度 或 高度小于 1時 (此時沒有繪制意義)
        if (size.width < 1 || size.height < 1) {
            CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
            self.contents = nil;

            //當圖層內(nèi)容為圖像時,講釋放操作留在 并行釋放隊列中進行
            if (image) {
                dispatch_async(YYAsyncLayerGetReleaseQueue(), ^{
                    CFRelease(image);
                });
            }
            if (task.didDisplay) task.didDisplay(self, YES);
            return;
        }

        ///為正常情況
        dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{
            //若發(fā)生取消操作,則取消繪制
            if (isCancelled()) return;


            UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
            CGContextRef context = UIGraphicsGetCurrentContext();


            task.display(context, size, isCancelled);

            //若取消 則釋放資源,取消繪制
            if (isCancelled()) {
                UIGraphicsEndImageContext();
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (task.didDisplay) task.didDisplay(self, NO);
                });
                return;
            }

            //將上下文轉(zhuǎn)換為圖片
            UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
            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 {
                    self.contents = (__bridge id)(image.CGImage);
                    if (task.didDisplay) task.didDisplay(self, YES);
                }
            });
        });

YYAsyncLayer 是通過創(chuàng)建異步創(chuàng)建圖像Context在其繪制,最后再主線程異步添加圖像 從而實現(xiàn)的異步繪制.同時,在繪制過程中 進行了多次進行取消判斷,以免額外繪制.

同步繪制

同步繪制就是直接繪制就好了~

        [_sentinel increase];
        if (task.willDisplay) task.willDisplay(self);
        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);

使用

替換當前View的Layer

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

修改需要屬性時 進行重繪制

- (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 {
    [self.layer setNeedsDisplay];
}

實現(xiàn)代理方法 完成繪制任務

- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {

    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;
        //在這里由于繪制文字會顛倒
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            CGContextTranslateCTM(context, 0, self.bounds.size.height);
            CGContextScaleCTM(context, 1.0, -1.0);
        }];
        NSAttributedString* str = [[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName:_font}];
        CGContextSetTextPosition(context, 0, font.pointSize);
        CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)str);
        CTLineDraw(line, context);
    };

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

    return task;
}

效果

作為對照,添加UILabel進行對比試驗,重寫其 -(void)drawRect:方法打印輸出比較.

screenshot.png

由此可知: 同步繪制任務(la2) 在viewDidAppear前完成繪制, 而AsyncLayer則在這之后再開始繪制任務,切繪制方法在異步執(zhí)行.

原生API對比.

關(guān)于異步繪制,iOS6 為CALayer添加了新的API drawsAsynchronously 屬性.當你設(shè)置 drawsAsynchronously = YES 后,-drawRect: 和 -drawInContext: 函數(shù)依然實在主線程調(diào)用的。但是所有的Core Graphics函數(shù)(包括UIKit的繪制API,最后其實還是Core Graphics的調(diào)用)不會做任何事情,而是所有的繪制命令會被在后臺線程處理。

這種方式就是先記錄繪制命令,然后在后臺線程執(zhí)行。為了實現(xiàn)這個過程,更多的事情不得不做,更多的內(nèi)存開銷。最后只是把一些工作從主線程移動出來。這個過程是需要權(quán)衡,測試的。

這個可能是代價最昂貴的的提高繪制性能的方法,也不會節(jié)省很多資源。

相比之下,AsyncLaye的性能會好一些, 但麻煩的是 繪制實現(xiàn)要自己寫~~~~~

錯誤提示

在我使用的版本(1.0)中 異步繪制的bitmap的scale為1.0 因為

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 從哪說起呢? 單純講多線程編程真的不知道從哪下嘴。。 不如我直接引用一個最簡單的問題,以這個作為切入點好了 在ma...
    Mr_Baymax閱讀 2,902評論 1 17
  • 1.介紹下內(nèi)存的幾大區(qū)域? 2.你是如何組件化解耦的? 3.runtime如何通過selector找到對應的IMP...
    小孩仔閱讀 1,814評論 0 21
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,574評論 30 472
  • 阿里-p6-一面 1.介紹下內(nèi)存的幾大區(qū)域? 2.你是如何組件化解耦的? 3.runtime如何通過selecto...
    CaptainMi閱讀 915評論 0 1
  • 對于我這個牌痞,最遠大的目標就是天天打麻將,可是每次打麻將之前雄赳赳氣昂昂的,如同一個戰(zhàn)勝的將軍,可是回來的時候恰...
    你猜我叫嘛閱讀 782評論 0 0

友情鏈接更多精彩內(nèi)容