关于YYAsyncLayer的简单总结。


一、背景

YYAsyncLayer是CALayer的子类,用于在子线程异步绘制与显示视图的内容,避免造成主线程卡顿。

卡顿的原理

屏幕显示内容的原理?
CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

为什么会有垂直同步机制?
显示系统通常会引入两个缓冲区,即双缓冲机制。在这种情况下,GPU 会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU 会直接把视频控制器的指针指向第二个缓冲器。但是当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象。为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。

卡顿的原因?
在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

CPU资源消耗的原因有哪些?
对象创建(复杂对象的创建)、对象调整(修改属性、调整视图层次)、大量对象销毁、布局计算、应用在复杂视图上的Autolayout、文本的宽高计算、大量文本的渲染、图片的解码、图像的绘制(绘制到画布中)。

GPU资源消耗的原因有哪些?
GPU 能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。
常见的资源消耗有较短时间显示大量图片时、多个视图(或者说 CALayer)重叠在一起显示、离屏渲染(CALayer 的 border、圆角、阴影、遮罩,CASharpLayer 的矢量图形显示,通常会触发离屏渲染,而离屏渲染通常发生在 GPU 中)。

YYAsyncLayer解决的问题?

结合实际情况,当遇到大量文本计算、图片的解码和绘制、多个视图的合成等情况,就可以使用YYAsyncLayer,在子线程进行这些操作,从而避免卡顿。

二、使用

官方的示例是通过自建UIView绘制大量文字。使用起来只需要三步:
(1)重写UIView的layerClass方法,使用YYAsyncLayer来创建UIView的Core Animation Layer。(The class used to create the view’s Core Animation layer.)
(2)在合适的时机,标记内容需要重绘(contentsNeedUpdated),比如设置字体时、设置文本时、layoutSubviews被调用时等。
(3)返回YYAsyncLayerDisplayTask实例,并在其中定义绘制的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@interface YYLabel : UIView
@property NSString *text;
@property UIFont *font;
@end
@implementation YYLabel
- (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 {
// do update
[self.layer setNeedsDisplay];
}

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

- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
// capture current state to display task
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;
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;
}
};

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

return task;
}
@end

三、实现

1,YYSentinel

一个线程安全的计数器。内部维护了一个int32_t类型的实例变量,它依赖于原子函数OSAtomicIncrement32()实现线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface YYSentinel : NSObject
@property (readonly) int32_t value;
- (int32_t)increase;
@end
@implementation YYSentinel {
int32_t _value;
}

- (int32_t)value {
return _value;
}

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

所以如果我们需要一个线程安全的单纯的计数器,不妨使用OSAtomicIncrement32()实现,而不必自己加锁。但是如果要在计数改变的时候加一些自己的业务,那这种方式就不可行了。

2,YYTransaction

YYTransaction只有两个方法接口。
方法一:包装targetselector并生成一个YYTransaction对象。
方法二:commit这个YYTransaction对象。

包装成YYTransaction对象的方法自不必说,其内部使用实例变量引用着传递的参数。对于commit方法,它内部使用全局的transactionSet集合添加了这个YYTransaction对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@interface YYTransaction : NSObject
+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector;
- (void)commit;
@end
@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;
}

// 把生成的YYTransaction对象放入transactionSet中
- (void)commit {
if (!_target || !_selector) return;
YYTransactionSetup();
[transactionSet addObject:self];
}

// isEqual: 是通过hash方法来判等的
- (NSUInteger)hash {
long v1 = (long)((void *)_selector);
long v2 = (long)_target;
return v1 ^ v2;
}

// 只要_target和_selector都一致,就说明'YYTransaction'是同一个
- (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;
}
@end

此外,YYTransaction还重写了- (NSUInteger)hash方法和- (BOOL)isEqual:(id)object方法。我们知道,如果两个对象相等,他们的hash值必须相等,如果某个类自定义了isEqual方法,并且这个类的实例有可能会被加入到集合中,一点要确保hash方法被重新定义。

重写- (BOOL)isEqual:(id)object方法:只要YYTransaction对象的target和selector一致,就判定这两个YYTransaction对象是一致的。
重写- (NSUInteger)hash方法:hash值是对象判等的必要非充分条件,基于hash的NSSet和NSDictionary在判断成员是否相等时, 会首先判断集合成员的hash值和目标对象的hash值是否相等, 如果相等再进行对象判等。

一般hash方法的重写逻辑是:对关键属性的hash值进行异或运算作为hash值:

1
2
3
- (NSUInteger)hash {
return [self.name hash] ^ [self.category hash];
}

特殊地,作者这里直接用_selector_target的地址做异或:

1
2
3
4
5
- (NSUInteger)hash {
long v1 = (long)((void *)_selector);
long v2 = (long)_target;
return v1 ^ v2;
}

