源码解析之–YYAsyncLayer异步绘制

来源:伯乐在线专栏作者 - Shelin

链接:http://ios.jobbole.com/86878/

点击 → 了解如何加入专栏作者

前言

YYAsyncLayer是异步绘制与显示的工具。最初是从YYKitDemo中接触到这个工具,为了保证列表滚动流畅,将视图绘制、以及图片解码等任务放到后台线程,在YYAsyncLayer之前还是想从YYKitDemo中性能优化说起,虽然些跑题了…

YYKitDemo

对于列表主要对两个代理方法的优化,一个与绘制显示有关,另一个与计算布局有关:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;

常规逻辑可能觉得应该先调用tableView : cellForRowAtIndexPath :返回UITableViewCell对象,事实上调用顺序是先返回UITableViewCell的高度,是因为UITableView继承自UIScrollView,滑动范围由属性contentSize来确定,UITableView的滑动范围需要通过每一行的UITableViewCell的高度计算确定,复杂cell如果在列表滚动过程中计算可能会造成一定程度的卡顿。

假设有20条数据,当前屏幕显示5条,tableView : heightForRowAtIndexPath :方法会先执行20次返回所有高度并计算出滑动范围,tableView : cellForRowAtIndexPath :执行5次返回当前屏幕显示的cell个数。

源码解析之–YYAsyncLayer异步绘制

TableViewOfPerformanceOptimization.png

从图中简单看下流程,从网络请求返回JSON数据,将Cell的高度以及内部视图的布局封装为Layout对象,Cell显示之前在异步线程计算好所有布局对象,并存入数组,每次调用tableView: heightForRowAtIndexPath :只需要从数组中取出,可避免重复的布局计算。同时在调用tableView: cellForRowAtIndexPath :对Cell内部视图异步绘制布局,以及图片的异步绘制解码,这里就要说到今天的主角YYAsyncLayer。

YYAsyncLayer

首先介绍里面几个类:

  • YYAsyncLayer:继承自CALayer,绘制、创建绘制线程的部分都在这个类。

  • YYTransaction:用于创建RunloopObserver监听MainRunloop的空闲时间,并将YYTranaction对象存放到集合中。

  • YYSentinel:提供获取当前值的value(只读)属性,以及- (int32_t)increase自增加的方法返回一个新的value值,用于判断异步绘制任务是否被取消的工具。

源码解析之–YYAsyncLayer异步绘制

AsyncDisplay.png

上图是整体异步绘制的实现思路,后面一步步说明。现在假设需要绘制Label,其实是继承自UIView,重写+ (Class)layerClass ,在需要重新绘制的地方调用下面方法,比如setter,layoutSubviews。

+ (Class)layerClass {

return YYAsyncLayer.class;

}

- (void)setText:(NSString *)text {

_text = text.copy;

[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];

}

- (void)layoutSubviews {

[super layoutSubviews];

[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];

}

YYTransaction有selector、target的属性,selector其实就是contentsNeedUpdated方法,此时并不会立即在后台线程去更新显示,而是将YYTransaction对象本身提交保存在transactionSet的集合中,上图中所示。

+ (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];

}

同时在YYTransaction.m中注册一个RunloopObserver,监听MainRunloop在kCFRunLoopCommonModes(包含kCFRunLoopDefaultMode、UITrackingRunLoopMode)下的kCFRunLoopBeforeWaiting和kCFRunLoopExit的状态,也就是说在一次Runloop空闲时去执行更新显示的操作。

kCFRunLoopBeforeWaiting:Runloop将要进入休眠。

kCFRunLoopExit:即将退出本次Runloop。

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

CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);

CFRelease(observer);

});

}

下面是RunloopObserver的回调方法,从transactionSet取出transaction对象执行SEL的方法,分发到每一次Runloop执行,避免一次Runloop执行时间太长。

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

}];

}

