iOS的異步繪製--YYAsyncLayer源碼分析

iOS的異步渲染

最近看了YYAsyncLayer在這裏總結一下。YYAsyncLayer是整個YYKit異步渲染的基礎。整個項目的Github地址在這裏。你能夠先下載了一睹爲快,也能夠跟着我一步一步的瞭解它是怎麼實現異步繪製的。git

如何實現異步

兩種方式能夠實現異步。一種是使用另外的一個線程,一種是使用RunLoop。另外開一個線程的方法有不少,可是如今最方便的就是GCD了。github

GCD

這裏介紹一些GCD裏經常使用的方法,爲了後面閱讀的須要。還有YYAsyncLayer中用到的更加高級的用法會在下文中深刻介紹。app

建立一個queue

dispatch_queue_t queue;
if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
  dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
  queue = dispatch_queue_create("com.ibireme.yykit.render", attr);
} else {
  queue = dispatch_queue_create("com.ibireme.yykit.render", DISPATCH_QUEUE_SERIAL);
  dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
}

若是iOS 8和以上版本的話,建立queue的方法和以前的版本的不太太同樣。在iOS 8和以上的版本中建立queue須要先建立一個dispatch_queue_attr_t類型的實例。並做爲參數傳入到queue的生成方法裏。異步

DISPATCH_QUEUE_SERIAL說明在這個queue內部的task是串行執行的。async

dispatch_set_target_queue

dispatch_set_target_queue 有兩個做用:工具

  1. 設定建立的queue的優先級。
  2. 讓多個serial的queue的任務由並行的變爲在target queue內是串行的。

這裏主要的做用是第一個。也就是把dispatch_queue_create建立的queue的優先級設置爲和dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)爲同一優先級。oop

蘋果的文檔在這裏動畫

dispatch_once

使用dispatch_oncedispatch_once_t的組合能夠實現其中的task只被執行一次。可是有一個前提條件,看代碼:ui

static dispatch_once_t onceToken; // 1

// 2
dispatch_once(&onceToken, ^{
  // 這裏的task只被執行一次
});
  1. 這裏的dispatch_once_t必須是靜態的。也就是要有APP同樣長的生存期來保證這段時間內task只被執行一次。若是不是static的,那麼只被執行一次是保證不了的。
  2. dispatch_once方法在這裏執行,onceToken在這裏有一個取地址的操做。也就是onceToken把地址傳入方法內部被初始化和賦值。

RunLoop

CFRunLoopRef runloop = CFRunLoopGetMain();  // 1
CFRunLoopObserverRef observer;
// 2
observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                    kCFRunLoopBeforeWaiting | kCFRunLoopExit,
                                    true,      // repeat
                                    0xFFFFFF,  // after CATransaction(2000000)
                                    YYRunLoopObserverCallBack, NULL);
// 3
CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes); 
CFRelease(observer);

咱們來分析一下這段代碼spa

  1. CFRunLoopGetMain方法返回主線程的RunLoop引用。後面用這個引用來添加回調。
  2. 使用系統內置的c方法建立一個RunLoop的觀察者,在建立這個觀察者的時候回同時指定回調方法。
  3. RunLoop實例添加觀察者,以後減小一個觀察者的引用。

在第二步建立觀察者的時候,還指定了觀察者觀察的事件:kCFRunLoopBeforeWaiting | kCFRunLoopExit,在
RunLoop進入等待或者即將要退出的時候開始執行觀察者。指定了觀察者是否重複(true)。指定了觀察者的優先級:0xFFFFFF,這個優先級比CATransaction優先級爲2000000的優先級更低。這是爲了確保系統的動畫優先執行,以後再執行異步渲染。

YYRunLoopObserverCallBack就是觀察者收到通知的時候要執行的回調方法。這個方法的聲明是這樣的:

static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);

渲染是怎麼回事