3,YYTransactionSetup函数

当对YYTransaction对象commit的时候会调用YYTransactionSetup()函数,这个函数只执行一次。这个函数的作用是,每次在主运行循环切换到kCFRunLoopBeforeWaitingkCFRunLoopExit这两种状态下会调用YYRunLoopObserverCallBack()函数。也即每当Main Runloop即将进入休眠和退出的时候,调用YYRunLoopObserverCallBack()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
// 添加监听者到kCFRunLoopCommonModes模式,Runloop持有这个observer
CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
// 带有Creat、Copy、Retain等名字的函数创建出来的对象,需要在最后做一次release
CFRelease(observer);
});
}

其实就是给主运行循环添加一个闲时任务,这个任务的优先级是0xFFFFFF,换成10进制就是16777215,比2000000大。设置任务优先级的参数类型是CFIndextypedef long CFIndex),数值越高表示优先级越低,0为最高优先级别,默认情况下使用0。

Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个 Observer 的优先级是 2000000。
当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。

那么我们明白了,这个任务比Core Animation的这个任务优先级低,是为了避免打乱“中间状态合并”操作。

再看看YYRunLoopObserverCallBack()函数:遍历transactionSet集合中的所有元素,使其target调用对应的selector

1
2
3
4
5
6
7
8
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];
}];
}

总结一下,外界通过传入target参数和selector参数创建一个YYTransaction,target和selector共同确定唯一的一个YYTransaction对象。调用YYTransaction的commit方法可以使得Main Runloop在即将进入休眠和退出的时候调用一次target的selector方法。

4,YYAsyncLayerDisplayTask

YYAsyncLayerDisplayTask类比较简单,只有三个属性,分别是内容即将展示的block、内容展示的block、内容展示完毕的block。外界把这三个操作传进来即可。

1
2
3
4
5
6
7
@interface YYAsyncLayerDisplayTask : NSObject
@property (nullable, nonatomic, copy) void (^willDisplay)(CALayer *layer);
@property (nullable, nonatomic, copy) void (^display)(CGContextRef context, CGSize size, BOOL(^isCancelled)(void));
@property (nullable, nonatomic, copy) void (^didDisplay)(CALayer *layer, BOOL finished);
@end
@implementation YYAsyncLayerDisplayTask
@end

5,YYAsyncLayer

CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等

YYAsyncLayer继承自CALayer,对外只暴露一个属性:displaysAsynchronously,表示是否开启异步绘制,默认是YES。

1
2
3
4
@interface YYAsyncLayer : CALayer
// Default is YES.
@property BOOL displaysAsynchronously;
@end

在YYAsyncLayer的构造方法中,初始化了计数器、设置displaysAsynchronously属性为YES,并设置CALayer的contentsScale和屏幕缩放比例一致。(contentsScale属性用于支持高分辨率屏幕,如果contentsScale设置为1.0,将会以每个点1个像素绘制图片,如果设置为2.0,则会以每个点2个像素绘制图片。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (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];
// 默认是YES
_displaysAsynchronously = YES;
return self;
}

这里的计数器作为标记位存在,通过改变计数器的值,表明CALayer的状态被改变了,这样就需要取消上次的绘制逻辑,开始新的绘制。所以可以看到_cancelAsyncDisplay方法其实就是对计数器加一。那么也不难理解,重写setNeedsDisplay方法,为啥要调用_cancelAsyncDisplay了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)dealloc {
// layer销毁的时候, 计数器加1, 表示原先的绘制需要取消
[_sentinel increase];
}

- (void)setNeedsDisplay {
// 被标记需要重新绘制,取消上次的异步调用
[self _cancelAsyncDisplay];
[super setNeedsDisplay];
}

- (void)_cancelAsyncDisplay {
// increase增加1, 与目前的不一致了, 表示目前的渲染已取消
[_sentinel increase];
}

除了计数器逻辑,YYAsyncLayer还重写了defaultValueForKey:方法。

官方解释:

CAAnimation和CALayer类是符合键值编码的容器类,这意味着可以给任意键赋值和取值。Core Animation 为键值编码添加了一个约定,通过该约定,类可以为没有赋值的键提供默认值。CAAnimation和CALayer 类通过使用defaultValueForKey:类方法来支持此约定。若要为键提供默认值,请创建所需类的子类,并重写其defaultValueForKey:方法。

If you define custom properties for a layer but do not set a value, this method returns a suitable “zero” default value based on the expected value of the key.
If key is not a known for property of the class, the result of the method is undefined.

1
2
3
4
5
6
7
+ (id)defaultValueForKey:(NSString *)key {
if ([key isEqualToString:@"displaysAsynchronously"]) {
return @(YES);
} else {
return [super defaultValueForKey:key];
}
}

