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


iOS的異步渲染

最近看了YYAsyncLayer在這里總結一下。YYAsyncLayer是整個YYKit異步渲染的基礎。整個項目的Github地址在這里。你可以先下載了一睹為快,也可以跟着我一步一步的了解它是怎么實現異步繪制的。

如何實現異步

兩種方式可以實現異步。一種是使用另外的一個線程,一種是使用RunLoop。另外開一個線程的方法有很多,但是現在最方便的就死GCD了。

GCD

這里介紹一些GCD里常用的方法,為了后面閱讀的需要。還有YYAsyncLayer中用到的更加高級的用法會在下文中深入介紹。

創建一個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是串行執行的。

dispatch_once

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

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

我們來分析一下這段代碼

  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

指定為16,我也是有些疑惑的。在Android里指定線程池的大小的時候通常的值是CPU的內核個數的兩倍。

設計,把點連成線

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
``
在實現的時候是這樣的:
```objc
#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方法的調用。

這樣一個調用鏈就形成了:用戶操作->[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]開始的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM