iOS的异步绘制--YYAsyncLayer源码分析
iOS的异步渲染
最近看了YYAsyncLayer在这里总结一下。YYAsyncLayer是整个YYKit异步渲染的基础。整个项目的Github地址在这里。你可以先下载了一睹为快,也可以跟着我一步一步的了解它是怎么实现异步绘制的。
如何实现异步
两种方式可以实现异步。一种是使用另外的一个线程,一种是使用RunLoop。另外开一个线程的方法有很多,但是现在最方便的就死GCD了。
GCD
这里介绍一些GCD里常用的方法,为了后面阅读的需要。还有YYAsyncLayer中用到的更加高级的用法会在下文中深入介绍。
创建一个queue
dispatch_queue_t queue;
if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
queue = dispatch_queue_create("com.ibireme.yykit.render", attr);
} else {
queue = dispatch_queue_create("com.ibireme.yykit.render", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
}
如果iOS 8和以上版本的话,创建queue的方法和之前的版本的不太太一样。在iOS 8和以上的版本中创建queue需要先创建一个dispatch_queue_attr_t类型的实例。并作为参数传入到queue的生成方法里。
DISPATCH_QUEUE_SERIAL说明在这个queue内部的task是串行执行的。
dispatch_once
使用dispatch_once和dispatch_once_t的组合可以实现其中的task只被执行一次。但是有一个前提条件,看代码:
static dispatch_once_t onceToken; // 1
// 2
dispatch_once(&onceToken, ^{
// 这里的task只被执行一次
});
- 这里的
dispatch_once_t必须是静态的。也就是要有APP一样长的生存期来保证这段时间内task只被执行一次。如果不是static的,那么只被执行一次是保证不了的。 dispatch_once方法在这里执行,onceToken在这里有一个取地址的操作。也就是onceToken把地址传入方法内部被初始化和赋值。
RunLoop
CFRunLoopRef runloop = CFRunLoopGetMain(); // 1
CFRunLoopObserverRef observer;
// 2
observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopBeforeWaiting |kCFRunLoopExit,
true, // repeat
0xFFFFFF, // after CATransaction(2000000)
YYRunLoopObserverCallBack, NULL);
// 3
CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
我们来分析一下这段代码
CFRunLoopGetMain方法返回主线程的RunLoop引用。后面用这个引用来添加回调。- 使用系统内置的c方法创建一个
RunLoop的观察者,在创建这个观察者的时候回同时指定回调方法。 - 给
RunLoop实例添加观察者,之后减少一个观察者的引用。
在第二步创建观察者的时候,还指定了观察者观察的事件:kCFRunLoopBeforeWaiting | kCFRunLoopExit,在
RunLoop进入等待或者即将要退出的时候开始执行观察者。指定了观察者是否重复(true)。指定了观察者的优先级:0xFFFFFF,这个优先级比CATransaction优先级为2000000的优先级更低。这是为了确保系统的动画优先执行,之后再执行异步渲染。
YYRunLoopObserverCallBack就是观察者收到通知的时候要执行的回调方法。这个方法的声明是这样的:
static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
渲染是怎么回事
渲染就是把我们代码里设置的代码的视图和数据结合,最后绘制成一张图呈现在用户的面前。每秒绘制60张图,用户看着就是流畅的揭秘男呈现,如果不到60帧,那么越少用户看着就会越卡。
CALayer
在iOS中,最终我们看到的视图都是在CALayer里呈现的,在CALayer有一个属性叫做contents,这里不放别的,放的就是显示用的一张图。
我们来看看YYAsyncLayer类的代码:
// 类声明
@interface YYAsyncLayer : CALayer // 1
/// Whether the render code is executed in background. Default is YES.
@property BOOL displaysAsynchronously;
@end
//类实现的一部分代码
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext(); // 2
// ...
dispatch_async(dispatch_get_main_queue(), ^{
self.contents = (__bridge id)(image.CGImage); // 3
});
YYAsyncLayer继承自CALayer。UIGraphicsGetImageFromCurrentImageContext这是一个CoreGraphics的调用,是在一些绘制之后返回组成的图片。- 在2>中生成的图片,最终被赋值给了
CALahyer#contents属性。
CoreGraphics
如果说CALayer是一个绘制结果的展示,那么绘制的过程就要用到CoreGraphics了。
在正式开始以前,首先需要了解一个方法的实现。这个方法会用来绘制具体的界面上的内容:
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;
}
};
你也看到了,这其实不是一个方法而是一个block。这个block会使用传入的CGContextRef context参数来绘制文字。
目前了解这么多就足够了,后面会有详细的介绍。
在YYAsyncLayer#_displayAsync方法是如何绘制的,_displayAsync是一个“私有方法”。
//这里我们只讨论异步的情况
// 1
CGSize size = self.bounds.size;
BOOL opaque = self.opaque;
CGFloat scale = self.contentsScale;
CGColorRef backgroundColor = (opaque && self.backgroundColor)
? CGColorRetain(self.backgroundColor) : NULL;
dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{ // 2
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
// 3
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); // 4
// 5
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 6
dispatch_async(dispatch_get_main_queue(), ^{
self.contents = (__bridge id)(image.CGImage);
});
});
解释如下:
- 准备工作,获取
size,opaque,scale和backgroundColor这个四个值。这些在获取绘制的取悦的时候用到。背景色另外有处理。 YYAsyncLayerGetDisplayQueue()方法返回一个dispatch_queue_t实例,并在其中开始异步操作。- 判断
opaque的值,如果是非透明的话处理背景色。这个时候就会用到第一步里获取到的backgroundColor变量的值。 - 在CoreGraphics一节开始的时候讲到的绘制具体内容的block。
- 绘制完毕,获取到
UIImage实例。 - 返回主线程,并给
contents属性设置绘制的成果图片。至此异步绘制全部结束。
为了让读者更加关注异步绘制这个主题,所以省略了部分代码。生路的代码中很多事检查是否取消的。异步的绘制,尤其是在一个滚动的UITableView或者UICollectionView中随时都可能会取消,所以即使的检查是否取消并终止正在进行的绘制很有必要。这些,你会在完整的代码中看到。
不能无限的开辟线程
我们都知道,把阻塞主线程执行的代码放入另外的线程里保证APP可以及时的响应用户的操作。但是线程的切换也是需要额外的开销的。也就是说,线程不能无限度的开辟下去。
那么,dispatch_queue_t的实例也不能一直增加下去。有人会说可以用dispatch_get_global_queue()来获取系统的队列。没错,但是这个情况只适用于少量的任务分配。因为,系统本身也会往这个queue里添加任务的。
所以,我们需要用自己的queue,但是是有限个的。在YY里给这个数量指定的值是16。
指定为16,我也是有些疑惑的。在Android里指定线程池的大小的时候通常的值是CPU的内核个数的两倍。
设计,把点连成线
YYAsyncLayer异步绘制的过程就是一个观察者执行的过程。所谓的观察者就是你设置了一个机关,当它被触发的时候可以执行你预设的东西。比如你走到一扇门前,它感应到了你的红外辐射就会打开。
async layer也是一样,它会把“感应器”放在run loop里。当run loop要闲下来的时候“感应器”的回调开始执行,告诉async layer可以开始异步渲染了。
但是异步渲染要干什么呢?我们现在就来说说异步渲染的内容从哪里来?一个需要异步渲染的view会在定义的时候就把需要异步渲染的内容通过layer保存在view的代理发送给layer。
CALayer和UIView的关系
UIView是显示层,而显示在屏幕上的内容是由CALayer来管理的。CALayer的一个代理方法可以在UIView宿主里实现。
YYAsyncLayer用的就是这个方式。代理为:
@protocol YYAsyncLayerDelegate <NSObject>
@required
/// This method is called to return a new display task when the layer's contents need update.
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask;
@end
``
在实现的时候是这样的:
```objc
#pragma mark - YYTextAsyncLayerDelegate
- (YYTextAsyncLayerDisplayTask *)newAsyncDisplayTask {
// 1
YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
// 2
task.willDisplay = ^(CALayer *layer) {
// ...
}
// 3
task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) {
// ...
}
// 4
task.didDisplay = ^(CALayer *layer, BOOL finished) {
// ...
}
return task;
}
- 创建了
YYAsyncLayerDisplayTask对象 - 设置task的
willDisplayblock回调。 3. 4.分别设置了其他的display回调block。
可见YYAsyncLayer的代理的实现会创建一个YYAsyncLayerDisplayTask的实例并返回。在这个实例中包含了layer显示顺序的回调:willDisplay、display和didDisplay。
setNeedsDisplay
对CALayer实例调用setNeedsDisplay方法之后CALayer的display方法就会被调用。YYAsyncLayer重写了display方法:
- (void)display {
super.contents = super.contents;
[self _displayAsync:_displaysAsynchronously];
}
最终会调用YYAsyncLayer实例的display方法。display方法又会调用到_displayAsync:方法,开始异步绘制的过程。
总结
最后,我们把整个异步渲染的过程来串联起来。
对一个包含了YYAsyncLayer的view,比如YYLable就像文档里的一样。重写layoutSubviews方法添加对layer的setNeedsDisplay方法的调用。
这样一个调用链就形成了:用户操作->[view layoutSubviews]->[view.layer setNeedsDisplay]->[layer display]->[layer _displayAsync]异步绘制开始(准确的说是_displayAsync方法的参数为true**的时候开始异步绘制)。
但是这并没有用到RunLoop。所以代码会修改为每次调用layoutSubviews的时候给RunLoop提交一个异步绘制的任务:
- (void)layoutSubviews {
[super layoutSubviews];
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}
- (void)contentsNeedUpdated {
// do update
[self.layer setNeedsDisplay];
}
这样每次RunLoop要进入休眠或者即将退出的时候会开始异步的绘制。这个任务是从[layer setNeedsDisplay]开始的。
iOS的异步绘制--YYAsyncLayer源码分析的更多相关文章
- Android异步消息传递机制源码分析
1.Android异步消息传递机制有以下两个方式:(异步消息传递来解决线程通信问题) handler 和 AsyncTask 2.handler官方解释的用途: 1).定时任务:通过handler.p ...
- Java异步编程——深入源码分析FutureTask
Java的异步编程是一项非常常用的多线程技术. 之前通过源码详细分析了ThreadPoolExecutor<你真的懂ThreadPoolExecutor线程池技术吗?看了源码你会有全新的认识&g ...
- jQuery使用():Callbacks回调函数列表之异步编程(含源码分析)
Callbacks的基本功能回调函数缓存与调用 特定需求模式的Callbacks Callbacks的模拟源码 一.Callbacks的基本功能回调函数缓存与调用 Callbacks即回调函数集合,在 ...
- Android应用层View绘制流程与源码分析
1 背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原 ...
- iOS常用框架源码分析
SDWebImage NSCache 类似可变字典,线程安全,使用可变字典自定义实现缓存时需要考虑加锁和释放锁 在内存不足时NSCache会自动释放存储的对象,不需要手动干预 NSCache的key不 ...
- iOS硬解H.264:-VideoToolboxDemo源码分析[草稿]
来源:http://www.cnblogs.com/michaellfx/p/understanding_-VideoToolboxDemo.html iOS硬解H.264:-VideoToolbox ...
- 异步编程之co——源码分析
异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...
- 一个由正则表达式引发的血案 vs2017使用rdlc实现批量打印 vs2017使用rdlc [asp.net core 源码分析] 01 - Session SignalR sql for xml path用法 MemCahe C# 操作Excel图形——绘制、读取、隐藏、删除图形 IOC,DIP,DI,IoC容器
1. 血案由来 近期我在为Lazada卖家中心做一个自助注册的项目,其中的shop name校验规则较为复杂,要求:1. 英文字母大小写2. 数字3. 越南文4. 一些特殊字符,如“&”,“- ...
- Log4j2异步情况下怎么防止丢日志的源码分析以及队列等待和拒绝策略分析
org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor以下所有源码均在此类中首先我们看下log4j2异步队列的初始化 从这里面我们 ...
随机推荐
- ps中如何让图层在画布内水平居中
下图每个小logo图案距离它们的上参考线的距离均为10px,而我们如何让图层在画布内水平居中??? 如上图中三个图层的图案是用来给Html/Css中的background属性使用的,虽然可以通过鼠标拖 ...
- 【深入Java虚拟机】之一:Java内存区域与内存溢出
转载请注明出处:http://blog.csdn.net/ns_code/article/details/17565503 内存区域 Java虚拟机在执行Java程序的过程中会把他所管理的内存划分为若 ...
- Swing-JRadioButton用法-入门
JRadioButton是Swing中的单选框.所谓单选框是指,在同一个组内虽然有多个单选框存在,然而同一时刻只能有一个单选框处于选中状态.它就像收音机的按钮,按下一个时此前被按下的会自动弹起,故因此 ...
- 201521123045 《Java程序设计》第6周学习总结
Java 第六周总结 1. 本周学习总结 1.1 面向对象学习暂告一段落,请使用思维导图,以封装.继承.多态为核心概念画一张思维导图,对面向对象思想进行一个总结.注1:关键词与内容不求多,但概念之间的 ...
- SQL基础巩固
1.一定要记住,SQL 对大小写不敏感! 2.分号是在数据库系统中分隔每条 SQL 语句的标准方法,这样就可以在对服务器的相同请求中执行一条以上的语句. 如果您使用的是 MS Access 和 SQL ...
- Oracle中Union与Union All的区别(适用多个数据库)
Oracle中Union与Union All的区别(适用多个数据库) 如果我们需要将两个select语句的结果作为一个整体显示出来,我们就需要用到union或者union all关键字.union(或 ...
- Saltstack批量编译部署nginx(多模块)
最近一直在研究saltstack的同步文件和批量执行命令,随着架构的变大,批量部署的需求也变得明显起来了,我需要用一条命令就部署好nginx和tomcat,并且符合我所有的环境需求,可以直接投入生产环 ...
- Linux环境下MySQL数据库用SQL语句插入中文显示 “问号或者乱码 ” 问题解决!
问题: 在普通用户权限下执行 mysql -u root -p进入mysql数据库,中间步骤省略,插入数据:insert into 库名(属性)values('汉字'); 会出现如下提示: Quer ...
- 在java中使用MongoDB数据库
Java 安装 要想在 Java 程序中使用 MongoDB,需要先确定是否安装了 MongoDB JDBC 驱动,并且要在机器上安装了 Java.查看 Java 教程来确保在机器上安装好 Java. ...
- BootStrap基礎知識
BootStrap基礎知識 1. .lead //突出 .text-left //文字居左 .text-right //文字居右 .text-center //文字居中 .text-justify / ...