從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://m.itdecent.cn/p/6757e964b956
        ///  創(chuàng)建一個RunLoop的觀察者
        /// allocator:該參數(shù)為對象內(nèi)存分配器,一般使用默認的分配器kCFAllocatorDefault?;蛘遪il
        /// 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://m.itdecent.cn/p/6757e964b956
        ///  創(chuàng)建一個RunLoop的觀察者
        /// allocator:該參數(shù)為對象內(nèi)存分配器,一般使用默認的分配器kCFAllocatorDefault?;蛘遪il
        /// 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中文譯本

YYAsyncLayer.swift

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容