前言
異步繪制吐葱,就是可以在子線程把需要繪制的圖形街望,提前在子線程處理好。將準(zhǔn)備好的圖像數(shù)據(jù)直接返給主線程使用弟跑,這樣可以降低主線程的壓力灾前。
一 UIView繪制渲染原理和流程
1. UIView調(diào)用setNeedsDisplay(setNeedsDisplay會(huì)調(diào)用自動(dòng)調(diào)用drawRect方法);
2. 系統(tǒng)會(huì)立刻調(diào)用view的layer的同名方法[view.layer setNeedsDisplay],之后相當(dāng)于在layer上面打上了一個(gè)臟標(biāo)記;
3. 然后再當(dāng)前runloop將要結(jié)束的時(shí)候,才會(huì)調(diào)用CALayer的display函數(shù)方法,然后才進(jìn)入到當(dāng)前視圖的真正繪制工作的流程當(dāng)中;
4. runloop即將結(jié)束, 開始視圖的繪制流程;
1.系統(tǒng)默認(rèn)繪制流程
1. CALayer內(nèi)部創(chuàng)建一個(gè)backing store(CGContextRef)();
2. 判斷l(xiāng)ayer是否有代理(1.有代理:調(diào)用delegete的drawLayer:inContext, 然后在合適的 實(shí)際回調(diào)代理, 在[UIView drawRect]中做一些繪制工作;2. 沒有代理:調(diào)用layer的drawInContext方法孟辑。)
3. layer上傳backingStore到GPU, 結(jié)束系統(tǒng)的繪制流程;
2.異步繪制流程
1. 某個(gè)時(shí)機(jī)調(diào)用setNeedsDisplay;
2. runloop將要結(jié)束的時(shí)候調(diào)用[CALayer display]
3. 如果代理實(shí)現(xiàn)了dispalyLayer將會(huì)調(diào)用此方法, 在子線程中去做異步繪制的工作;
4. 子線程中做的工作:創(chuàng)建上下文, 控件的繪制, 生成圖片;
5. 轉(zhuǎn)到主線程, 設(shè)置layer.contents, 將生成的視圖展示在layer上面;
主要思想:
//異步繪制:切換至子線程
DispatchQueue.global().async {
///獲取當(dāng)前上下文
UIGraphicsBeginImageContextWithOptions(size, false, scale)
//1.獲取上下文
let context = UIGraphicsGetCurrentContext()
//TODO
...............
//生成圖片
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
///子線程完成工作, 切換到主線程展示
DispatchQueue.main.async {
self.layer.contents = img
}
}
二 異步繪制源碼解析(參考YYKit)
以一個(gè)異步繪制的Label為主體哎甲,主要包括XWAsyncLayerDelegate蔫敲,XWAsyncLayerDisplayTask,XWLabel炭玫,XWTransaction奈嘿,XWAsyncLayer,XWsentinel吞加;關(guān)系類圖如下:
異步繪制開始到結(jié)束流程:
1. 當(dāng)XWLabel有新的更新提交時(shí)裙犹,通過XWTransaction將一個(gè)或者多個(gè)繪制的任務(wù)(layer.setNeedsDisplay)添加到transactionSet,并在Runloop注冊(cè)了一個(gè)Observer
2. 當(dāng) RunLoop 進(jìn)入休眠前榴鼎、CA 處理完事件后,就會(huì)逐一執(zhí)行transactionSet里的任務(wù)
3. 執(zhí)行任務(wù) layer.setNeedsDisplay會(huì)自動(dòng)調(diào)用layer的display方法伯诬,判斷是否需要異步繪制
4. 需要異步繪制,layer會(huì)向 delegate( UIView ),請(qǐng)求一個(gè)異步繪制的任務(wù)并將任務(wù)添加到異步隊(duì)列中巫财。在異步繪制時(shí)盗似,Layer 會(huì)傳遞一個(gè) BOOL(^isCancelled)() 這樣的 block,繪制代碼可以隨時(shí)調(diào)用該 block 判斷繪制任務(wù)是否已經(jīng)被取消
5. 不需要異步繪制則直接同步繪制
2.1 XWTransaction之源碼分析
XWTransaction存儲(chǔ)了target和selector平项,通過仿照CoreAnimation的繪制機(jī)制赫舒,監(jiān)聽主線程RunLoop,在空閑階段插入繪制任務(wù)闽瓢,并將任務(wù)優(yōu)先級(jí)設(shè)置在CoreAnimation繪制完成之后接癌,然后遍歷繪制任務(wù)集合進(jìn)行繪制工作并且清空集合。
在runloop中注冊(cè)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循環(huán)的回調(diào)
let XWRunLoopObserverCallBack: CFRunLoopObserverCallBack = {_,_,_ in
guard (transactionSet?.count) ?? 0 > 0 else { return }
let currentSet = transactionSet
//取完上一次需要調(diào)用的XWTransaction事務(wù)對(duì)象后后進(jìn)行清空
transactionSet = Set()
//遍歷set扣讼,執(zhí)行里面的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實(shí)現(xiàn)注冊(cè)一次runLoop監(jiān)聽kCFRunLoopBeforeWaiting與kCFRunLoopExit(僅會(huì)注冊(cè)一次)
2 . 通過transactionSet: Set<XWTransaction>添加事件任務(wù)集
2. 在runLoop處于beforeWaiting和exit時(shí)在回調(diào)里逐一執(zhí)行transactionSet的任務(wù)
注意指定了觀察者的優(yōu)先級(jí):0xFFFFFF缺猛,這個(gè)優(yōu)先級(jí)比CATransaction優(yōu)先級(jí)為2000000的優(yōu)先級(jí)更低。這是為了確保系統(tǒng)的動(dòng)畫優(yōu)先執(zhí)行椭符,之后再執(zhí)行異步渲染荔燎。
事務(wù)是通過CATransaction類來做管理,管理了一疊你不能訪問的事務(wù)销钝。CATransaction沒有屬性或者實(shí)例方法有咨,并且也不能用+alloc和-init方法創(chuàng)建它。但是可以用+begin和+commit分別來入椪艚。或者出棧座享。 任何可以做動(dòng)畫的圖層屬性都會(huì)被添加到棧頂?shù)氖聞?wù),你可以通過+setAnimationDuration:方法設(shè)置當(dāng)前事務(wù)的動(dòng)畫時(shí)間似忧,或者通過+animationDuration方法來獲取值(默認(rèn)0.25秒)渣叛。 Core Animation在每個(gè)run loop周期中自動(dòng)開始一次新的事務(wù)(run loop是iOS負(fù)責(zé)收集用戶輸入,處理定時(shí)器或者網(wǎng)絡(luò)事件并且重新繪制屏幕的東西)橡娄,即使你不顯式的用[CATransaction begin]開始一次事務(wù)诗箍,任何在一次run loop循環(huán)中屬性的改變都會(huì)被集中起來,然后做一次0.25秒的動(dòng)畫挽唉。
2.2 XWSentine之源碼分析
XWSentine對(duì)OSAtomicIncrement32()函數(shù)的封裝, 改函數(shù)為一個(gè)線程安全的計(jì)數(shù)器,用于判斷異步繪制任務(wù)是否被取消
OSAtomicIncrement32是線程安全的滤祖,多線程下保障了數(shù)據(jù)的同步操作和安全
class XWSentinel: NSObject {
private var _value: Int32 = 0
public var value: Int32 {
return _value
}
@discardableResult
public func increase() -> Int32 {
// OSAtomic原子操作更趨于數(shù)據(jù)的底層,從更深層次來對(duì)單例進(jìn)行保護(hù)瓶籽。同時(shí)匠童,它沒有阻斷其它線程對(duì)函數(shù)的訪問。
return OSAtomicIncrement32(&_value)
}
}
因?yàn)樵趇OS10中塑顺,方法OSAtomicAdd32,OSAtomicDecrement32已經(jīng)被廢棄('OSAtomicIncrement32' is deprecated:first deprecated in iOS 10.0)
需要使用對(duì)應(yīng)的方法替換汤求,具體如下:
1.#import <stdatomic.h>
2.將對(duì)應(yīng)的計(jì)數(shù)器,由int32_t類型設(shè)置為atomic_int類型
3.OSAtomicAdd32 替換-> atomic_fetch_add(&atomicCount,1);
OSAtomicDecrement32 替換-> atomic_fetch_sub(&atomicCount, 1);
注:在開發(fā)過程中有多線程需要共享和同時(shí)記錄時(shí)可使用OSAtomicIncrement32严拒,或者OSAtomicAdd32保障線程安全
2.3 XWAsyncLayerDelegate之源碼分析
XWAsyncLayerDelegate 的 newAsyncDisplayTask 是提供了 XWAsyncLayer 需要在后臺(tái)隊(duì)列繪制的內(nèi)容扬绪。異步繪制的UIView必須實(shí)現(xiàn)該協(xié)議且返回異步繪制task
/**
XWAsyncLayer's的delegate協(xié)議,一般是uiview裤唠。必須實(shí)現(xiàn)這個(gè)方法
*/
protocol XWAsyncLayerDelegate {
//當(dāng)layer的contents需要更新的時(shí)候挤牛,返回一個(gè)新的展示任務(wù)
var newAsyncDisplayTask: XWAsyncLayerDisplayTask { get }
}
2.4 XWAsyncLayerDisplayTask之源碼分析
display在mainthread或者background thread調(diào)用,這要求display應(yīng)該是線程安全的种蘸,這里是通過XWSentinel保證線程安全墓赴。willdisplay和didDisplay在mainthread調(diào)用。
/**
XWAsyncLayer在后臺(tái)渲染contents的顯示任務(wù)類
*/
open class XWAsyncLayerDisplayTask: NSObject {
/**
這個(gè)block會(huì)在異步渲染開始的前調(diào)用航瞭,只在主線程調(diào)用诫硕。
*/
public var willDisplay: ((CALayer) -> Void)?
/**
這個(gè)block會(huì)調(diào)用去顯示layer的內(nèi)容
*/
public var display: ((_ context: CGContext, _ size: CGSize, _ isCancelled: (() -> Bool)?) -> Void)?
/**
這個(gè)block會(huì)在異步渲染結(jié)束后調(diào)用,只在主線程調(diào)用刊侯。
*/
public var didDisplay: ((_ layer: CALayer, _ finished: Bool) -> Void)?
}
2.4 XWAsyncLayer之源碼分析
XWAsyncLayer為了異步繪制而繼承CALayer的子類章办。通過使用CoreGraphic相關(guān)方法,在子線程中繪制內(nèi)容Context滨彻,繪制完成后藕届,回到主線程對(duì)layer.contents進(jìn)行直接顯示。 通過開辟線程進(jìn)行異步繪制疮绷,但是不能無限開辟線程
我們都知道翰舌,把阻塞主線程執(zhí)行的代碼放入另外的線程里保證APP可以及時(shí)的響應(yīng)用戶的操作。但是線程的切換也是需要額外的開銷的冬骚。也就是說椅贱,線程不能無限度的開辟下去。
那么只冻,dispatch_queue_t的實(shí)例也不能一直增加下去庇麦。有人會(huì)說可以用dispatch_get_global_queue()來獲取系統(tǒng)的隊(duì)列。沒錯(cuò)喜德,但是這個(gè)情況只適用于少量的任務(wù)分配山橄。因?yàn)椋到y(tǒng)本身也會(huì)往這個(gè)queue里添加任務(wù)的舍悯。
所以航棱,我們需要用自己的queue睡雇,但是是有限個(gè)的。參考YY這個(gè)數(shù)量指定的值是16饮醇。
異步繪制主要代碼如下:
func displayAsync(async: Bool) {
//獲取delegate對(duì)象它抱,這邊默認(rèn)是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調(diào)用繪制前观蓄,可以通過判斷isCancelled布爾值的值來停止繪制,減少性能上的消耗祠墅,以及避免出現(xiàn)線程阻塞的情況侮穿,比如TableView快速滑動(dòng)的時(shí)候,就可以通過這樣的判斷毁嗦,來避免不必要的繪制亲茅,提升滑動(dòng)的流暢性.
let isCancelled = {
return value != sentinel!.value
}
// 異步繪制
XWAsyncLayerGetDisplayQueue.async {
guard !isCancelled() else { return }
//獲取上下文和size
..............
//異步繪制
task.display?(context, size, isCancelled)
//若取消 則釋放資源,取消繪制
if isCancelled() {
//調(diào)用UIGraphicsEndImageContext函數(shù)關(guān)閉圖形上下文
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)
task.display?(context, bounds.size, {return false })
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
contents = image?.cgImage
task.didDisplay?(self, true)
}
}
display任務(wù)解析:
1. isCancelled捕獲_sentinel計(jì)數(shù)器
2. 將異步繪制內(nèi)容添加到異步隊(duì)列中
3. 繪制任務(wù)開始時(shí)先通過isCancelled判斷是否取消繪制,為false時(shí)通過獲取de legate的task開啟繪制
4. 繪制完成后關(guān)閉上下文金矛,切回到主線程芯急,將繪制的image賦值給layer的contens
注:isCancelled的block對(duì)_sentinel的value的捕獲以和當(dāng)前值比較以達(dá)到判斷是否需要取消繪制
2.4 XWLabel之源碼分析
XWLabel通過實(shí)現(xiàn)XWAsyncLayerDelegate協(xié)議返回異步繪制的XWAsyncLayerDisplayTask任務(wù),重寫layerClass返回自定義的XWAsyncLayer以實(shí)現(xiàn)異步繪制
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 返回繪制任務(wù)
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隊(duì)列中
func commitUpdate() {
//XWTransaction.transaction(with: self, selector: #selector(layoutNeedRedraw))?.commit()
self.layoutNeedRedraw()
}
@objc func layoutNeedRedraw() {
self.layer.setNeedsDisplay()
}
}
總結(jié)
最后,我們把整個(gè)異步渲染的過程來串聯(lián)起來饼酿。
1. UIView觸發(fā)layoutSubviews榕酒,或者主動(dòng)調(diào)用layer的setNeedsDisplay
2. layer調(diào)用display方法
3. 判斷是否需要異步,需要異步將繪制任務(wù)添加到隊(duì)列中
4. 繪制完成切回主線程故俐,設(shè)置layer的contents