接下来是异步绘制,这里用了一个比较巧妙的方法处理,当使用GCD时提交大量并发任务到后台线程导致线程被锁住、休眠的情况,创建与程序当前激活CPU数量(activeProcessorCount)相同的串行队列,并限制MAX_QUEUE_COUNT,将队列存放在数组中。

YYAsyncLayer.m有一个方法YYAsyncLayerGetDisplayQueue来获取这个队列用于绘制(这部分YYKit中有独立的工具YYDispatchQueuePool)。创建队列中有一个参数是告诉队列执行任务的服务质量quality of service,在iOS8+之后相比之前系统有所不同。

  • iOS8之前队列优先级:

DISPATCH_QUEUE_PRIORITY_HIGH 2 高优先级

DISPATCH_QUEUE_PRIORITY_DEFAULT 0 默认优先级

DISPATCH_QUEUE_PRIORITY_LOW (-2) 低优先级

DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN 后台优先级

  • iOS8+之后:

QOS_CLASS_USER_INTERACTIVE 0x21, 用户交互(希望尽快完成,不要放太耗时操作)

QOS_CLASS_USER_INITIATED 0x19, 用户期望(不要放太耗时操作)

QOS_CLASS_DEFAULT 0x15, 默认(用来重置对列使用的)

QOS_CLASS_UTILITY 0x11, 实用工具(耗时操作,可以使用这个选项)

QOS_CLASS_BACKGROUND 0x09, 后台

QOS_CLASS_UNSPECIFIED 0x00, 未指定

/// Global display queue, used for content rendering.

static dispatch_queue_t YYAsyncLayerGetDisplayQueue() {

#ifdef YYDispatchQueuePool_h

return YYDispatchQueueGetForQOS(NSQualityOfServiceUserInitiated);

#else

#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  MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount);

if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {

for (NSUInteger i = 0; i

接下来是关于绘制部分的代码,对外接口YYAsyncLayerDelegate代理中提供- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask方法用于回调绘制的代码,以及是否异步绘制的BOOl类型属性displaysAsynchronously,同时重写CALayer的display 方法来调用绘制的方法- (void)_displayAsync:(BOOL)async。

这里有必要了解关于后台的绘制任务何时会被取消,下面两种情况需要取消,并调用了YYSentinel的increase方法,使value值增加(线程安全):

  • 在视图调用setNeedsDisplay时说明视图的内容需要被更新,将当前的绘制任务取消,需要重新显示。

  • 以及视图被释放调用了dealloc方法。

在YYAsyncLayer.h中定义了YYAsyncLayerDisplayTask类,有三个block属性用于绘制的回调操作,从命名可以看出分别是将要绘制,正在绘制,以及绘制完成的回调,可以从block传入的参数BOOL(^isCancelled)(void)判断当前绘制是否被取消。

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

下面是部分- (void)_displayAsync:(BOOL)async绘制的代码,主要是一些逻辑判断以及绘制函数,在异步执行之前通过YYAsyncLayerGetDisplayQueue创建的队列,这里通过YYSentinel判断当前的value是否等于之前的值,如果不相等,说明绘制任务被取消了,绘制过程会多次判断是否取消,如果是则return,保证被取消的任务能及时退出,如果绘制完毕则设置图片到layer.contents。

if (async) {  //异步

if (task.willDisplay) task.willDisplay(self);

YYSentinel *sentinel = _sentinel;

int32_t value = sentinel.value;

NSLog(@" --- %d ---", value);

//判断当前计数是否等于之前计数

BOOL (^isCancelled)() = ^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

最后

关于具体使用可以看看程序的示例,这是从YYAsyncLayer中学到的一些技巧,自己还试着简单实现一遍,项目中遇到的性能问题可也以依据这些思路去找到最合适的解决方案,挺想说一句阅读源码是件比较要耐心的事,但确实可以收获颇多。最近也有换环境工作的计划,坐标帝都,欢迎骚扰https://github.com/ShelinShelin

上一篇:在Vue中通过自定义指令获取元素


下一篇:dagger @Component @Module @Inject