YYAsyncLayer 學習


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


YYAsyncLayer 學習

簡介

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

組件內容

YYAsyncLayer主要有3個類.

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

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

3, YYAsyncLayer, 將其替換為View的Layer類,實現異步繪制

YYTransaction

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

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

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

空閑回調block

// 因為該對象還要被存放至集合中,當子類實現了isEqual方法時,則同時也要實現 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
    }];
}

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

YYSentine

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

#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 = 運行該進程的系統的處于激活狀態的處理器數量,
        queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
        //確保 0<queueCount<16
        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));
            }
        }
    });

    // 此為線程安全的自增計數,每調用一次,+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
}

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

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

低優先級的全局隊列作為 對象的釋放隊列,

代理方法

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

初始化

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

//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);
        ///執行完其他非空block后 返回
        return;
    }

異步繪制

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

        int32_t value = sentinel.value;

        //判斷是否要取消的block, 在圖層的dealloc方法,取消繪制方法中 和 同步繪制方法中 進行線程安全的自增操作. 在調用該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;

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

        ///為正常情況
        dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{
            //若發生取消操作,則取消繪制
            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;
            }

            //將上下文轉換為圖片
            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 是通過創建異步創建圖像Context在其繪制,最后再主線程異步添加圖像 從而實現的異步繪制.同時,在繪制過程中 進行了多次進行取消判斷,以免額外繪制.

同步繪制

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

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

實現代理方法 完成繪制任務

- (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則在這之后再開始繪制任務,切繪制方法在異步執行.

原生API對比.

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

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

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

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

錯誤提示

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

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

推薦閱讀更多精彩內容

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