关于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 { [self .layer setNeedsDisplay]; } + (Class)layerClass { return YYAsyncLayer.class; } - (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 ; 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) { } else { } }; 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只有两个方法接口。 方法一:包装target
和selector
并生成一个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; } - (void )commit { if (!_target || !_selector) return ; YYTransactionSetup(); [transactionSet addObject:self ]; } - (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; } @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()
函数,这个函数只执行一次。这个函数的作用是,每次在主运行循环切换到kCFRunLoopBeforeWaiting
和kCFRunLoopExit
这两种状态下会调用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 , 0xFFFFFF , YYRunLoopObserverCallBack, NULL ); CFRunLoopAddObserver (runloop, observer, kCFRunLoopCommonModes); CFRelease (observer); }); }
其实就是给主运行循环添加一个闲时任务,这个任务的优先级是0xFFFFFF,换成10进制就是16777215,比2000000大。设置任务优先级的参数类型是CFIndex
(typedef 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@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; static dispatch_once_t onceToken; dispatch_once (&onceToken, ^{ scale = [UIScreen mainScreen].scale; }); self .contentsScale = scale; _sentinel = [YYSentinel new]; _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 { [_sentinel increase ]; } - (void )setNeedsDisplay { [self _cancelAsyncDisplay ]; [super setNeedsDisplay ]; } - (void )_cancelAsyncDisplay { [_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.contents
为nil
,这样赋值的意义是什么呢?
看一下核心逻辑:
结合之前的官方示例,外界自定义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; YYAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask]; if (!task.display) { 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 ) { 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(), ^{ if (isCancelled()) { CGColorRelease (backgroundColor); return ; } UIGraphicsBeginImageContextWithOptions (size, opaque, scale); CGContextRef context = UIGraphicsGetCurrentContext (); 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); 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 ); } }); }); } 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, ^{ 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 = 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); } }); int32_t cur = OSAtomicIncrement32(&counter); if (cur < 0 ) cur = -cur; 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 保持界面流畅的技巧