渲染就是把咱們代碼裏設置的代碼的視圖和數據結合,最後繪製成一張圖呈如今用戶的面前。每秒繪製60張圖,用戶看着就是流暢的界面呈現,若是不到60幀,那麼用戶看到的幀數越少就會越卡。

CALayer

在iOS中,最終咱們看到的視圖都是在CALayer裏呈現的,在CALayer有一個屬性叫作contents,這裏不放別的,放的就是顯示用的一張圖。

咱們來看看YYAsyncLayer類的代碼:

// 類聲明
  @interface YYAsyncLayer : CALayer // 1
  /// Whether the render code is executed in background. Default is YES.
  @property BOOL displaysAsynchronously;
  @end

  //類實現的一部分代碼
  UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
  UIGraphicsEndImageContext();  // 2
  // ...
  dispatch_async(dispatch_get_main_queue(), ^{
      self.contents = (__bridge id)(image.CGImage); // 3
  });
  1. YYAsyncLayer繼承自CALayer
  2. UIGraphicsGetImageFromCurrentImageContext這是一個CoreGraphics的調用,是在一些繪製以後返回組成的圖片。
  3. 在2>中生成的圖片,最終被賦值給了CALahyer#contents屬性。

CoreGraphics

若是說CALayer是一個繪製結果的展現,那麼繪製的過程就要用到CoreGraphics了。

在正式開始之前,首先須要瞭解一個方法的實現。這個方法會用來繪製具體的界面上的內容:

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

你也看到了,這其實不是一個方法而是一個block。這個block會使用傳入的CGContextRef context參數來繪製文字。

目前瞭解這麼多就足夠了,後面會有詳細的介紹。

YYAsyncLayer#_displayAsync方法是如何繪製的,_displayAsync是一個「私有方法」。

//這裏咱們只討論異步的狀況
// 1
CGSize size = self.bounds.size;
BOOL opaque = self.opaque;
CGFloat scale = self.contentsScale;
CGColorRef backgroundColor = (opaque && self.backgroundColor) 
  ? CGColorRetain(self.backgroundColor) : NULL;

dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{  // 2
  UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
  CGContextRef context = UIGraphicsGetCurrentContext();
  // 3
  if (opaque) {
    CGContextSaveGState(context); {
      if (!backgroundColor || CGColorGetAlpha(backgroundColor) < 1) {
        CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
        CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
        CGContextFillPath(context);
      }
      if (backgroundColor) {
        CGContextSetFillColorWithColor(context, backgroundColor);
        CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
        CGContextFillPath(context);
      }
    } CGContextRestoreGState(context);
    CGColorRelease(backgroundColor);
  }
  task.display(context, size, isCancelled);   // 4

  // 5
  UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
  UIGraphicsEndImageContext();

  // 6
  dispatch_async(dispatch_get_main_queue(), ^{
    self.contents = (__bridge id)(image.CGImage);
  });
});

解釋以下:

  1. 準備工做,獲取size, opaque, scalebackgroundColor這個四個值。這些在獲取繪製的取悅的時候用到。背景色另外有處理。
  2. YYAsyncLayerGetDisplayQueue()方法返回一個dispatch_queue_t實例,並在其中開始異步操做。
  3. 判斷opaque的值,若是是非透明的話處理背景色。這個時候就會用到第一步裏獲取到的backgroundColor變量的值。
  4. CoreGraphics一節開始的時候講到的繪製具體內容的block。
  5. 繪製完畢,獲取到UIImage實例。
  6. 返回主線程,並給contents屬性設置繪製的成果圖片。至此異步繪製所有結束。

爲了讓讀者更加關注異步繪製這個主題,因此省略了部分代碼。生路的代碼中不少事檢查是否取消的。異步的繪製,尤爲是在一個滾動的UITableView或者UICollectionView中隨時均可能會取消,因此即便的檢查是否取消並終止正在進行的繪製頗有必要。這些,你會在完整的代碼中看到。

不能無限的開闢線程