所以,当没有初始化属性的值的时候,重写这个方法可以提供这个属性的默认值。但是如果这个key不是类的已知属性,那么这个方法的结果是无效的。

最核心的还是绘制逻辑,先看一下CALayer的display方法,这个方法用于加载Layer的内容(Reloads the content of this layer)。

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

这个方法的默认逻辑是:调用代理(UIView)的displayLayer:方法,从而实现内容更新。如若代理未实现这个方法,它会创建一个backing store,调用自己的drawInContext方法填充backing store,并用这个backing store替换原先的那个。子类可以重写这个方法,来直接设置contents属性。

The layer calls this method at appropriate times to update the layer’s content. If the layer has a delegate object, this method attempts to call the delegate’s displayLayer: method, which the delegate can use to update the layer’s contents. If the delegate does not implement the displayLayer: method, this method creates a backing store and calls the layer’s drawInContext: method to fill that backing store with content. The new backing store replaces the previous contents of the layer.Subclasses can override this method and use it to set the layer’s contents property directly.

这个backing store可以理解为一个合适尺寸的空寄宿图和一个Core Graphics的绘制上下文环境。

YYAsyncLayer重写了这个方法,通过_displayAsync:进行内容重绘,但在这之前有这样一句话:super.contents = super.contents;

先说contents属性,我们知道CALayer的contents属性虽然是id类型,但是只能赋值为CGImage类型,否则图层将是空白的。这里的super.contentsnil,这样赋值的意义是什么呢?

看一下核心逻辑:

结合之前的官方示例,外界自定义UIView的同时,需要实现newAsyncDisplayTask方法,返回一个YYAsyncLayerDisplayTask实例。这里的
self.delegate很明显,就是指外界自定义的UIView(CALayer的delegate是UIView)。

首先是判断task是否有设置display回调,没有就不做后续处理。

当异步绘制开关打开时,首先定义一个判断绘制被取消的block:isCancelled,即判断计数器的值是否改变了。上文已经说过,setNeedsDisplay等需要重绘的时候计数器的值会改变。
接着判断Layer的宽或者高小于1的情况,这种极端情况不做后续处理,把获取到的contents(如果有)设置为nil。这里有个巧妙的点,没有直接self.contents = nil;,而是把这个contents取出来,放在全局队列中做release操作,避免阻塞当前主线程。
然后就进入异步逻辑,开启图形上下文,进行背景色填充(没有背景色或者透明度小于1则使用白色填充),调用外界绘制逻辑(display回调),从当前上下文中获取生成的新内容(UIImage),切换到主线程对contents赋值。

在整个异步绘制操作中,有多达四次做了isCancelled判断,这样可以及早感知Layer的状态,及时取消绘制,尽可能多的节省CPU资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
- (void)_displayAsync:(BOOL)async {
// 强引用代理, 避免被释放
__strong id<YYAsyncLayerDelegate> delegate = (id)self.delegate;
// 获取一个Display Task
YYAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];
if (!task.display) { // 没有需要绘制的内容, 设置contents为空
if (task.willDisplay) task.willDisplay(self);
self.contents = nil;
if (task.didDisplay) task.didDisplay(self, YES);
return;
}

if (async) { // 异步绘制
if (task.willDisplay) task.willDisplay(self);
YYSentinel *sentinel = _sentinel;
int32_t value = sentinel.value;
BOOL (^isCancelled)(void) = ^BOOL() {
// 值相等, 说明正在绘制没有取消, 否则说明已经取消啦
return value != sentinel.value;
};
CGSize size = self.bounds.size;
BOOL opaque = self.opaque;
CGFloat scale = self.contentsScale;
// 获取背景色
CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL;
if (size.width < 1 || size.height < 1) { // 宽或者高小于1的情况
// 异步释放
CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
self.contents = nil;
if (image) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
CFRelease(image);
});
}
if (task.didDisplay) task.didDisplay(self, YES);
CGColorRelease(backgroundColor);
return;
}

