YYAsyncLayer源碼分析
本節(jié)關鍵字
- 異步繪制
- RunLoop
這是YYAsyncLayer
的結構
YYAsyncLayer:異步繪制的CALayer子類贷洲,這個類做的核心和
VVeboTableViewDemo
中VVeboLabel
的核心是一模一樣的缸榄。你可以到回去看看從VVeboTableViewDemo到YYAsyncLayer(一)YYSentinel:線程安全的計數(shù)器伯复。
YYTransaction:注冊RunLoop在系統(tǒng)空閑時調(diào)用樱报。(如果你不了解或者沒有聽說過RunLoop阵面,不用擔心,下面我同樣會推薦相關的文章誊涯,讓你了解和實踐RunLoop)
YYAsyncLayerDisplayTask: 用于回調(diào)畫布
YYAsyncLayerDelegate: 給接收
YYAsyncLayer
的UIView開的接口
本文依然是用Swift版YYAsyncLayer
進行分析
YYAsyncLayer
上圖中高亮的方法為整個類的核心方法
private func _displayAsync(_ async: Bool) {
/// 如果需要使用異步繪制的地方?jīng)]有實現(xiàn)該代理挡毅,直接返回
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中是函數(shù)調(diào)用,在這里是這個isCancelled block丹壕。所以每次提交到queue的task的isCancelled block是不同的block對象,其中捕獲的value的值都是這個task創(chuàng)建時sentinel.value的值薇溃,而捕獲的sentinel的引用都是這個layer的sentinel的引用菌赖,最后在block執(zhí)行的時候,value的值就是捕獲的value沐序,而sentinel.value則可能已經(jīng)發(fā)生了變化琉用。
*/
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
}
/// 第一個參數(shù)表示所要創(chuàng)建的圖片的尺寸;
/// 第二個參數(shù)用來指定所生成圖片的背景是否為不透明策幼,如上我們使用true而不是false邑时,則我們得到的圖片背景將會是黑色,顯然這不是我想要的特姐;
/// 第三個參數(shù)指定生成圖片的縮放因子晶丘,這個縮放因子與UIImage的scale屬性所指的含義是一致的。傳入0則表示讓圖片的縮放因子根據(jù)屏幕的分辨率而變化,所以我們得到的圖片不管是在單分辨率還是視網(wǎng)膜屏上看起來都會很好浅浮。
/// 注意這個與UIGraphicsEndImageContext()成對出現(xiàn)
/// iOS10 中新增了UIGraphicsImageRenderer(bounds: _)
UIGraphicsBeginImageContextWithOptions(size, opaque, scale)
/// 獲取繪制畫布
/// 每一個UIView都有一個layer沫浆,每一個layer都有個content,這個content指向的是一塊緩存滚秩,叫做backing store专执。
/// UIView的繪制和渲染是兩個過程,當UIView被繪制時郁油,CPU執(zhí)行drawRect本股,通過context將數(shù)據(jù)寫入backing store
/// http://vizlabxt.github.io/blog/2012/10/22/UIView-Rendering/
guard let context = UIGraphicsGetCurrentContext() else { return }
if opaque {
/*
成對出現(xiàn)
CGContextSaveGState與CGContextRestoreGState的作用
使用Quartz時涉及到一個圖形上下文,其中圖形上下文中包含一個保存過的圖形狀態(tài)堆棧桐腌。在Quartz創(chuàng)建圖形上下文時拄显,該堆棧是空的。CGContextSaveGState函數(shù)的作用是將當前圖形狀態(tài)推入堆棧哩掺。之后凿叠,您對圖形狀態(tài)所做的修改會影響隨后的描畫操作,但不影響存儲在堆棧中的拷貝嚼吞。在修改完成后盒件。
您可以通過CGContextRestoreGState函數(shù)把堆棧頂部的狀態(tài)彈出,返回到之前的圖形狀態(tài)舱禽。這種推入和彈出的方式是回到之前圖形狀態(tài)的快速方法炒刁,避免逐個撤消所有的狀態(tài)修改;這也是將某些狀態(tài)(比如裁剪路徑)恢復到原有設置的唯一方式誊稚。
*/
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()
}
// 回調(diào)繪制
task.display?(context, size, isCancelled)
// 如果取消,提前結束繪制
if isCancelled() {
UIGraphicsEndImageContext()
DispatchQueue.main.async {
task.didDisplay?(self, false)
}
return
}
// 從畫布中獲取圖片里伯,與UIGraphicsEndImageContext()成對出現(xiàn)
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
很相似盆均。其實他們不僅命名很相似,就是內(nèi)部結構也很相似肩碟。
再看YYTransaction
:
其中YYTransactionSetup
func YYTransactionSetup() {
DispatchQueue.once(token: onceToken) {
transactionSet = Set()
/// 獲取main RunLoop
let runloop = CFRunLoopGetMain()
var observer: CFRunLoopObserver?
/// http://www.reibang.com/p/6757e964b956
/// 創(chuàng)建一個RunLoop的觀察者
/// allocator:該參數(shù)為對象內(nèi)存分配器强窖,一般使用默認的分配器kCFAllocatorDefault∠髌恚或者nil
/// activities:該參數(shù)配置觀察者監(jiān)聽Run Loop的哪種運行狀態(tài)翅溺。在示例中,我們讓觀察者監(jiān)聽Run Loop的所有運行狀態(tài)。
/// repeats:該參數(shù)標識觀察者只監(jiān)聽一次還是每次Run Loop運行時都監(jiān)聽未巫。
/// order: 觀察者優(yōu)先級窿撬,當Run Loop中有多個觀察者監(jiān)聽同一個運行狀態(tài)時,那么就根據(jù)該優(yōu)先級判斷叙凡,0為最高優(yōu)先級別劈伴。
/// callout:觀察者的回調(diào)函數(shù),在Core Foundation框架中用CFRunLoopObserverCallBack重定義了回調(diào)函數(shù)的閉包握爷。
/// context:觀察者的上下文跛璧。 (類似與KVO傳遞的context,可以傳遞信息新啼,)因為這個函數(shù)創(chuàng)建ovserver的時候需要傳遞進一個函數(shù)指針追城,而這個函數(shù)指針可能用在n多個oberver 可以當做區(qū)分是哪個observer的狀機態(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
的用途和CATransaction
的用途是有某種相似之處的。
再看
蘋果對CATransaction的定義(你也可以看看這本書里的解釋)
A mechanism for batching multiple layer-tree operations into atomic updates to the render tree.
**谷歌翻譯: ** 用于將多個層樹操作批量化為渲染樹的原子更新的機制冠胯。
事務是通過CATransaction類來做管理火诸,這個類的設計有些奇怪,不像你從它的命名預期的那樣去管理一個簡單的事務荠察,而是管理了一疊你不能訪問的事務置蜀。CATransaction沒有屬性或者實例方法,并且也不能用+alloc和-init方法創(chuàng)建它悉盆。但是可以用+begin和+commit分別來入椂⒒纾或者出棧。
任何可以做動畫的圖層屬性都會被添加到棧頂?shù)氖聞栈烂耍憧梢酝ㄟ^+setAnimationDuration:方法設置當前事務的動畫時間廷雅,或者通過+animationDuration方法來獲取值(默認0.25秒)。
Core Animation在每個run loop周期中自動開始一次新的事務(run loop是iOS負責收集用戶輸入京髓,處理定時器或者網(wǎng)絡事件并且重新繪制屏幕的東西),即使你不顯式的用[CATransaction begin]開始一次事務商架,任何在一次run loop循環(huán)中屬性的改變都會被集中起來堰怨,然后做一次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.reibang.com/p/6757e964b956
/// 創(chuàng)建一個RunLoop的觀察者
/// allocator:該參數(shù)為對象內(nèi)存分配器,一般使用默認的分配器kCFAllocatorDefault揽涮】倥海或者nil
/// activities:該參數(shù)配置觀察者監(jiān)聽Run Loop的哪種運行狀態(tài)。在示例中蒋困,我們讓觀察者監(jiān)聽Run Loop的所有運行狀態(tài)盾似。
/// repeats:該參數(shù)標識觀察者只監(jiān)聽一次還是每次Run Loop運行時都監(jiān)聽。
/// order: 觀察者優(yōu)先級雪标,當Run Loop中有多個觀察者監(jiān)聽同一個運行狀態(tài)時零院,那么就根據(jù)該優(yōu)先級判斷,0為最高優(yōu)先級別村刨。
/// callout:觀察者的回調(diào)函數(shù)告抄,在Core Foundation框架中用CFRunLoopObserverCallBack重定義了回調(diào)函數(shù)的閉包。
/// context:觀察者的上下文嵌牺。 (類似與KVO傳遞的context打洼,可以傳遞信息,)因為這個函數(shù)創(chuàng)建ovserver的時候需要傳遞進一個函數(shù)指針逆粹,而這個函數(shù)指針可能用在n多個oberver 可以當做區(qū)分是哪個observer的狀機態(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
}
}
不難理解這段代碼的主要作用:
- 是觀察
RunLoop
的狀態(tài)為beforeWaiting
奢方、exit
時執(zhí)行回調(diào)搔扁。 - 其中
selector
其實就是CATransaction
中的事物,target
就是執(zhí)行selector
的對象
結論
那現(xiàn)在我們就可以基本明白了YYTransaction
的作用就是,
把你需要執(zhí)行的方法(事物)蟋字,先存儲起來稿蹲,等到RunLoop
的狀態(tài)為beforeWaiting
、exit
時統(tǒng)一執(zhí)行鹊奖。
通過這兩篇源碼的分析苛聘,異步繪制這個概念應該在大腦里已經(jīng)有了一定的印象了,稍加練習忠聚,其實就可以熟練掌握设哗。
尾巴
YYAsyncLayer
的核心就是這些了,其實通篇看下來两蟀,你會發(fā)現(xiàn)基本沒有什么費腦的地方网梢。在佩服作者的同時,我們更多的是需要反思自己赂毯,雖然每個人的天賦不一樣战虏,但是我們的努力程度之低拣宰,往往沒到拼天賦那一步。
在下一篇文章中我會逐一對這些進行回答烦感,并且貼出它們的原理巡社,與大家一同真正掌握iOS優(yōu)化
- 為什么需要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中文譯本