咱們都知道,把阻塞主線程執行的代碼放入另外的線程裏保證APP能夠及時的響應用戶的操做。可是線程的切換也是須要額外的開銷的。也就是說,線程不能無限度的開闢下去。

那麼,dispatch_queue_t的實例也不能一直增長下去。有人會說能夠用dispatch_get_global_queue()來獲取系統的隊列。沒錯,可是這個狀況只適用於少許的任務分配。由於,系統自己也會往這個queue裏添加任務的。

因此,咱們須要用本身的queue,可是是有限個的。在YY裏給這個數量指定的最大值是16。它會首先判斷CPU的核數(int)[NSProcessInfo processInfo].activeProcessorCount。若是核數大於給定的最大值則使用最大值。

開闢線程的時候使用的是YYKit裏本身的一套「線程池」工具來控制開闢的線程數量的。

設計,把點連成線

YYAsyncLayer異步繪製的過程就是一個觀察者執行的過程。所謂的觀察者就是你設置了一個機關,當它被觸發的時候能夠執行你預設的東西。好比你走到一扇門前,它感應到了你的紅外輻射就會打開。

async layer也是同樣,它會把「感應器」放在run loop裏。當run loop要閒下來的時候「感應器」的回調開始執行,告訴async layer能夠開始異步渲染了。

可是異步渲染要幹什麼呢?咱們如今就來講說異步渲染的內容從哪裏來?一個須要異步渲染的view會在定義的時候就把須要異步渲染的內容經過layer保存在view的代理髮送給layer。

CALayer和UIView的關係

UIView是顯示層,而顯示在屏幕上的內容是由CALayer來管理的。CALayer的一個代理方法能夠在UIView宿主裏實現。

YYAsyncLayer用的就是這個方式。代理爲:

@protocol YYAsyncLayerDelegate <NSObject>
@required
/// This method is called to return a new display task when the layer's contents need update.
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask;
@end

在實現的時候是這樣的:

#pragma mark - YYTextAsyncLayerDelegate

- (YYTextAsyncLayerDisplayTask *)newAsyncDisplayTask {
  // 1
  YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new]; 

  // 2 
  task.willDisplay = ^(CALayer *layer) {
    // ...
  }

  // 3 
  task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) {
    // ...
  }

  // 4 
  task.didDisplay = ^(CALayer *layer, BOOL finished) {
    // ...
  }

  return task;
}
  1. 建立了YYAsyncLayerDisplayTask對象
  2. 設置task的willDisplayblock回調。 3. 4.分別設置了其餘的display回調block。

可見YYAsyncLayer的代理的實現會建立一個YYAsyncLayerDisplayTask的實例並返回。在這個實例中包含了layer顯示順序的回調:willDisplaydisplaydidDisplay

setNeedsDisplay

CALayer實例調用setNeedsDisplay方法以後CALayerdisplay方法就會被調用。YYAsyncLayer重寫了display方法:

- (void)display {
  super.contents = super.contents;
  [self _displayAsync:_displaysAsynchronously];
}

最終會調用YYAsyncLayer實例的display方法。display方法又會調用到_displayAsync:方法,開始異步繪製的過程。

總結

最後,咱們把整個異步渲染的過程來串聯起來。

對一個包含了YYAsyncLayer的view,好比YYLable就像文檔裏的同樣。重寫layoutSubviews方法添加對layer的setNeedsDisplay方法的調用。

這樣一個調用鏈就造成了:用戶操做->RunLoop(Waiting | Exiting)->調用observer的回調->[view layoutSubviews]->[view.layer setNeedsDisplay]->[layer display]->[layer _displayAsync]異步繪製開始(準確的說是_displayAsync方法的參數爲true**的時候開始異步繪製)。

可是這並無用到RunLoop。因此代碼會修改成每次調用layoutSubviews的時候給RunLoop提交一個異步繪製的任務:

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

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

這樣每次RunLoop要進入休眠或者即將退出的時候會開始異步的繪製。這個任務是從[layer setNeedsDisplay]開始的。

相關文章
相關標籤/搜索