// 从这里开始进入子线程
dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{
// 进入子线程后, 第1次取消绘制判断
if (isCancelled()) {
CGColorRelease(backgroundColor);
return;
}
// 开启图形上下文
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
if (opaque) {
// UIGraphicsPushContext:压栈当前的绘制对象, 生成新的绘制图层。UIKit的绘制必须在当前的上下文中绘制,而UIGraphicsPushContext可以将当前的参数context转化为可以UIKit绘制的上下文,进行绘制图片。
// CGContextSaveGState:压栈当前的绘制状态,这里是为了不"污染"原先的context内容, 保存状态后绘制新的内容
CGContextSaveGState(context);
{
// 没有背景色或者 透明度小于1, 使用白色填充
if (!backgroundColor || CGColorGetAlpha(backgroundColor) < 1) {
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
CGContextFillPath(context);
}
// 这里使用backgroundColor填充
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);
// display完毕后, 第2次取消绘制判断
if (isCancelled()) {
// 如果确定已经取消绘制了, 关闭图形上下文
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
if (task.didDisplay) task.didDisplay(self, NO);
});
return;
}
// 从当前上下文中获取生成的新内容
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 从上下文中获取图片后, 第3次取消绘制判断
if (isCancelled()) {
dispatch_async(dispatch_get_main_queue(), ^{
if (task.didDisplay) task.didDisplay(self, NO);
});
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
// 转到主线程对contents赋值,第4次取消绘制判断
if (isCancelled()) {
if (task.didDisplay) task.didDisplay(self, NO);
} else {
// 在这里对contents赋值
self.contents = (__bridge id)(image.CGImage);
if (task.didDisplay) task.didDisplay(self, YES);
}
});
});
} else {
// 修改计数器的值,表明进行了一次新的绘制。不修改的话,状态会紊乱。
[_sentinel increase];
// 下面的代码都是和异步绘制相同的套路
if (task.willDisplay) task.willDisplay(self);
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);
CGContextRef context = UIGraphicsGetCurrentContext();
if (self.opaque) {
CGSize size = self.bounds.size;
size.width *= self.contentsScale;
size.height *= self.contentsScale;
CGContextSaveGState(context); {
if (!self.backgroundColor || CGColorGetAlpha(self.backgroundColor) < 1) {
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
CGContextFillPath(context);
}
if (self.backgroundColor) {
CGContextSetFillColorWithColor(context, self.backgroundColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
CGContextFillPath(context);
}
} CGContextRestoreGState(context);
}
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);
}
}

对于同步绘制操作,逻辑是一致的,不用切换线程,总结下就是4步:

1
2
3
4
5
6
7
8
9
1)开启图形上下文
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);
CGContextRef context = UIGraphicsGetCurrentContext();
2)绘制
task.display(context, self.bounds.size, ^{return NO;});
3)取出绘制后的内容(图片)
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
4)关闭图形上下文
UIGraphicsEndImageContext();

异步绘制是切换到YYAsyncLayerGetDisplayQueue()队列完成的,看一下这个队列实现的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static dispatch_queue_t YYAsyncLayerGetDisplayQueue() {
#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, ^{
// 获取运行该进程的系统的处于激活状态的处理器数量, iPhone 6s 是2个
queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount;
for (NSUInteger i = 0; i < queueCount; i++) {
dispatch_queue_attr_t attr =
// QOS_CLASS_USER_INITIATED 用户发起并等待的优先级
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);
}
});
// counter自增一, 并取出该值
int32_t cur = OSAtomicIncrement32(&counter);
if (cur < 0) cur = -cur; // counter 为 INT_MAX时,溢出
return queues[(cur) % queueCount]; // 取余
#undef MAX_QUEUE_COUNT
}

作者通过创建与处理器核心相同数量的串行队列来轮询处理异步任务,实现并发效果。

为什么不用并行队列呢?
因为并行和并发是有区别的。在单核设备上,CPU通过频繁的切换上下文来运行不同的线程,速度足够快以至于我们看起来它是”并行“处理的,然而我们只能说这种情况是并发而非并行。使用并行队列(DISPATCH_QUEUE_CONCURRENT)并不能完全体现出多核处理器的优势。
实际上,一个n核设备同一时刻最多能”并行“执行n个任务,也就是最多有n个线程是相互不竞争CPU资源的。串行队列(DISPATCH_QUEUE_SERIAL)中只有一个线程,因此作者使用和处理器核心相同数量的串行队列来轮询处理异步任务,有效的减少了线程调度操作。

当使用并行队列时,无法精确的控制线程数量,很有可能创建过多的线程,如果开辟的线程过多,超过了处理器核心数量,实际上某些并行的线程之间就可能竞争同一个处理器的资源,频繁的切换上下文也会消耗处理器资源。

四、总结

YYAsyncLayer在Main Runloop空闲的时候执行绘制任务,并利用多核设备优势实现真正的各个子线程”并行“操作,从而解决视图内容绘制时的卡顿问题。下面六个问题可以概况全篇:

1,App为什么会卡顿?
2,CPU资源消耗的原因有哪些?
3,GPU资源消耗的原因有哪些?
4,异步渲染的核心逻辑有几步?
5,YYAsyncLayer在什么时候执行异步渲染操作,怎么实现的?
6,YYAsyncLayer是怎么做到异步操作的?

参考资料
OSAtomic原子操作
YYAsyncLayer 的异步绘制之道
iOS 保持界面流畅的技巧