YYAsyncLayer源码分析
本节关键字
- 异步绘制
- RunLoop
这是YYAsyncLayer
的结构
YYAsyncLayer:异步绘制的CALayer子类,这个类做的核心和
VVeboTableViewDemo
中VVeboLabel
的核心是一模一样的。你可以到回去看看从VVeboTableViewDemo到YYAsyncLayer(一)YYSentinel:线程安全的计数器。
YYTransaction:注册RunLoop在系统空闲时调用。(如果你不了解或者没有听说过RunLoop,不用担心,下面我同样会推荐相关的文章,让你了解和实践RunLoop)
YYAsyncLayerDisplayTask: 用于回调画布
YYAsyncLayerDelegate: 给接收
YYAsyncLayer
的UIView开的接口
本文依然是用Swift版YYAsyncLayer
进行分析
YYAsyncLayer
上图中高亮的方法为整个类的核心方法
private func _displayAsync(_ async: Bool) {
/// 如果需要使用异步绘制的地方没有实现该代理,直接返回
guard let mydelegate = delegate as? YYAsyncLayerDelegate else { return }
/// 接收来自需要异步绘制类的任务对象
let task = mydelegate.newAsyncDisplayTask
/// 如果display闭包为空,直接返回
if task.display == nil {
task.willDisplay?(self)
contents = nil
task.didDisplay?(self, true)
return
}
// 是否需要异步绘制,默认是开启异步绘制的
if async {
/// 绘制将要开始
task.willDisplay?(self)
/// https://github.com/ibireme/YYAsyncLayer/issues/6
/*
一个Operation/Task对应唯一一个isCancelled,在NSOperation中是函数调用,在这里是这个isCancelled block。所以每次提交到queue的task的isCancelled block是不同的block对象,其中捕获的value的值都是这个task创建时sentinel.value的值,而捕获的sentinel的引用都是这个layer的sentinel的引用,最后在block执行的时候,value的值就是捕获的value,而sentinel.value则可能已经发生了变化。
*/
let sentinel = _sentinel
let value = sentinel!.value
let isCancelled: (() -> Bool) = {
return value != sentinel!.value
}
let size = bounds.size
let opaque = isOpaque
let scale = contentsScale
let backgroundColor = (opaque && self.backgroundColor != nil) ? self.backgroundColor : nil
/// 太小不绘制
if size.width < 1 || size.height < 1 {
var image = contents
contents = nil
if image != nil {
YYAsyncLayerGetReleaseQueue.async {
image = nil
}
}
task.didDisplay?(self, true)
return
}
/// 将绘制操作放入自定义队列中
YYAsyncLayerGetDisplayQueue.async {
if isCancelled() {
return
}
/// 第一个参数表示所要创建的图片的尺寸;
/// 第二个参数用来指定所生成图片的背景是否为不透明,如上我们使用true而不是false,则我们得到的图片背景将会是黑色,显然这不是我想要的;
/// 第三个参数指定生成图片的缩放因子,这个缩放因子与UIImage的scale属性所指的含义是一致的。传入0则表示让图片的缩放因子根据屏幕的分辨率而变化,所以我们得到的图片不管是在单分辨率还是视网膜屏上看起来都会很好。
/// 注意这个与UIGraphicsEndImageContext()成对出现
/// iOS10 中新增了UIGraphicsImageRenderer(bounds: _)
UIGraphicsBeginImageContextWithOptions(size, opaque, scale)
/// 获取绘制画布
/// 每一个UIView都有一个layer,每一个layer都有个content,这个content指向的是一块缓存,叫做backing store。
/// UIView的绘制和渲染是两个过程,当UIView被绘制时,CPU执行drawRect,通过context将数据写入backing store
/// http://vizlabxt.github.io/blog/2012/10/22/UIView-Rendering/
guard let context = UIGraphicsGetCurrentContext() else { return }
if opaque {
/*
成对出现
CGContextSaveGState与CGContextRestoreGState的作用
使用Quartz时涉及到一个图形上下文,其中图形上下文中包含一个保存过的图形状态堆栈。在Quartz创建图形上下文时,该堆栈是空的。CGContextSaveGState函数的作用是将当前图形状态推入堆栈。之后,您对图形状态所做的修改会影响随后的描画操作,但不影响存储在堆栈中的拷贝。在修改完成后。
您可以通过CGContextRestoreGState函数把堆栈顶部的状态弹出,返回到之前的图形状态。这种推入和弹出的方式是回到之前图形状态的快速方法,避免逐个撤消所有的状态修改;这也是将某些状态(比如裁剪路径)恢复到原有设置的唯一方式。
*/
context.saveGState()
if backgroundColor == nil || backgroundColor!.alpha < 1 {
context.setFillColor(UIColor.white.cgColor) // 设置填充颜色,setStrokeColor为边框颜色
context.addRect(CGRect(x: 0, y: 0, width: size.width * scale, height: size.height * scale))
context.fillPath() // 填充路径
// 上面两句与这句等效
// context.fill(CGRect(x: 0, y: 0, width: size.width * scale, height: size.height * scale))
}
if let backgroundColor = backgroundColor {
context.setFillColor(backgroundColor)
context.addRect(CGRect(x: 0, y: 0, width: size.width * scale, height: size.height * scale))
context.fillPath()
}
context.restoreGState()
}
// 回调绘制
task.display?(context, size, isCancelled)
// 如果取消,提前结束绘制
if isCancelled() {
UIGraphicsEndImageContext()
DispatchQueue.main.async {
task.didDisplay?(self, false)
}
return
}
// 从画布中获取图片,与UIGraphicsEndImageContext()成对出现
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// 如果取消,提前结束绘制
if isCancelled() {
DispatchQueue.main.async {
task.didDisplay?(self, false)
}
return
}
DispatchQueue.main.async {
if isCancelled() {
task.didDisplay?(self, false)
} else {
// 绘制成功
self.contents = image?.cgImage
task.didDisplay?(self, true)
}
}
}
} else {
// 同步绘制
_sentinel.increase()
task.willDisplay?(self)
UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, contentsScale)
guard let context = UIGraphicsGetCurrentContext() else { return }
if isOpaque {
var size = bounds.size
size.width *= contentsScale
size.height *= contentsScale
context.saveGState()
if backgroundColor == nil || backgroundColor!.alpha < 1 {
context.setFillColor(UIColor.white.cgColor)
context.addRect(CGRect(origin: .zero, size: size))
context.fillPath()
}
if let backgroundColor = backgroundColor {
context.setFillColor(backgroundColor)
context.addRect(CGRect(origin: .zero, size: size))
context.fillPath()
}
context.restoreGState()
}
task.display?(context, bounds.size, {return false })
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
contents = image?.cgImage
task.didDisplay?(self, true)
}
}
如果你去对比从VVeboTableViewDemo到YYAsyncLayer(一)中的VVeboLabel
,他们的核心思想其实是一模一样的
YYTransaction
这个类和另外两个类是独立的。那么他是干嘛用的了?作者构建他的理由是什么呢?
我们看看这张图:(在任意项目的func viewDidLoad()中打个断点,你的堆栈信息大概就是这样的)
图中有个CATransaction
的东西,似乎和YYTransaction
很相似。其实他们不仅命名很相似,就是内部结构也很相似。
再看YYTransaction
:
其中YYTransactionSetup
func YYTransactionSetup() {
DispatchQueue.once(token: onceToken) {
transactionSet = Set()
/// 获取main RunLoop
let runloop = CFRunLoopGetMain()
var observer: CFRunLoopObserver?
/// http://www.jianshu.com/p/6757e964b956
/// 创建一个RunLoop的观察者
/// allocator:该参数为对象内存分配器,一般使用默认的分配器kCFAllocatorDefault。或者nil
/// activities:该参数配置观察者监听Run Loop的哪种运行状态。在示例中,我们让观察者监听Run Loop的所有运行状态。
/// repeats:该参数标识观察者只监听一次还是每次Run Loop运行时都监听。
/// order: 观察者优先级,当Run Loop中有多个观察者监听同一个运行状态时,那么就根据该优先级判断,0为最高优先级别。
/// callout:观察者的回调函数,在Core Foundation框架中用CFRunLoopObserverCallBack重定义了回调函数的闭包。
/// context:观察者的上下文。 (类似与KVO传递的context,可以传递信息,)因为这个函数创建ovserver的时候需要传递进一个函数指针,而这个函数指针可能用在n多个oberver 可以当做区分是哪个observer的状机态。(下面的通过block创建的observer一般是一对一的,一般也不需要Context,),还有一个例子类似与NSNOtificationCenter的 SEL和 Block方式。
observer = CFRunLoopObserverCreate(
kCFAllocatorDefault,
CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.exit.rawValue,
true, 0xFFFFFF,
YYRunLoopObserverCallBack,
nil
)
//将观察者添加到主线程runloop的common模式下的观察中
CFRunLoopAddObserver(runloop, observer, .commonModes)
observer = nil
}
}
到这里,我们可以感觉到YYTransaction
的用途和CATransaction
的用途是有某种相似之处的。
再看
苹果对CATransaction的定义(你也可以看看这本书里的解释)
A mechanism for batching multiple layer-tree operations into atomic updates to the render tree.
**谷歌翻译: ** 用于将多个层树操作批量化为渲染树的原子更新的机制。
事务是通过CATransaction类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。但是可以用+begin和+commit分别来入栈或者出栈。
任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)。
Core Animation在每个run loop周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西),即使你不显式的用[CATransaction begin]开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。
通过这段解释,我们可以获得关键信息是:CATransaction是用了对事物来做管理的。Core Animation在每个run loop周期中自动开始一次新的事务
在这里你不需要恐惧RunLoop,即使我们一点也不了解,下面的代码也是可以看懂的
let YYRunLoopObserverCallBack: CFRunLoopObserverCallBack = {_,_,_ in
if (transactionSet?.count ?? 0) == 0 {
return
}
let currentSet = transactionSet
transactionSet = Set()
for item in currentSet! {
_ = (item.target as? NSObject)?.perform(item.selector)
}
}
func YYTransactionSetup() {
DispatchQueue.once(token: onceToken) {
transactionSet = Set()
/// 获取main RunLoop
let runloop = CFRunLoopGetMain()
var observer: CFRunLoopObserver?
/// http://www.jianshu.com/p/6757e964b956
/// 创建一个RunLoop的观察者
/// allocator:该参数为对象内存分配器,一般使用默认的分配器kCFAllocatorDefault。或者nil
/// activities:该参数配置观察者监听Run Loop的哪种运行状态。在示例中,我们让观察者监听Run Loop的所有运行状态。
/// repeats:该参数标识观察者只监听一次还是每次Run Loop运行时都监听。
/// order: 观察者优先级,当Run Loop中有多个观察者监听同一个运行状态时,那么就根据该优先级判断,0为最高优先级别。
/// callout:观察者的回调函数,在Core Foundation框架中用CFRunLoopObserverCallBack重定义了回调函数的闭包。
/// context:观察者的上下文。 (类似与KVO传递的context,可以传递信息,)因为这个函数创建ovserver的时候需要传递进一个函数指针,而这个函数指针可能用在n多个oberver 可以当做区分是哪个observer的状机态。(下面的通过block创建的observer一般是一对一的,一般也不需要Context,),还有一个例子类似与NSNOtificationCenter的 SEL和 Block方式。
observer = CFRunLoopObserverCreate(
kCFAllocatorDefault,
CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.exit.rawValue,
true, 0xFFFFFF,
YYRunLoopObserverCallBack,
nil
)
//将观察者添加到主线程runloop的common模式下的观察中
CFRunLoopAddObserver(runloop, observer, .commonModes)
observer = nil
}
}
不难理解这段代码的主要作用:
- 是观察
RunLoop
的状态为beforeWaiting
、exit
时执行回调。 - 其中
selector
其实就是CATransaction
中的事物,target
就是执行selector
的对象
结论
那现在我们就可以基本明白了YYTransaction
的作用就是,
把你需要执行的方法(事物),先存储起来,等到RunLoop
的状态为beforeWaiting
、exit
时统一执行。
通过这两篇源码的分析,异步绘制这个概念应该在大脑里已经有了一定的印象了,稍加练习,其实就可以熟练掌握。
尾巴
YYAsyncLayer
的核心就是这些了,其实通篇看下来,你会发现基本没有什么费脑的地方。在佩服作者的同时,我们更多的是需要反思自己,虽然每个人的天赋不一样,但是我们的努力程度之低,往往没到拼天赋那一步。
在下一篇文章中我会逐一对这些进行回答,并且贴出它们的原理,与大家一同真正掌握iOS优化
- 为什么需要60fps?
- 为什么要减少混合?
- 为什么要避免离屏渲染?
- UIView和CALayer的关系?
- 为什么在4之后Twitter的绘制方案不能提升性能了?
......
推荐文章
RunLoop:
深入理解RunLoop
iOS线下分享《RunLoop》
iOS RunLoop 编程手册 (译)
runloop原理
YYAsyncLayer使用:
http://www.itwendao.com/article/detail/62384.html
其他
iOS Core Animation: Advanced Techniques中文译本