源码学习-YYAsyncLayer

YYAsyncLayer 是进行视图异步渲染的一个库。它的主要实现思路是

  1. 替换原有的 UIView 视图的 CALayer 类为 YYAsyncLayer
  2. YYAsyncLayer 里面的绘制方法里,创建异步绘制的上下文进行异步绘制,将具体的视图绘制外包给原有的视图去做,最后在异步绘制得到 Image 图片,通过主线程赋值给 CALayercontents 属性。
  3. 绘制的时机还是通过观察 Runloop 对应的时间节点,在指定的时间节点,将提交给数组中的绘制任务完成。

# YYAsyncLayer 使用方式

直接贴了 YYAsyncLayer 的使用方式

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

@implementation YYLabel

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

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

#pragma mark - YYAsyncLayer

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

- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {    
    // capture current state to display task
    NSString *text = _text;
    YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
    task.willDisplay = ^(CALayer *layer) {
        //...
    };
    task.display = ^(CGContextRef context, CGSize size, BOOL(^isCancelled)(void)) {
        //绘制任务
    };
    task.didDisplay = ^(CALayer *layer, BOOL finished) {
        //
    };
    return task;
}
@end
  1. 自定制类复写 +(Class)layerClass 方法,替换原有默认的 CALayer 类为库中的 YYAsyncLayer 类。
  2. 实现协议方法 - (YYAsyncLayerDisplayTask *)newAsyncDisplayTask 返回异步绘制任务类 YYAsyncLayerDisplayTask。这个类中最关键的就是 display 回调方法,就是在这个方法里面完成了提交给子线程的异步绘制任务。
  3. 当修改原始视图内容的时候,通过 YYTransaction 类提交修改任务,最终调用 YYAsyncLayerdisplay 方法

# YYAsyncLayer

_displayAsync 方法说明

YYAsyncLayer 类中 _displayAsync 方法是具体异步绘制核心的方法,主要就是从代理视图获取到具体的绘制任务实例,用作 YYAsyncLayer 绘制的回调方法。

该方法简单实现如下:

YYAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];
task.willDisplay(self);
dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{
    UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    task.display(context, size, isCancelled);
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    dispatch_async(dispatch_get_main_queue(), ^{
			  self.contents = (__bridge id)(image.CGImage);
        if (task.didDisplay) task.didDisplay(self, YES);
    });
});

在子线程中通过 UIGraphicsBeginImageContextWithOptions 创建一个基于位图的绘图上下文,然后拿到该绘图上下文之后,回调给代理(通常是视图)层去完成具体的绘制操作,最后将绘制好的内容通过上下文转成图片,在回调的主线程将图片内容赋值给 Layer 的 contents 属性。

这个绘制的过程和 CoreAnimation 进行绘制的流程几乎一模一样,在 CoreAnimation 的绘制过程中也是 CA 负责创建 backing store (我理解其实也是上下文的意思),然后再 UIViewdrawRect: 方法中直接获取上下文,进行绘制。所以 task 的 display 方法和 UIViewdrawRect: 方法本质上一模一样。只不过是 task 的 display 方法是在一个异步的线程中执行的,当然这也就是 YYAsyncLayer 库实现的目的。

YYAsyncLayerGetDisplayQueue 方法说明

这个方法用来获取渲染线程队列。作者实现这个方法的原因是「使用 concurrent queue 时不可避免会遇到这种问题,但使用 serial queue 又不能充分利用多核 CPU 的资源」,所以创建多个串行队列,来供渲染时取用。

这里的关键是,设置多少个串行队列?YY 给出的答案是根据系统当前活跃的 CPU 核数 activeProcessorCount ,同时每次取队列池中的队列都保证和上一次渲染时的串行队列不同,这样便能利用多核的 CPU 资源,有点类似线程池,但是队列和线程并不是一个意思。

queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount;

而且串行队列的优先级都是 user interactive,表示线队列的优先级还是挺高的。

user interactive 用户交互的任务,通常和UI有关

# YYTransaction

这个类显然是参考了 CA:Transaction 的命名。YYTransaction 的主要职责是存储不同视图绘制任务,并且在给定的时间节点去触发绘制任务。

首先在初始化配置方法里去创建 Runloop 观察者,观察 kCFRunLoopBeforeWaiting, kCFRunLoopExit 这两个时间节点,并配置对应的触发方法 YYRunLoopObserverCallBack

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) {
        [transaction.target performSelector:transaction.selector];
    }];
}
static void YYTransactionSetup() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        transactionSet = [NSMutableSet new];
        CFRunLoopRef runloop = CFRunLoopGetMain();
        CFRunLoopObserverRef observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting | kCFRunLoopExit, true, 0xFFFFFF, YYRunLoopObserverCallBack, NULL);
        CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
        CFRelease(observer);
    });
}

当视图提交修改后,想要触发的话,视图就通过下面的方法,将绘制任务提交到全局 transactionSet 数组中,在上面的 Runloop 中取出数组中存储的 YYTransaction 实例,并执行里面存储的绘制任务。绘制任务当然最终还是要触发到 YYAsyncLayer_displayAsync 方法。

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

# YYSentinel

本质上就是操作一个全局的计数器。不在 YYAsyncLayer 中直接使用静态变量的原因,我感觉还是出于线程安全的考虑。

基本上就是这些吧,感觉 YY 大神真的对系统绘制的这套东西门清,才能写出和系统绘制思路如此相似的类,牛逼。

参考地址:

  1. Github-YYAsyncLayer (opens new window)
  2. iOS 保持界面流畅的技巧 (opens new window)
  3. 开源学习之YYAsyncLayer (opens new window)