来源:伯乐在线专栏作者 - 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个数。

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值,用于判断异步绘制任务是否被取消的工具。

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

源码解析之–YYAsyncLayer异步绘制的更多相关文章

  1. Redis源码解析:19Hiredis异步API代码解析

    Hiredis中的异步API函数需要与事件库(libevent,libev, ev)一起工作.因为事件循环的机制,异步环境中的命令是自动管道化的.因为命令是异步发送的,因此发送命令时,必要情况下,需要 ...

  2. 异步任务spring @Async注解源码解析

    1.引子 开启异步任务使用方法: 1).方法上加@Async注解 2).启动类或者配置类上@EnableAsync 2.源码解析 虽然spring5已经出来了,但是我们还是使用的spring4,本文就 ...

  3. Redux异步解决方案之Redux-Thunk原理及源码解析

    前段时间,我们写了一篇Redux源码分析的文章,也分析了跟React连接的库React-Redux的源码实现.但是在Redux的生态中还有一个很重要的部分没有涉及到,那就是Redux的异步解决方案.本 ...

  4. RecyclerView 源码分析(一) —— 绘制流程解析

    概述 对于 RecyclerView 是那么熟悉又那么陌生.熟悉是因为作为一名 Android 开发者,RecyclerView 是经常会在项目里面用到的,陌生是因为只是知道怎么用,但是却不知道 Re ...

  5. [源码解析] PyTorch 分布式(16) --- 使用异步执行实现批处理 RPC

    [源码解析] PyTorch 分布式(16) --- 使用异步执行实现批处理 RPC 目录 [源码解析] PyTorch 分布式(16) --- 使用异步执行实现批处理 RPC 0x00 摘要 0x0 ...

  6. redux源码解析(深度解析redux+异步demo)

    redux源码解析 1.首先让我们看看都有哪些内容 2.让我们看看redux的流程图 Store:一个库,保存数据的地方,整个项目只有一个 创建store Redux提供 creatStore 函数来 ...

  7. Android 开源项目源码解析(第二期)

    Android 开源项目源码解析(第二期) 阅读目录 android-Ultra-Pull-To-Refresh 源码解析 DynamicLoadApk 源码解析 NineOldAnimations ...

  8. Android源码解析系列

    转载请标明出处:一片枫叶的专栏 知乎上看了一篇非常不错的博文:有没有必要阅读Android源码 看完之后痛定思过,平时所学往往是知其然然不知其所以然,所以为了更好的深入Android体系,决定学习an ...

  9. 还怕问源码?Github上神级Android三方源码解析手册,已有7.6 KStar

    或许对于许多Android开发者来说,所谓的Android工程师的工作"不过就是用XML实现设计师的美术图,用JSON解析服务器的数据,再把数据显示到界面上"就好了,源码什么的,看 ...

随机推荐

  1. 查看Mac OSX机器上存在的所有Device

    可以使用instruments -s来查看Mac OSX机器上存在的所有Device,包括模拟器创建的Device以及真实连接的iPad,iPhone等设备.

  2. easyui datagrid单击单元格选择此列

    示例代码实现单击jquery easyui datagrid的单元格时,取消datagrid默认选中高亮此行的样式,改为选中单击的单元格所在的列,高亮此列上的所有单元格.可以配置全局single变量, ...

  3. IT传道解惑:心累了就读读

    写在开始 学习不是因为缺少时间而是缺少努力 Studies this matter, lacks the time, but is lacks diligently. 只要你想学好,用心去学,肯下功夫 ...

  4. unicode转中文

    <pre name="code" class="html">[root@dr-mysql01 ~]# cat a1.pl my $str=" ...

  5. 【HDOJ】2133 What day is it

    需要注意数据有效性. #include <stdio.h> #define isLeapYear(y) (y%4==0&&y%100!=0)||(y%400==0) ][] ...

  6. WordPress Bradesco Gateway插件‘falha.php’跨站脚本漏洞

    漏洞名称: WordPress Bradesco Gateway插件‘falha.php’跨站脚本漏洞 CNNVD编号: CNNVD-201309-451 发布时间: 2013-09-26 更新时间: ...

  7. windows下 破解 Sublime Text3 和汉化

    这货已经出到3了. windows下载,破解,使用方法: 一:破解 1: 去官网下载最新版本 http://www.sublimetext.com/3 2:下载破解器(SublimeTextKeyge ...

  8. 【转】Android自定义控件

    原文网址:http://blog.163.com/ppy2790@126/blog/static/103242241201382210910473/ 开发自定义控件的步骤: 1.了解View的工作原理 ...

  9. 开源库CImg 数据格式存储

    CImg为开源图像处理库,仅有一个头文件CImg.h便包含了对图像的所有处理函数,函数操作简单,编程方便,但国内使用者较少 其homepage:http://cimg.sourceforge.net/ ...

  10. 一个PHP书单 -摘自网络

    # PHP <PHP程序设计>(第2版) –PHP语法和入门最好的书 <PHP5权威编程> –PHP入门后升级书 <深入PHP:面向对象.模式与实践>(第3版) – ...