关于macOS替代品之CADisplayLink


theme: cyanosis

cycling.gif

什么是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色彩转换、分屏操作之后如下所展示;

tutieshi_640x524_1s.gif

该类是在写GIF使用滤镜时刻的产物,需要的老铁们直接拿去使用吧。另外如果对动态图注入滤镜效果感兴趣的朋友也可以联系我,邮箱yangkj310@gmail.com,喜欢就给我点个星🌟吧!

✌️.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值