关于macOS替代品之CADisplayLink

什么是CADisplayLink

CADisplayLink是一个能让咱们以和屏幕改写率相同的频率将内容画到屏幕上的定时器。

  • CADisplayLink以特定形式注册到runloop后,每当屏幕显示内容改写完毕的时分,runloop就会向CADisplayLink指定的target发送一次指定的selector音讯,CADisplayLink类对应的selector就会被调用一次。

  • 通常情况下,iOS设备的改写频率事60HZ也便是每秒60次,那么每一次改写的时间便是1/60秒大约16.7毫秒。

  • iOS设备的屏幕改写频率是固定的,CADisplayLink 在正常情况下会在每次改写完毕都被调用,精确度适当高。但假如调用的办法比较耗时,超过了屏幕改写周期,就会导致越过若干次回调调用时机

  • 假如CPU过于繁忙,无法确保屏幕 60次/秒 的改写率,就会导致越过若干次调用回调办法的时机,越过次数取决CPU的忙碌程度

DisplayLink概览

对外开放办法特点,简略模拟iOS体系对应的CADisplayLink

// See: https://developer.apple.com/documentation/quartzcore/cadisplaylink
public protocol DisplayLinkProtocol: NSObjectProtocol {
    /// 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 { get }
    /// Returns the time between each frame, that is, the time interval between each screen refresh.
    var timestamp: CFTimeInterval { get }
    /// Sets how many frames between calls to the selector method, defult 1
    var frameInterval: Int { get }
    /// When true the object is prevented from firing. Initial state is false.
    var isPaused: Bool { get set }
    /// Create a new display link object for the main display. It will invoke the method called `sel` on `target`,
    /// the method has the signature ``(void)selector:(CADisplayLink *)sender``.
    init(target: Any, selector sel: Selector)
    /// Adds the receiver to the given run-loop and mode.
    func add(to runloop: RunLoop, forMode mode: RunLoop.Mode)
    /// Removes the object from all runloop modes and releases the `target` object.
    func invalidate()
}

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
}

DisplayLink运用

因为跟屏幕改写同步,非常适合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,于是乎制作一款替代品,代码如下可直接搬去运用;

import Foundation
#if os(macOS)
import AppKit
typealias CADisplayLink = Snowflake.DisplayLink
// See: https://developer.apple.com/documentation/quartzcore/cadisplaylink
public protocol DisplayLinkProtocol: NSObjectProtocol {
    /// 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 { get }
    /// Returns the time between each frame, that is, the time interval between each screen refresh.
    var timestamp: CFTimeInterval { get }
    /// Sets how many frames between calls to the selector method, defult 1
    var frameInterval: Int { get }
    /// When true the object is prevented from firing. Initial state is false.
    var isPaused: Bool { get set }
    /// Create a new display link object for the main display. It will invoke the method called `sel` on `target`,
    /// the method has the signature ``(void)selector:(CADisplayLink *)sender``.
    init(target: Any, selector sel: Selector)
    /// Adds the receiver to the given run-loop and mode.
    func add(to runloop: RunLoop, forMode mode: RunLoop.Mode)
    /// Removes the object from all runloop modes and releases the `target` object.
    func invalidate()
}
/// Analog to the CADisplayLink in iOS.
public final class DisplayLink: NSObject, DisplayLinkProtocol {
    // 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.
    public 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.
    public 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.
    public 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
    public var frameInterval: Int {
        guard let timer = timer else { return DisplayLink.frameInterval }
        CVDisplayLinkGetCurrentTime(timer, &timeStampRef)
        return Int(timeStampRef.rateScalar)
    }
    public 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
    }
    public 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? NSObjectProtocol 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
            }
        })
    }
    public var isPaused: Bool = false {
        didSet {
            isPaused ? cancel() : start()
        }
    }
    public 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

滤镜动态图GIF

  • 注入灵魂出窍、rbga色彩转化、分屏操作之后如下所展示;
let filters: [C7FilterProtocol] = [
  C7SoulOut(soul: 0.75),
  C7ColorConvert(with: .rbga),
  C7Storyboard(ranks: 2),
]
let URL = URL(string: "https://raw.githubusercontent.com/yangKJ/KJBannerViewDemo/master/KJBannerViewDemo/Resources/IMG_0139.GIF")!
imageView.play(withGIFURL: URL, filters: filters)

关于macOS替代品之CADisplayLink

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

  • 再附上一个Metal滤镜库HarbethDemo地址,目前包含100+种滤镜,同时也支持CoreImage混合运用。
  • 再附上一个开发加速库KJCategoriesDemo地址
  • 再附上一个网络基础库RxNetworksDemo地址
  • 喜欢的老板们能够点个星,谢谢各位老板!!!

✌️.