众所周知,在iOS系统中,UI相关操作和用户的操作都是在主线程中进行,大量的UI操作,会造成主线程阻塞,影响应用的流畅度和用户体验,为了保证APP可以及时的响应用户的操作,所以一些UI的绘制工作,最好放到子线程,进行异步绘制,减轻主线程的工作,因此YY作者写出了一个异步绘制的工具YYAsyncLayer。
YYAsyncLayer 是 CALayer 的子类,当它需要显示内容(比如调用了 [layer setNeedDisplay])时,它会向 delegate,也就是 UIView 请求一个异步绘制的任务。在异步绘制时,Layer 会传递一个 BOOL(^isCancelled)() 这样的 block,绘制代码可以随时调用该 block 判断绘制任务是否已经被取消。
当 TableView 快速滑动时,会有大量异步绘制任务提交到后台线程去执行。但是有时滑动速度过快时,绘制任务还没有完成就可能已经被取消了。如果这时仍然继续绘制,就会造成大量的 CPU 资源浪费,甚至阻塞线程并造成后续的绘制任务迟迟无法完成。我的做法是尽量快速、提前判断当前绘制任务是否已经被取消;在绘制每一行文本前,我都会调用 isCancelled() 来进行判断,保证被取消的任务能及时退出,不至于影响后续操作。iOS 保持界面流畅的技巧
一、YYAsyncLayer 文件类组成
YYAsyncLayer中主要有三个类:
YYTransaction:注册一个通知,在监控runLoop睡眠和退出,来执行任务回调,利用runloop空闲,执行任务。(如果你想知道为什么要在睡眠和退出的时候,执行任务,你可以看下YY的这篇博客了解下深入理解RunLoop)
YYSentine:线程安全的计数器,通过判断计数器的值是否相等,来判断异步绘制任务是否被取消。
YYAsyncLayer:异步渲染的核心类,是CALayer子类,用来异步渲染layer内容。
二、YYAsyncLayer 源码分析
1.YYTransaction源码分析
YYTransaction绘制任务的机制是仿照CoreAnimation的绘制机制,监听主线程RunLoop,在空闲阶段插入绘制任务,并将任务优先级设置在CoreAnimation绘制完成之后,然后遍历绘制任务集合进行绘制工作并且清空集合,具体可以看源码。
/**
YYTransaction let you perform a selector once before current runloop sleep.
*/
@interface YYTransaction : NSObject
/**
Creates and returns a transaction with a specified target and selector.
@param target A specified target, the target is retained until runloop end.
@param selector A selector for target.
@return A new transaction, or nil if an error occurs.
*/
+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector;
/**
Commit the trancaction to main runloop.
@discussion It will perform the selector on the target once before main runloop's
current loop sleep. If the same transaction (same target and same selector) has
already commit to runloop in this loop, this method do nothing.
*/
- (void)commit;
@end
上面的YYTransaction.h文件中有两个方法:
第一个方法是+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector,根据传入的target和selector来创建一个任务。
第二个方法是- (void)commit;,它用来在runloop睡眠的时候,执行传入任务,并且对于相同的任务,在runloop中只执行一次。
看到这里你应该有两个疑问,
第一是如何来实现在runLoop将要休眠的时候,来执行传进来的任务???
第二是如何保证相同的任务只执行一次???
那么下面我们来看下源码,分析上面的两个问题
// 注册 Runloop Observer
static void YYTransactionSetup() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
transactionSet = [NSMutableSet new];
CFRunLoopRef runloop = CFRunLoopGetMain();
CFRunLoopObserverRef observer;
/**
创建一个RunLoop的观察者
allocator:该参数为对象内存分配器,一般使用默认的分配器kCFAllocatorDefault。或者nil
activities:该参数配置观察者监听Run Loop的哪种运行状态,这里我们监听beforeWaiting和exit状态
repeats:CFRunLoopObserver是否循环调用。
order:CFRunLoopObserver的优先级,当在Runloop同一运行阶段中有多个CFRunLoopObserver时,根据这个来先后调用CFRunLoopObserver,0为最高优先级别。正常情况下使用0。
callout:观察者的回调函数,在Core Foundation框架中用CFRunLoopObserverCallBack重定义了回调函数的闭包。
context:观察者的上下文。 (类似与KVO传递的context,可以传递信息,)因为这个函数创建ovserver的时候需要传递进一个函数指针,而这个函数指针可能用在n多个oberver 可以当做区分是哪个observer的状机态。(下面的通过block创建的observer一般是一对一的,一般也不需要Context,),还有一个例子类似与NSNOtificationCenter的 SEL和 Block方式
*/
observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopBeforeWaiting | kCFRunLoopExit,
true, // repeat
0xFFFFFF, // after CATransaction(2000000)
YYRunLoopObserverCallBack, NULL);
CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
});
}
//监听回调的方法
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) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[transaction.target performSelector:transaction.selector];
#pragma clang diagnostic pop
}];
}
上面的代码就是runLoop将要休眠的时候,执行任务的核心代码,
首先我们来看一下YYTransactionSetup方法中CFRunLoopObserverCreate函数的参数选取:
1. 任务执行的时机
从上面源码可以看出是在 kCFRunLoopBeforeWaiting | kCFRunLoopExit,也就是在将要睡眠和推出的时候来执行。
2.执行的优先级
从上面源码可以看出是0xFFFFFF, // after CATransaction(2000000),这是在CoreAnimation绘制完成之后之后执行。
3.执行的runLoop
从上面源码可以看出是 CFRunLoopGetMain();也就是主线程的runLoop中执行这个回调,这是因为执行的任务跟UI相关,必须要在主线程执行。
然后我们看下回调方法,是通过执行 transactionSet 中的 transaction来执行具体的方法,看到这个第一个问题“如何来实现在runLoop将要休眠的时候,执行任务”已经有答案了吧。
下面我们来看下“如何保证相同的任务只执行一次???”
- (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;
}
就是在commit的时候将transaction添加到transactionSet中,而我们知道NSMutableSet中不会出现相同的对象,所以这就实现了相同的任务只执行一次,同事由于NSMutableSet中是通过isEqual和hash来判断对象是否相同的,所以将这两个方法重写,保证transactionSet中任务的唯一性。
2.** YYAsyncLayer源码分析**
YYAsyncLayer为了异步绘制而继承CALayer的子类。通过使用CoreGraphic相关方法,在子线程中绘制内容Context,绘制完成后,回到主线程对layer.contents进行直接显示。
@interface YYAsyncLayer : CALayer
/// Whether the render code is executed in background. Default is YES.
@property BOOL displaysAsynchronously;
@end
@protocol YYAsyncLayerDelegate
@required
/// This method is called to return a new display task when the layer's contents need update.
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask;
@end
/**
A display task used by YYAsyncLayer to render the contents in background queue.
*/
@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
YYAsyncLayer中主要有三部分:
1. YYAsyncLayerDelegate
YYAsyncLayerDelegate 的 newAsyncDisplayTask 是提供了 YYAsyncLayer 需要在后台队列绘制的内容
2. YYAsyncLayerDisplayTask
display 在mainthread或者background thread调用 这要求 display 应该是线程安全的
willdisplay 和 didDisplay 在 mainthread 调用。
3.YYAsyncLayer
YYAsyncLayer是通过创建异步创建图像Context在其绘制,最后再主线程异步添加图像从而实现的异步绘制。同时,在绘制过程中进行了多次进行取消判断,以避免额外绘制.
YYAsyncLayer如何实现异步绘制和取消绘制功能:
1)异步绘制
通过 重写display 方法,调用- (void)_displayAsync:(BOOL)async,在后台线程中调用task.display 进行绘制,最终在主线程中将绘制图片赋值给self.contents。
2)是否取消绘制
通过isCancelled来判断是否取消绘制,主要是利用了局部变量被block捕获后,在block中value就不会改变,通过判断block中的value和外部的value值是否相等,来判断任务是否已经取消。
YYSentinel *sentinel = _sentinel;
int32_t value = sentinel.value;
BOOL (^isCancelled)() = ^BOOL() {
return value != sentinel.value;
};
3)如何创建队列
通过[NSProcessInfo processInfo].activeProcessorCount控制队列的最大数量和cpu的数量保持一致,因为线程的切换也是需要额外的开销的。所以线程不是越多,执行效率越高。
三、YYAsyncLayer 问题总结
1. YYTransaction中,如何在runLoop将要休眠的时候,来执行传进来的任务?
通过YYTransactionSetup方法中执行CFRunLoopObserverCreate函数实现,具体的函数参数选取如下:
1) 任务执行的时机
从上面源码可以看出是在 kCFRunLoopBeforeWaiting | kCFRunLoopExit,也就是在将要睡眠和推出的时候来执行。
2)执行的优先级
从上面源码可以看出是0xFFFFFF, // after CATransaction(2000000),这是在CoreAnimation绘制完成之后之后执行。
3)执行的runLoop
从上面源码可以看出是 CFRunLoopGetMain();也就是主线程的runLoop中执行这个回调,这是因为执行的任务跟UI相关,必须要在主线程执行。
2. YYTransaction如何保证相同的任务(transaction)只执行一次?
通过在commit的时候将transaction添加到transactionSet中,而我们知道NSMutableSet中不会出现相同的对象,所以这就实现了相同的任务只执行一次,同事由于NSMutableSet中是通过isEqual和hash来判断对象是否相同的,所以将这两个方法重写,保证transactionSet中任务的唯一性。
3.YYAsyncLayer如何实现异步绘制和取消绘制功能
1)异步绘制
通过 重写display 方法,调用- (void)_displayAsync:(BOOL)async,在后台线程中调用task.display 进行绘制,最终在主线程中将绘制图片赋值给self.contents。
2)是否取消绘制
通过isCancelled来判断是否取消绘制,主要是利用了局部变量被block捕获后,在block中value就不会改变,通过判断block中的value和外部的value值是否相等,来判断任务是否已经取消。
YYSentinel *sentinel = _sentinel;
int32_t value = sentinel.value;
BOOL (^isCancelled)() = ^BOOL() {
return value != sentinel.value;
};
3)如何创建队列
通过[NSProcessInfo processInfo].activeProcessorCount控制队列的最大数量和cpu的数量保持一致,因为线程的切换也是需要额外的开销的。所以线程不是越多,执行效率越高。
四、YYAsyncLayer 相关知识点总结
1.CFRunLoopObserverCreate函数使用,创建runLoop监听。
2.NSMutableSet使用,一直相同对象判断条件hash和isEqual。
3.OSAtomicIncrement32线程安全的自增计数,每调用一次+1。
4.GCD相关队列,dispatch_once等使用。
5.block捕获局部变量,值不会改变特性。
6.CoreGraphics相关绘制API的使用。
参考资料:
https://github.com/ibireme/YYAsyncLayer
https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/
https://blog.ibireme.com/2015/05/18/runloop/
https://juejin.im/post/5a0a52b5f265da43247ff4ad
https://www.jianshu.com/p/58e7571d7806
iOS的异步绘制--YYAsyncLayer源码分析