美文网首页
关于macOS替代品之CADisplayLink

关于macOS替代品之CADisplayLink

作者: 弹吉他的少年 | 来源:发表于2023-01-09 15:15 被阅读0次

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

    ✌️.

    相关文章

      网友评论

          本文标题:关于macOS替代品之CADisplayLink

          本文链接:https://www.haomeiwen.com/subject/tvopcdtx.html