從VVeboTableViewDemo到YYAsyncLayer(二)

YYAsyncLayer源碼分析

本節(jié)關鍵字

  • 異步繪制
  • RunLoop
YYAsyncLayer目錄結構

這是YYAsyncLayer的結構

  • YYAsyncLayer:異步繪制的CALayer子類贷洲,這個類做的核心和VVeboTableViewDemoVVeboLabel 的核心是一模一樣的缸榄。你可以到回去看看從VVeboTableViewDemo到YYAsyncLayer(一)

  • YYSentinel:線程安全的計數(shù)器伯复。

  • YYTransaction:注冊RunLoop在系統(tǒng)空閑時調(diào)用樱报。(如果你不了解或者沒有聽說過RunLoop阵面,不用擔心,下面我同樣會推薦相關的文章誊涯,讓你了解和實踐RunLoop)

  • YYAsyncLayerDisplayTask: 用于回調(diào)畫布

  • YYAsyncLayerDelegate: 給接收YYAsyncLayer的UIView開的接口

本文依然是用Swift版YYAsyncLayer進行分析

YYAsyncLayer

屏幕快照 2017-04-14 下午5.10.56.png

上圖中高亮的方法為整個類的核心方法

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()中打個斷點困鸥,你的堆棧信息大概就是這樣的)

屏幕快照 2017-04-17 下午2.28.54.png

圖中有個CATransaction 的東西匈织,似乎和YYTransaction很相似盆均。其實他們不僅命名很相似,就是內(nèi)部結構也很相似肩碟。
再看YYTransaction

屏幕快照 2017-04-17 下午2.35.41.png

其中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)為beforeWaitingexit時統(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中文譯本

YYAsyncLayer.swift

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末气笙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子怯晕,更是在濱河造成了極大的恐慌潜圃,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舟茶,死亡現(xiàn)場離奇詭異谭期,居然都是意外死亡,警方通過查閱死者的電腦和手機吧凉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門隧出,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人阀捅,你說我怎么就攤上這事胀瞪。” “怎么了饲鄙?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵凄诞,是天一觀的道長。 經(jīng)常有香客問我忍级,道長帆谍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任轴咱,我火速辦了婚禮汛蝙,結果婚禮上,老公的妹妹穿的比我還像新娘朴肺。我一直安慰自己窖剑,他們只是感情好,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布戈稿。 她就那樣靜靜地躺著苛吱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪器瘪。 梳的紋絲不亂的頭發(fā)上翠储,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天,我揣著相機與錄音橡疼,去河邊找鬼援所。 笑死,一個胖子當著我的面吹牛欣除,可吹牛的內(nèi)容都是我干的住拭。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼历帚,長吁一口氣:“原來是場噩夢啊……” “哼滔岳!你這毒婦竟也來了?” 一聲冷哼從身側響起挽牢,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤谱煤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后禽拔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刘离,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年睹栖,在試婚紗的時候發(fā)現(xiàn)自己被綠了硫惕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡野来,死狀恐怖恼除,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情曼氛,我是刑警寧澤豁辉,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站搪锣,受9級特大地震影響秋忙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜构舟,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一灰追、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧狗超,春花似錦弹澎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至渗稍,卻和暖如春佩迟,著一層夾襖步出監(jiān)牢的瞬間团滥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工报强, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留灸姊,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓秉溉,卻偏偏與公主長得像力惯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子召嘶,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容