YYAsyncLayer是ibireme開源用于圖層異步繪制的一個組件,將耗時操作(如文本布局計算)放在RunLoop空閑時去做,進(jìn)而減少卡頓,代碼我也是寫了Swift版本YYAsyncLayerSwift儡羔。
YYAsyncLayer結(jié)構(gòu)
YYAsyncLayer一共分為三個部分:
- YYTransaction:將YYAsyncLayer委托的繪制任務(wù)注冊Runloop調(diào)用宣羊,在RunLoop空閑時執(zhí)行
- YYSentine:線程安全的計數(shù)器,用于判斷異步繪制任務(wù)是否被取消
- YYAsyncLayer:CALayer子類汰蜘,用來異步渲染layer內(nèi)容
YYAsyncLayer內(nèi)使用YYTransaction在 RunLoop 中注冊了一個 Observer仇冯,監(jiān)視的事件和 Core Animation 一樣,但優(yōu)先級比 CA 要低族操。當(dāng) RunLoop 進(jìn)入休眠前苛坚、CA 處理完事件后,YYTransaction 就會執(zhí)行該 loop 內(nèi)提交的所有任務(wù)色难。 在YYAsyncLayer中泼舱,通過重寫CALayer顯示display方法,向delegate請求一個異步繪制的任務(wù)枷莉,并且在子線程中繪制Core Graphic對象娇昙,最后再回到主線程中設(shè)置layer.contents內(nèi)容。
YYAsyncLayer 是 CALayer 的子類笤妙,當(dāng)它需要顯示內(nèi)容(比如調(diào)用了 [layer setNeedDisplay])時冒掌,它會向 delegate噪裕,也就是 UIView 請求一個異步繪制的任務(wù)。在異步繪制時股毫,Layer 會傳遞一個 BOOL(^isCancelled)() 這樣的 block膳音,繪制代碼可以隨時調(diào)用該 block 判斷繪制任務(wù)是否已經(jīng)被取消。
異步繪制思路圖
YYTransaction
YYTransaction繪制任務(wù)的機(jī)制仿照CoreAnimation的繪制機(jī)制铃诬,監(jiān)聽主線程RunLoop祭陷,在空閑階段插入繪制任務(wù),并將任務(wù)優(yōu)先級設(shè)置在CoreAnimation繪制完成之后氧急,然后遍歷繪制任務(wù)集合進(jìn)行繪制工作并且清空集合颗胡。
事務(wù)是通過CATransaction類來做管理毫深,這個類的設(shè)計有些奇怪吩坝,不像你從它的命名預(yù)期的那樣去管理一個簡單的事務(wù),而是管理了一疊你不能訪問的事務(wù)哑蔫。CATransaction沒有屬性或者實(shí)例方法钉寝,并且也不能用+alloc和-init方法創(chuàng)建它。但是可以用+begin和+commit分別來入椪⒚裕或者出棧嵌纲。
任何可以做動畫的圖層屬性都會被添加到棧頂?shù)氖聞?wù),你可以通過+setAnimationDuration:方法設(shè)置當(dāng)前事務(wù)的動畫時間腥沽,或者通過+animationDuration方法來獲取值(默認(rèn)0.25秒)逮走。
Core Animation在每個run loop周期中自動開始一次新的事務(wù)(run loop是iOS負(fù)責(zé)收集用戶輸入,處理定時器或者網(wǎng)絡(luò)事件并且重新繪制屏幕的東西)今阳,即使你不顯式的用[CATransaction begin]開始一次事務(wù)师溅,任何在一次run loop循環(huán)中屬性的改變都會被集中起來,然后做一次0.25秒的動畫盾舌。
YYTransaction存儲了target和selector墓臭,并且在runloop中注冊kCFRunLoopBeforeWaiting與kCFRunLoopExit。
主線程 RunLoop觀察者在RunLoop進(jìn)入kCFRunLoopBeforeWaiting或kCFRunLoopExit開始執(zhí)行觀察者妖谴。
注意指定了觀察者的優(yōu)先級:0xFFFFFF窿锉,這個優(yōu)先級比CATransaction優(yōu)先級為2000000的優(yōu)先級更低。這是為了確保系統(tǒng)的動畫優(yōu)先執(zhí)行膝舅,之后再執(zhí)行異步渲染嗡载。
private let onceToken = UUID().uuidString
private var transactionSet: Set<YYTransaction>?
private func YYTransactionSetup() {
DispatchQueue.once(token: onceToken) {
transactionSet = Set()
/// 獲取main RunLoop
let runloop = CFRunLoopGetCurrent()
var observer: CFRunLoopObserver?
//RunLoop循環(huán)的回調(diào)
let YYRunLoopObserverCallBack: CFRunLoopObserverCallBack = {_,_,_ in
guard (transactionSet?.count) ?? 0 > 0 else { return }
let currentSet = transactionSet
//取完上一次需要調(diào)用的YYTransaction事務(wù)對象后后進(jìn)行清空
transactionSet = Set()
//遍歷set,執(zhí)行里面的selector
for transaction in currentSet! {
_ = (transaction.target as AnyObject).perform(transaction.selector)
}
}
/**
創(chuàng)建一個RunLoop的觀察者
allocator:該參數(shù)為對象內(nèi)存分配器仍稀,一般使用默認(rèn)的分配器kCFAllocatorDefault洼滚。或者nil
activities:該參數(shù)配置觀察者監(jiān)聽Run Loop的哪種運(yùn)行狀態(tài)琳轿,這里我們監(jiān)聽beforeWaiting和exit狀態(tài)
repeats:CFRunLoopObserver是否循環(huán)調(diào)用。
order:CFRunLoopObserver的優(yōu)先級癞松,當(dāng)在Runloop同一運(yùn)行階段中有多個CFRunLoopObserver時怠硼,根據(jù)這個來先后調(diào)用CFRunLoopObserver,0為最高優(yōu)先級別吧秕。正常情況下使用0。
callout:觀察者的回調(diào)函數(shù)迹炼,在Core Foundation框架中用CFRunLoopObserverCallBack重定義了回調(diào)函數(shù)的閉包砸彬。
context:觀察者的上下文。 (類似與KVO傳遞的context斯入,可以傳遞信息砂碉,)因?yàn)檫@個函數(shù)創(chuàng)建ovserver的時候需要傳遞進(jìn)一個函數(shù)指針,而這個函數(shù)指針可能用在n多個oberver 可以當(dāng)做區(qū)分是哪個observer的狀機(jī)態(tài)刻两。(下面的通過block創(chuàng)建的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 let you perform a selector once before current runloop sleep.
*/
class YYTransaction: NSObject {
var target: Any?
var selector: Selector?
/**
創(chuàng)建和返回一個transaction通過一個定義的target和selector
@param target 執(zhí)行target滋迈,target會在runloop結(jié)束前被retain
@param selector target的selector
@return 1個新的transaction,或者有錯誤時返回nil
*/
static func transaction(with target: AnyObject, selector: Selector) -> YYTransaction?{
let t = YYTransaction()
t.target = target
t.selector = selector
return t
}
/**
Commit the trancaction to main runloop.
@discussion It will perform the selector on the target once before main runloop's
current loop sleep. If the same transaction (same target and same selector) has
already commit to runloop in this loop, this method do nothing.
*/
func commit() {
guard target != nil && selector != nil else {
//初始化runloop監(jiān)聽
YYTransactionSetup()
//添加行為到Set中
transactionSet?.insert(self)
return
}
}
/**
因?yàn)樵搶ο筮€要被存放至Set集合中户誓,通過重寫isEqual和hash來支持根據(jù)selector,target判斷相等性.
確保不會將具有相同target和selector的委托對象放入Set中
*/
override var hash: Int {
let v1 = selector?.hashValue ?? 0
let v2 = (target as AnyObject).hashValue ?? 0
return v1 ^ v2
}
override func isEqual(_ object: Any?) -> Bool {
guard let other = object as? YYTransaction else {
return false
}
guard other != self else {
return true
}
return other.selector == selector
}
}
YYSentine
YYSentine對OSAtomicIncrement32()函數(shù)的封裝, 改函數(shù)為一個線程安全的計數(shù)器,用于判斷異步繪制任務(wù)是否被取消
/**
線程安全的計數(shù)器
YYSentine對OSAtomicIncrement32()函數(shù)的封裝, 改函數(shù)為一個線程安全的計數(shù)器,用于判斷異步繪制任務(wù)是否被取消
*/
class YYSentinel: NSObject {
private var _value: Int32 = 0
public var value: Int32 {
return _value
}
@discardableResult
public func increase() -> Int32 {
// OSAtomic原子操作更趨于數(shù)據(jù)的底層饼灿,從更深層次來對單例進(jìn)行保護(hù)。同時帝美,它沒有阻斷其它線程對函數(shù)的訪問碍彭。
return OSAtomicIncrement32(&_value)
}
}
YYAsyncLayer
YYAsyncLayer為了異步繪制而繼承CALayer的子類。通過使用CoreGraphic相關(guān)方法悼潭,在子線程中繪制內(nèi)容Context庇忌,繪制完成后,回到主線程對layer.contents進(jìn)行直接顯示女责。
通過開辟線程進(jìn)行異步繪制漆枚,但是不能無限開辟線程
我們都知道,把阻塞主線程執(zhí)行的代碼放入另外的線程里保證APP可以及時的響應(yīng)用戶的操作抵知。但是線程的切換也是需要額外的開銷的墙基。也就是說,線程不能無限度的開辟下去刷喜。
那么残制,dispatch_queue_t的實(shí)例也不能一直增加下去。有人會說可以用dispatch_get_global_queue()來獲取系統(tǒng)的隊(duì)列掖疮。沒錯初茶,但是這個情況只適用于少量的任務(wù)分配。因?yàn)樽巧粒到y(tǒng)本身也會往這個queue里添加任務(wù)的恼布。
所以螺戳,我們需要用自己的queue,但是是有限個的折汞。在YY里給這個數(shù)量指定的值是16倔幼。
YYAsyncLayerDelegate
YYAsyncLayerDelegate 的 newAsyncDisplayTask 是提供了 YYAsyncLayer 需要在后臺隊(duì)列繪制的內(nèi)容
/**
YYAsyncLayer's的delegate協(xié)議,一般是uiview爽待。必須實(shí)現(xiàn)這個方法
*/
protocol YYAsyncLayerDelegate {
//當(dāng)layer的contents需要更新的時候损同,返回一個新的展示任務(wù)
var newAsyncDisplayTask: YYAsyncLayerDisplayTask { get }
}
YYAsyncLayerDisplayTask
display在mainthread或者background thread調(diào)用,這要求display應(yīng)該是線程安全的鸟款,這里是通過YYSentinel保證線程安全膏燃。willdisplay和didDisplay在mainthread調(diào)用。
/**
YYAsyncLayer在后臺渲染contents的顯示任務(wù)類
*/
open class YYAsyncLayerDisplayTask: NSObject {
/**
這個block會在異步渲染開始的前調(diào)用何什,只在主線程調(diào)用组哩。
*/
public var willDisplay: ((CALayer) -> Void)?
/**
這個block會調(diào)用去顯示layer的內(nèi)容
*/
public var display: ((_ context: CGContext, _ size: CGSize, _ isCancelled: (() -> Bool)?) -> Void)?
/**
這個block會在異步渲染結(jié)束后調(diào)用,只在主線程調(diào)用富俄。
*/
public var didDisplay: ((_ layer: CALayer, _ finished: Bool) -> Void)?
}
YYAsyncLayer
YYAsyncLayer是通過創(chuàng)建異步創(chuàng)建圖像Context在其繪制禁炒,最后再主線程異步添加圖像從而實(shí)現(xiàn)的異步繪制。同時霍比,在繪制過程中進(jìn)行了多次進(jìn)行取消判斷,以避免額外繪制.
import Foundation
import UIKit
//全局釋放隊(duì)列
private let YYAsyncLayerGetReleaseQueue = DispatchQueue.global(qos: .utility)
private let onceToken = UUID().uuidString
private let MAX_QUEUE_COUNT = 16
private var queueCount = 0
private var queues = [DispatchQueue](repeating: DispatchQueue(label: ""), count: MAX_QUEUE_COUNT)
private var counter: Int32 = 0
//全局顯示隊(duì)列暴备,給content渲染用
private let YYAsyncLayerGetDisplayQueue: DispatchQueue = {
////GCD只運(yùn)行一次悠瞬。使用字符串token作為once的ID,執(zhí)行once的時候加了一個鎖涯捻,避免多線程下的token判斷不準(zhǔn)確的問題浅妆。
DispatchQueue.once(token: onceToken) {
// https://cnbin.github.io/blog/2015/05/21/nsprocessinfo-huo-qu-jin-cheng-xin-xi/
// queueCount = 運(yùn)行該進(jìn)程的系統(tǒng)的處于激活狀態(tài)的處理器數(shù)量
queueCount = ProcessInfo().activeProcessorCount
//處理器數(shù)量,最多創(chuàng)建16個serial線程
queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount
//創(chuàng)建指定數(shù)量的串行隊(duì)列存放在隊(duì)列數(shù)組中
for i in 0 ..< queueCount {
queues[i] = DispatchQueue(label: "com.ibireme.MTkit.render")
}
}
//此為線程安全的自增計數(shù),每調(diào)用一次+1
var cur = OSAtomicIncrement32(&counter)
if cur < 0 {
cur = -cur
}
return queues[Int(cur) % queueCount]
}()
/**
YYAsyncLayer是異步渲染的CALayer子類
*/
class YYAsyncLayer: CALayer {
//是否異步渲染
var displaysAsynchronously = true
//計數(shù)障癌,用于取消異步繪制
var _sentinel: YYSentinel!
var scale: CGFloat = 0
private let _onceToken = UUID().uuidString
override class func defaultValue(forKey key: String) -> Any? {
if key == "displaysAsynchronously" {
return true
} else {
return super.defaultValue(forKey: key)
}
}
override init() {
super.init()
DispatchQueue.once(token: _onceToken) {
scale = UIScreen.main.scale
}
//默認(rèn)異步,每個圖層都配置一個計數(shù)器
contentsScale = scale
_sentinel = YYSentinel()
}
//取消繪制
deinit {
_sentinel.increase()
}
//需要重新渲染的時候凌外,取消原來沒有完成的異步渲染
override func setNeedsDisplay() {
self.cancelAsyncDisplay()
super.setNeedsDisplay()
}
/**
重寫展示方法,設(shè)置contents內(nèi)容
*/
override func display() {
super.contents = super.contents
displayAsync(async: displaysAsynchronously)
}
func displayAsync(async: Bool) {
//獲取delegate對象涛浙,這邊默認(rèn)是CALayer的delegate康辑,持有它的UIView
guard let delegate = self.delegate as? YYAsyncLayerDelegate else { return }
//delegate的初始化方法
let task = delegate.newAsyncDisplayTask
if task.display == nil {
task.willDisplay?(self)
contents = nil
task.didDisplay?(self, true)
return
}
if async {
task.willDisplay?(self)
let sentinel = _sentinel
let value = sentinel!.value
//判斷是否要取消的block,在displayblock調(diào)用繪制前轿亮,可以通過判斷isCancelled布爾值的值來停止繪制疮薇,減少性能上的消耗,以及避免出現(xiàn)線程阻塞的情況我注,比如TableView快速滑動的時候按咒,就可以通過這樣的判斷,來避免不必要的繪制但骨,提升滑動的流暢性.
let isCancelled = {
return value != sentinel!.value
}
let size = bounds.size
let opaque = isOpaque
let scale = contentsScale
var backgroundColor = (opaque && (self.backgroundColor != nil)) ? self.backgroundColor : nil
// 當(dāng)圖層寬度或高度小于1時(此時沒有繪制意義)
if size.width < 1 || size.height < 1 {
//獲取contents內(nèi)容
var image = contents
//清除內(nèi)容
contents = nil
//當(dāng)圖層內(nèi)容為圖片時,將釋放操作留在并行釋放隊(duì)列中進(jìn)行
if (image != nil) {
YYAsyncLayerGetReleaseQueue.async {
image = nil
}
//已經(jīng)展示完成block励七,finish為yes
task.didDisplay?(self, true)
backgroundColor = nil
return
}
}
// 異步繪制
YYAsyncLayerGetDisplayQueue.async {
guard !isCancelled() else { return }
/**
系統(tǒng)會維護(hù)一個CGContextRef的棧智袭,UIGraphicsGetCurrentContext()會取出棧頂?shù)腸ontext,所以在setFrame調(diào)用UIGraphicsGetCurrentContext(), 但獲得的上下文總是nil掠抬。只能在drawRect里調(diào)用UIGraphicsGetCurrentContext()补履,
因?yàn)樵赿rawRect之前,系統(tǒng)會往棧里面壓入一個valid的CGContextRef剿另,除非自己去維護(hù)一個CGContextRef箫锤,否則不應(yīng)該在其他地方取CGContextRef。
那如果就像在drawRect之外獲得context怎么辦雨女?那只能自己創(chuàng)建位圖上下文了
*/
/**
UIGraphicsBeginImageContext這個方法也可以來獲取圖形上下文進(jìn)行繪制的話就會出現(xiàn)你繪制出來的圖片相當(dāng)?shù)哪:柙埽鋵?shí)原因很簡單
因?yàn)?UIGraphicsBeginImageContext(size) = UIGraphicsBeginImageContextWithOptions(size,NO,1.0)
*/
/**
創(chuàng)建一個圖片類型的上下文。調(diào)用UIGraphicsBeginImageContextWithOptions函數(shù)就可獲得用來處理圖片的圖形上下文氛堕。利用該上下文馏臭,你就可以在其上進(jìn)行繪圖,并生成圖片
第一個參數(shù)表示所要創(chuàng)建的圖片的尺寸
第二個參數(shù)表示這個圖層是否完全透明讼稚,一般情況下最好設(shè)置為YES括儒,這樣可以讓圖層在渲染的時候效率更高
第三個參數(shù)指定生成圖片的縮放因子,這個縮放因子與UIImage的scale屬性所指的含義是一致的锐想。傳入0則表示讓圖片的縮放因子根據(jù)屏幕的分辨率而變化帮寻,所以我們得到的圖片不管是在單分辨率還是視網(wǎng)膜屏上看起來都會很好
*/
UIGraphicsBeginImageContextWithOptions(size, opaque, scale)
guard let context = UIGraphicsGetCurrentContext() else { return }
//將坐標(biāo)系上下翻轉(zhuǎn)
context.textMatrix = CGAffineTransform.identity
context.translateBy(x: 0, y: self.bounds.height)
context.scaleBy(x: 1, y: -1)
if opaque {
/**
使用Quartz時涉及到一個圖形上下文,其中圖形上下文中包含一個保存過的圖形狀態(tài)堆棧赠摇。在Quartz創(chuàng)建圖形上下文時固逗,該堆棧是空的。CGContextSaveGState函數(shù)的作用是將當(dāng)前圖形狀態(tài)推入堆棧藕帜。之后烫罩,您對圖形狀態(tài)所做的修改會影響隨后的描畫操作,但不影響存儲在堆棧中的拷貝洽故。在修改完成后贝攒,您可以通過CGContextRestoreGState函數(shù)把堆棧頂部的狀態(tài)彈出,返回到之前的圖形狀態(tài)时甚。這種推入和彈出的方式是回到之前圖形狀態(tài)的快速方法隘弊,避免逐個撤消所有的狀態(tài)修改;這也是將某些狀態(tài)(比如裁剪路徑)恢復(fù)到原有設(shè)置的唯一方式撞秋。
*/
context.saveGState()
if backgroundColor == nil || backgroundColor!.alpha < 1 {
//設(shè)置填充顏色长捧,setStrokeColor為邊框顏色
context.setFillColor(UIColor.white.cgColor)
//添加矩形邊框路徑
context.addRect(CGRect(x: 0, y: 0, width: size.width * scale, height: size.height * scale))
context.fillPath()
}
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()
backgroundColor = nil
}
task.display?(context, size, isCancelled)
//若取消 則釋放資源,取消繪制
if isCancelled() {
//調(diào)用UIGraphicsEndImageContext函數(shù)關(guān)閉圖形上下文
UIGraphicsEndImageContext()
DispatchQueue.main.async {
task.didDisplay?(self, false)
}
return
}
//UIGraphicsGetImageFromCurrentImageContext函數(shù)可從當(dāng)前上下文中獲取一個UIImage對象
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
//若取消 則釋放資源,取消繪制
if isCancelled() {
UIGraphicsEndImageContext()
DispatchQueue.main.async {
task.didDisplay?(self, false)
}
return
}
//主線程異步將繪制結(jié)果的圖片賦值給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)
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)
}
}
private func cancelAsyncDisplay() {
// 增加計數(shù),標(biāo)明取消之前的渲染
_sentinel.increase()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
需要注意的是繪制的圖片會發(fā)生倒置問題吻贿,源碼中并未進(jìn)行立正操作串结。
究其原因是因?yàn)镃oreGraphics源于Mac OS X系統(tǒng),在Mac OS X中,坐標(biāo)原點(diǎn)在左下方并且正y坐標(biāo)是朝上的肌割,而在iOS中卧蜓,原點(diǎn)坐標(biāo)是在左上方并且正y坐標(biāo)是朝下的。在大多數(shù)情況下把敞,這不會出現(xiàn)任何問題弥奸,因?yàn)閳D形上下文的坐標(biāo)系統(tǒng)是會自動調(diào)節(jié)補(bǔ)償?shù)摹5莿?chuàng)建和繪制一個CGImage對象時就會暴露出倒置問題奋早。所以用到以下函數(shù)進(jìn)行立正
context.textMatrix = CGAffineTransform.identity
context.translateBy(x: 0, y: self.bounds.height)
context.scaleBy(x: 1, y: -1)