iOS 異步渲染

前言

異步繪制吐葱,就是可以在子線程把需要繪制的圖形街望,提前在子線程處理好。將準(zhǔn)備好的圖像數(shù)據(jù)直接返給主線程使用弟跑,這樣可以降低主線程的壓力灾前。

一 UIView繪制渲染原理和流程

UIView繪制流程.png

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)繪制流程
系統(tǒng)繪制流程.png

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.異步繪制流程
異步繪制流程.png

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)系類圖如下:


類圖.png
異步繪制開始到結(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末想鹰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子药版,更是在濱河造成了極大的恐慌辑舷,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件槽片,死亡現(xiàn)場離奇詭異何缓,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)还栓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門碌廓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人剩盒,你說我怎么就攤上這事谷婆。” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵纪挎,是天一觀的道長期贫。 經(jīng)常有香客問我,道長廷区,這世上最難降的妖魔是什么唯灵? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任贾铝,我火速辦了婚禮隙轻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘垢揩。我一直安慰自己玖绿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布叁巨。 她就那樣靜靜地躺著斑匪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪锋勺。 梳的紋絲不亂的頭發(fā)上蚀瘸,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音庶橱,去河邊找鬼贮勃。 笑死,一個(gè)胖子當(dāng)著我的面吹牛苏章,可吹牛的內(nèi)容都是我干的寂嘉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼枫绅,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼泉孩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起并淋,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤寓搬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后县耽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體句喷,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年酬诀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了脏嚷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瞒御,死狀恐怖父叙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤趾唱,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布涌乳,位于F島的核電站,受9級(jí)特大地震影響甜癞,放射性物質(zhì)發(fā)生泄漏夕晓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一悠咱、第九天 我趴在偏房一處隱蔽的房頂上張望蒸辆。 院中可真熱鬧,春花似錦析既、人聲如沸躬贡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拂玻。三九已至,卻和暖如春宰译,著一層夾襖步出監(jiān)牢的瞬間檐蚜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工沿侈, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闯第,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓肋坚,卻偏偏與公主長得像乡括,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子智厌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345