theme: cyanosis

什麼是CADisplayLink
CADisplayLink是一個能讓我們以和螢幕重新整理率相同的頻率将内容畫到螢幕上的定時器。
- CADisplayLink以特定模式注冊到runloop後,每當螢幕顯示内容重新整理結束的時候,runloop就會向CADisplayLink指定的target發送一次指定的selector消息,CADisplayLink類對應的selector就會被調用一次。
- 通常情況下,iOS裝置的重新整理頻率事60HZ也就是每秒60次,那麼每一次重新整理的時間就是1/60秒大概16.7毫秒。
- iOS裝置的螢幕重新整理頻率是固定的,CADisplayLink 在正常情況下會在每次重新整理結束都被調用,精确度相當高。但如果調用的方法比較耗時,超過了螢幕重新整理周期,就會導緻跳過若幹次回調調用機會
- 如果CPU過于繁忙,無法保證螢幕 60次/秒 的重新整理率,就會導緻跳過若幹次調用回調方法的機會,跳過次數取決CPU的忙碌程度
DisplayLink方法和屬性介紹
- 初始化
然後把 CADisplayLink 對象添加到 runloop 中後,并給它提供一個 target 和 select 在螢幕重新整理的時候調用
/// Responsible for starting and stopping the animation.
private lazy var displayLink: CADisplayLink = {
self.displayLinkInitialized = true
let target = DisplayLinkProxy(target: self)
let display = CADisplayLink(target: target, selector: #selector(DisplayLinkProxy.onScreenUpdate(_:)))
//displayLink.add(to: .main, forMode: RunLoop.Mode.common)
display.add(to: .current, forMode: RunLoop.Mode.default)
display.isPaused = true
return display
}()
- 停止方法
執行 invalidate 操作時,CADisplayLink 對象就會從 runloop 中移除,selector 調用也随即停止
deinit {
if displayLinkInitialized {
displayLink.invalidate()
}
}
- 開啟or暫停
開啟計時器或者暫停計時器操作,
/// Start animating.
func startAnimating() {
if frameStore?.isAnimatable ?? false {
displayLink.isPaused = false
}
}
/// Stop animating.
func stopAnimating() {
displayLink.isPaused = true
}
- 每幀之間的時間
60HZ的重新整理率為每秒60次,每次重新整理需要1/60秒,大約16.7毫秒。
/// The refresh rate of 60HZ is 60 times per second, each refresh takes 1/60 of a second about 16.7 milliseconds.
var duration: CFTimeInterval {
guard let timer = timer else { return DisplayLink.duration }
CVDisplayLinkGetCurrentTime(timer, &timeStampRef)
return CFTimeInterval(timeStampRef.videoRefreshPeriod) / CFTimeInterval(timeStampRef.videoTimeScale)
}
- 上一次螢幕重新整理的時間戳
傳回每個幀之間的時間,即每個螢幕重新整理之間的時間間隔。
/// Returns the time between each frame, that is, the time interval between each screen refresh.
var timestamp: CFTimeInterval {
guard let timer = timer else { return DisplayLink.timestamp }
CVDisplayLinkGetCurrentTime(timer, &timeStampRef)
return CFTimeInterval(timeStampRef.videoTime) / CFTimeInterval(timeStampRef.videoTimeScale)
}
- 定義每次之間必須傳遞多少個顯示幀
用來設定間隔多少幀調用一次 selector 方法,預設值是1,即每幀都調用一次。如果每幀都調用一次的話,對于iOS裝置來說那重新整理頻率就是60HZ也就是每秒60次,如果将 frameInterval 設為2那麼就會兩幀調用一次,也就是變成了每秒重新整理30次。
/// Sets how many frames between calls to the selector method, defult 1
var frameInterval: Int {
guard let timer = timer else { return DisplayLink.frameInterval }
CVDisplayLinkGetCurrentTime(timer, &timeStampRef)
return timeStampRef.rateScalar
}
CADisplayLink 的使用
由于跟螢幕重新整理同步,非常适合UI的重複繪制,如:下載下傳進度條,自定義動畫設計,視訊播放渲染等;
/// A proxy class to avoid a retain cycle with the display link.
final class DisplayLinkProxy: NSObject {
weak var target: Animator?
init(target: Animator) {
self.target = target
}
/// Lets the target update the frame if needed.
@objc func onScreenUpdate(_ sender: CADisplayLink) {
guard let animator = target, let store = animator.frameStore else {
return
}
if store.isFinished {
animator.stopAnimating()
animator.animationBlock?(store.loopDuration)
return
}
store.shouldChangeFrame(with: sender.duration) {
if $0 { animator.delegate.updateImageIfNeeded() }
}
}
}
DisplayLink設計實作
由于macOS不支援CADisplayLink,于是乎制作一款替代品,代碼如下可直接搬去使用;
#if os(macOS)
import AppKit
typealias CADisplayLink = Snowflake.DisplayLink
/// Analog to the CADisplayLink in iOS.
class DisplayLink: NSObject {
// This is the value of CADisplayLink.
private static let duration = 0.016666667
private static let frameInterval = 1
private static let timestamp = 0.0 // 該值随時會變,就取個開始值吧!
private let target: Any
private let selector: Selector
private let selParameterNumbers: Int
private let timer: CVDisplayLink?
private var source: DispatchSourceUserDataAdd?
private var timeStampRef: CVTimeStamp = CVTimeStamp()
/// Use this callback when the Selector parameter exceeds 1.
var callback: Optional<(_ displayLink: DisplayLink) -> ()> = nil
/// The refresh rate of 60HZ is 60 times per second, each refresh takes 1/60 of a second about 16.7 milliseconds.
var duration: CFTimeInterval {
guard let timer = timer else { return DisplayLink.duration }
CVDisplayLinkGetCurrentTime(timer, &timeStampRef)
return CFTimeInterval(timeStampRef.videoRefreshPeriod) / CFTimeInterval(timeStampRef.videoTimeScale)
}
/// Returns the time between each frame, that is, the time interval between each screen refresh.
var timestamp: CFTimeInterval {
guard let timer = timer else { return DisplayLink.timestamp }
CVDisplayLinkGetCurrentTime(timer, &timeStampRef)
return CFTimeInterval(timeStampRef.videoTime) / CFTimeInterval(timeStampRef.videoTimeScale)
}
/// Sets how many frames between calls to the selector method, defult 1
var frameInterval: Int {
guard let timer = timer else { return DisplayLink.frameInterval }
CVDisplayLinkGetCurrentTime(timer, &timeStampRef)
return Int(timeStampRef.rateScalar)
}
init(target: Any, selector sel: Selector) {
self.target = target
self.selector = sel
self.selParameterNumbers = DisplayLink.selectorParameterNumbers(sel)
var timerRef: CVDisplayLink? = nil
CVDisplayLinkCreateWithActiveCGDisplays(&timerRef)
self.timer = timerRef
}
func add(to runloop: RunLoop, forMode mode: RunLoop.Mode) {
guard let timer = timer else { return }
let queue: DispatchQueue = runloop == RunLoop.main ? .main : .global()
self.source = DispatchSource.makeUserDataAddSource(queue: queue)
var successLink = CVDisplayLinkSetOutputCallback(timer, { (_, _, _, _, _, pointer) -> CVReturn in
if let sourceUnsafeRaw = pointer {
let sourceUnmanaged = Unmanaged<DispatchSourceUserDataAdd>.fromOpaque(sourceUnsafeRaw)
sourceUnmanaged.takeUnretainedValue().add(data: 1)
}
return kCVReturnSuccess
}, Unmanaged.passUnretained(source!).toOpaque())
guard successLink == kCVReturnSuccess else {
return
}
successLink = CVDisplayLinkSetCurrentCGDisplay(timer, CGMainDisplayID())
guard successLink == kCVReturnSuccess else {
return
}
// Timer setup
source!.setEventHandler(handler: { [weak self] in
guard let `self` = self, let target = self.target as? NSObject else {
return
}
switch self.selParameterNumbers {
case 0 where self.selector.description.isEmpty == false:
target.perform(self.selector)
case 1:
target.perform(self.selector, with: self)
default:
self.callback?(self)
break
}
})
}
var isPaused: Bool = true {
didSet {
isPaused ? cancel() : start()
}
}
func invalidate() {
cancel()
}
deinit {
if running() {
cancel()
}
}
}
extension DisplayLink {
/// Get the number of parameters contained in the Selector method.
private class func selectorParameterNumbers(_ sel: Selector) -> Int {
var number: Int = 0
for x in sel.description where x == ":" {
number += 1
}
return number
}
/// Starts the timer.
private func start() {
guard !running(), let timer = timer else { return }
CVDisplayLinkStart(timer)
source?.resume()
}
/// Cancels the timer, can be restarted aftewards.
private func cancel() {
guard running(), let timer = timer else { return }
CVDisplayLinkStop(timer)
source?.cancel()
}
private func running() -> Bool {
guard let timer = timer else { return false }
return CVDisplayLinkIsRunning(timer)
}
}
#endif
最後
- 注入靈魂出竅、rbga色彩轉換、分屏操作之後如下所展示;
該類是在寫GIF使用濾鏡時刻的産物,需要的老鐵們直接拿去使用吧。另外如果對動态圖注入濾鏡效果感興趣的朋友也可以聯系我,郵箱[email protected],喜歡就給我點個星🌟吧!
- 再附上一個Metal濾鏡庫HarbethDemo位址,目前包含
種濾鏡,同時也支援CoreImage混合使用。100+
- 再附上一個開發加速庫KJCategoriesDemo位址
- 再附上一個網絡基礎庫RxNetworksDemo位址
- 喜歡的老闆們可以點個星🌟,謝謝各位老闆!!!
✌️.