前言
异步绘制,就是可以在子线程把需要绘制的图形,提前在子线程处理好。将准备好的图像数据直接返给主线程使用,这样可以降低主线程的压力。
一 UIView绘制渲染原理和流程
1. UIView调用setNeedsDisplay(setNeedsDisplay会调用自动调用drawRect方法);
2. 系统会立刻调用view的layer的同名方法[view.layer setNeedsDisplay],之后相当于在layer上面打上了一个脏标记;
3. 然后再当前runloop将要结束的时候,才会调用CALayer的display函数方法,然后才进入到当前视图的真正绘制工作的流程当中;
4. runloop即将结束, 开始视图的绘制流程;
1.系统默认绘制流程
1. CALayer内部创建一个backing store(CGContextRef)();
2. 判断layer是否有代理(1.有代理:调用delegete的drawLayer:inContext, 然后在合适的 实际回调代理, 在[UIView drawRect]中做一些绘制工作;2. 没有代理:调用layer的drawInContext方法。)
3. layer上传backingStore到GPU, 结束系统的绘制流程;
2.异步绘制流程
1. 某个时机调用setNeedsDisplay;
2. runloop将要结束的时候调用[CALayer display]
3. 如果代理实现了dispalyLayer将会调用此方法, 在子线程中去做异步绘制的工作;
4. 子线程中做的工作:创建上下文, 控件的绘制, 生成图片;
5. 转到主线程, 设置layer.contents, 将生成的视图展示在layer上面;
主要思想:
//异步绘制:切换至子线程
DispatchQueue.global().async {
///获取当前上下文
UIGraphicsBeginImageContextWithOptions(size, false, scale)
//1.获取上下文
let context = UIGraphicsGetCurrentContext()
//TODO
...............
//生成图片
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
///子线程完成工作, 切换到主线程展示
DispatchQueue.main.async {
self.layer.contents = img
}
}
二 异步绘制源码解析(参考YYKit)
以一个异步绘制的Label为主体,主要包括XWAsyncLayerDelegate,XWAsyncLayerDisplayTask,XWLabel,XWTransaction,XWAsyncLayer,XWsentinel;关系类图如下:
异步绘制开始到结束流程:
1. 当XWLabel有新的更新提交时,通过XWTransaction将一个或者多个绘制的任务(layer.setNeedsDisplay)添加到transactionSet,并在Runloop注册了一个Observer
2. 当 RunLoop 进入休眠前、CA 处理完事件后,就会逐一执行transactionSet里的任务
3. 执行任务 layer.setNeedsDisplay会自动调用layer的display方法,判断是否需要异步绘制
4. 需要异步绘制,layer会向 delegate( UIView ),请求一个异步绘制的任务并将任务添加到异步队列中。在异步绘制时,Layer 会传递一个 BOOL(^isCancelled)() 这样的 block,绘制代码可以随时调用该 block 判断绘制任务是否已经被取消
5. 不需要异步绘制则直接同步绘制
2.1 XWTransaction之源码分析
XWTransaction存储了target和selector,通过仿照CoreAnimation的绘制机制,监听主线程RunLoop,在空闲阶段插入绘制任务,并将任务优先级设置在CoreAnimation绘制完成之后,然后遍历绘制任务集合进行绘制工作并且清空集合。
在runloop中注册observer:
private let onceToken = UUID().uuidString
private var transactionSet: Set<XWTransaction>?
private func XWTransactionSetup() {
DispatchQueue.once(token: onceToken) {
transactionSet = Set()
/// 获取main RunLoop
let runloop = CFRunLoopGetCurrent()
var observer: CFRunLoopObserver?
//RunLoop循环的回调
let XWRunLoopObserverCallBack: CFRunLoopObserverCallBack = {_,_,_ in
guard (transactionSet?.count) ?? 0 > 0 else { return }
let currentSet = transactionSet
//取完上一次需要调用的XWTransaction事务对象后后进行清空
transactionSet = Set()
//遍历set,执行里面的selector
for transaction in currentSet! {
_ = (transaction.target as AnyObject).perform(transaction.selector)
}
}
observer = CFRunLoopObserverCreate(
kCFAllocatorDefault,
CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.exit.rawValue,
true,
0xFFFFFF,
XWRunLoopObserverCallBack,
nil
)
//将观察者添加到主线程runloop的common模式下的观察中
CFRunLoopAddObserver(runloop, observer, .commonModes)
observer = nil
}
}
1. 通过GCD实现注册一次runLoop监听kCFRunLoopBeforeWaiting与kCFRunLoopExit(仅会注册一次)
2 . 通过transactionSet: Set<XWTransaction>添加事件任务集
2. 在runLoop处于beforeWaiting和exit时在回调里逐一执行transactionSet的任务
注意指定了观察者的优先级:0xFFFFFF,这个优先级比CATransaction优先级为2000000的优先级更低。这是为了确保系统的动画优先执行,之后再执行异步渲染。
事务是通过CATransaction类来做管理,管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。但是可以用+begin和+commit分别来入栈或者出栈。 任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)。 Core Animation在每个run loop周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西),即使你不显式的用[CATransaction begin]开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。
2.2 XWSentine之源码分析
XWSentine对OSAtomicIncrement32()函数的封装, 改函数为一个线程安全的计数器,用于判断异步绘制任务是否被取消
OSAtomicIncrement32是线程安全的,多线程下保障了数据的同步操作和安全
class XWSentinel: NSObject {
private var _value: Int32 = 0
public var value: Int32 {
return _value
}
@discardableResult
public func increase() -> Int32 {
// OSAtomic原子操作更趋于数据的底层,从更深层次来对单例进行保护。同时,它没有阻断其它线程对函数的访问。
return OSAtomicIncrement32(&_value)
}
}
因为在iOS10中,方法OSAtomicAdd32,OSAtomicDecrement32已经被废弃('OSAtomicIncrement32' is deprecated:first deprecated in iOS 10.0)
需要使用对应的方法替换,具体如下:
1.#import <stdatomic.h>
2.将对应的计数器,由int32_t类型设置为atomic_int类型
3.OSAtomicAdd32 替换-> atomic_fetch_add(&atomicCount,1);
OSAtomicDecrement32 替换-> atomic_fetch_sub(&atomicCount, 1);
注:在开发过程中有多线程需要共享和同时记录时可使用OSAtomicIncrement32,或者OSAtomicAdd32保障线程安全
2.3 XWAsyncLayerDelegate之源码分析
XWAsyncLayerDelegate 的 newAsyncDisplayTask 是提供了 XWAsyncLayer 需要在后台队列绘制的内容。异步绘制的UIView必须实现该协议且返回异步绘制task
/**
XWAsyncLayer's的delegate协议,一般是uiview。必须实现这个方法
*/
protocol XWAsyncLayerDelegate {
//当layer的contents需要更新的时候,返回一个新的展示任务
var newAsyncDisplayTask: XWAsyncLayerDisplayTask { get }
}
2.4 XWAsyncLayerDisplayTask之源码分析
display在mainthread或者background thread调用,这要求display应该是线程安全的,这里是通过XWSentinel保证线程安全。willdisplay和didDisplay在mainthread调用。
/**
XWAsyncLayer在后台渲染contents的显示任务类
*/
open class XWAsyncLayerDisplayTask: NSObject {
/**
这个block会在异步渲染开始的前调用,只在主线程调用。
*/
public var willDisplay: ((CALayer) -> Void)?
/**
这个block会调用去显示layer的内容
*/
public var display: ((_ context: CGContext, _ size: CGSize, _ isCancelled: (() -> Bool)?) -> Void)?
/**
这个block会在异步渲染结束后调用,只在主线程调用。
*/
public var didDisplay: ((_ layer: CALayer, _ finished: Bool) -> Void)?
}
2.4 XWAsyncLayer之源码分析
XWAsyncLayer为了异步绘制而继承CALayer的子类。通过使用CoreGraphic相关方法,在子线程中绘制内容Context,绘制完成后,回到主线程对layer.contents进行直接显示。 通过开辟线程进行异步绘制,但是不能无限开辟线程
我们都知道,把阻塞主线程执行的代码放入另外的线程里保证APP可以及时的响应用户的操作。但是线程的切换也是需要额外的开销的。也就是说,线程不能无限度的开辟下去。
那么,dispatch_queue_t的实例也不能一直增加下去。有人会说可以用dispatch_get_global_queue()来获取系统的队列。没错,但是这个情况只适用于少量的任务分配。因为,系统本身也会往这个queue里添加任务的。
所以,我们需要用自己的queue,但是是有限个的。参考YY这个数量指定的值是16。
异步绘制主要代码如下:
func displayAsync(async: Bool) {
//获取delegate对象,这边默认是CALayer的delegate,持有它的UIView
guard let delegate = self.delegate as? XWAsyncLayerDelegate else { return }
//delegate的初始化方法
let task = delegate.newAsyncDisplayTask
if async {
task.willDisplay?(self)
let sentinel = _sentinel
let value = sentinel!.value
//判断是否要取消的block,在displayblock调用绘制前,可以通过判断isCancelled布尔值的值来停止绘制,减少性能上的消耗,以及避免出现线程阻塞的情况,比如TableView快速滑动的时候,就可以通过这样的判断,来避免不必要的绘制,提升滑动的流畅性.
let isCancelled = {
return value != sentinel!.value
}
// 异步绘制
XWAsyncLayerGetDisplayQueue.async {
guard !isCancelled() else { return }
//获取上下文和size
..............
//异步绘制
task.display?(context, size, isCancelled)
//若取消 则释放资源,取消绘制
if isCancelled() {
//调用UIGraphicsEndImageContext函数关闭图形上下文
UIGraphicsEndImageContext()
DispatchQueue.main.async {
task.didDisplay?(self, false)
}
return
}
//主线程异步将绘制结果的图片赋值给contents
DispatchQueue.main.async {
if isCancelled() {
task.didDisplay?(self, false)
}else{
self.contents = image?.cgImage
task.didDisplay?(self, true)
}
}
}
}else{
同步绘制
_sentinel.increase()
task.willDisplay?(self)
task.display?(context, bounds.size, {return false })
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
contents = image?.cgImage
task.didDisplay?(self, true)
}
}
display任务解析:
1. isCancelled捕获_sentinel计数器
2. 将异步绘制内容添加到异步队列中
3. 绘制任务开始时先通过isCancelled判断是否取消绘制,为false时通过获取de legate的task开启绘制
4. 绘制完成后关闭上下文,切回到主线程,将绘制的image赋值给layer的contens
注:isCancelled的block对_sentinel的value的捕获以和当前值比较以达到判断是否需要取消绘制
2.4 XWLabel之源码分析
XWLabel通过实现XWAsyncLayerDelegate协议返回异步绘制的XWAsyncLayerDisplayTask任务,重写layerClass返回自定义的XWAsyncLayer以实现异步绘制
class XWLabel: UIView, XWAsyncLayerDelegate {
var attributedText: NSAttributedString? {
didSet {
if self.attributedText?.length ?? 0 > 0 {
self.commitUpdate()
}
}
}
var displaysAsynchronously: Bool = false {
didSet{
if let asyncLayer = self.layer as? XWAsyncLayer {
asyncLayer.displaysAsynchronously = self.displaysAsynchronously
}
}
}
///MARK:XWAsyncLayerDelegate 返回绘制任务
var newAsyncDisplayTask: XWAsyncLayerDisplayTask {
let task = XWAsyncLayerDisplayTask()
task.willDisplay = { layer in
}
task.display = { (context, size, isCancel) in
}
task.didDisplay = { (layer, finished) in
}
return task
}
///MARK: 重写layerClass,返回异步的XWAsyncLayer
override class var layerClass: AnyClass {
return XWAsyncLayer.self
}
///MARK: 提交更新,添加到runLoop队列中
func commitUpdate() {
//XWTransaction.transaction(with: self, selector: #selector(layoutNeedRedraw))?.commit()
self.layoutNeedRedraw()
}
@objc func layoutNeedRedraw() {
self.layer.setNeedsDisplay()
}
}
总结
最后,我们把整个异步渲染的过程来串联起来。
1. UIView触发layoutSubviews,或者主动调用layer的setNeedsDisplay
2. layer调用display方法
3. 判断是否需要异步,需要异步将绘制任务添加到队列中
4. 绘制完成切回主线程,设置layer的contents