关于macOS替代品之CADisplayLink

本文涉及的产品
实时计算 Flink 版,5000CU*H 3个月
实时数仓Hologres,5000CU*H 100GB 3个月
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
简介: 关于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/cadisplaylinkpublicprotocolDisplayLinkProtocol:NSObjectProtocol{/// 每帧之间的时间,60HZ的刷新率为每秒60次,每次刷新需要1/60秒,大约16.7毫秒varduration:CFTimeInterval{get}/// 返回每个帧之间的时间,即每个屏幕刷新之间的时间间隔vartimestamp:CFTimeInterval{get}/// 定义每次之间必须传递多少个显示帧varframeInterval:Int{get}/// 是否处于暂停状态varisPaused:Bool{getset}/// 使用您指定的目标和选择器创建显示链接/// 将在“target”上调用名为“sel”的方法,该方法对应``(void)selector:(CADisplayLink *)sender``init(target:Any,selectorsel:Selector)/// 将接收器添加到给定的运行循环和模式中funcadd(torunloop:RunLoop,forModemode:RunLoop.Mode)/// 从运行循环的给定模式中移除接收器funcremove(fromrunloop:RunLoop,forModemode:RunLoop.Mode)/// 销毁计时器,并释放“目标”对象funcinvalidate()}


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,于是乎制作一款替代品,代码如下可直接搬去使用;

////  CADisplayLink.swift//  Harbeth////  Created by Condy on 2023/1/6.//importFoundation#ifos(macOS)importAppKitpublictypealiasCADisplayLink=Harbeth.DisplayLink// See: https://developer.apple.com/documentation/quartzcore/cadisplaylinkpublicprotocolDisplayLinkProtocol:NSObjectProtocol{/// The refresh rate of 60HZ is 60 times per second, each refresh takes 1/60 of a second about 16.7 milliseconds.varduration:CFTimeInterval{get}/// Returns the time between each frame, that is, the time interval between each screen refresh.vartimestamp:CFTimeInterval{get}/// Sets how many frames between calls to the selector method, defult 1varframeInterval:Int{get}/// A Boolean value that indicates whether the system suspends the display link’s notifications to the target.varisPaused:Bool{getset}/// Creates a display link with the target and selector you specify./// It will invoke the method called `sel` on `target`, the method has the signature ``(void)selector:(CADisplayLink *)sender``./// - Parameters:///   - target: An object the system notifies to update the screen.///   - sel: The method to call on the target.init(target:Any,selectorsel:Selector)/// Adds the receiver to the given run-loop and mode./// - Parameters:///   - runloop: The run loop to associate with the display link.///   - mode: The mode in which to add the display link to the run loop.funcadd(torunloop:RunLoop,forModemode:RunLoop.Mode)/// Removes the receiver from the given mode of the runloop./// This will implicitly release it when removed from the last mode it has been registered for./// - Parameters:///   - runloop: The run loop to associate with the display link.///   - mode: The mode in which to remove the display link to the run loop.funcremove(fromrunloop:RunLoop,forModemode:RunLoop.Mode)/// Removes the object from all runloop modes and releases the `target` object.funcinvalidate()}/// Analog to the CADisplayLink in iOS.publicfinalclassDisplayLink:NSObject,DisplayLinkProtocol{// This is the value of CADisplayLink.privatestaticletduration=0.016666667privatestaticletframeInterval=1privatestaticlettimestamp=0.0// 该值随时会变,就取个开始值吧!privatelettarget:Anyprivateletselector:SelectorprivateletselParameterNumbers:Intprivatelettimer:CVDisplayLink?privatevarsource:DispatchSourceUserDataAdd?privatevartimeStampRef:CVTimeStamp=CVTimeStamp()/// Use this callback when the Selector parameter exceeds 1.publicvarcallback: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.publicvarduration:CFTimeInterval{guardlettimer=timerelse{returnDisplayLink.duration}CVDisplayLinkGetCurrentTime(timer,&timeStampRef)returnCFTimeInterval(timeStampRef.videoRefreshPeriod)/CFTimeInterval(timeStampRef.videoTimeScale)}/// Returns the time between each frame, that is, the time interval between each screen refresh.publicvartimestamp:CFTimeInterval{guardlettimer=timerelse{returnDisplayLink.timestamp}CVDisplayLinkGetCurrentTime(timer,&timeStampRef)returnCFTimeInterval(timeStampRef.videoTime)/CFTimeInterval(timeStampRef.videoTimeScale)}/// Sets how many frames between calls to the selector method, defult 1publicvarframeInterval:Int{guardlettimer=timerelse{returnDisplayLink.frameInterval}CVDisplayLinkGetCurrentTime(timer,&timeStampRef)returnInt(timeStampRef.rateScalar)}publicinit(target:Any,selectorsel:Selector){self.target=targetself.selector=selself.selParameterNumbers=DisplayLink.selectorParameterNumbers(sel)vartimerRef:CVDisplayLink?=nilCVDisplayLinkCreateWithActiveCGDisplays(&timerRef)self.timer=timerRef}publicfuncadd(torunloop:RunLoop,forModemode:RunLoop.Mode){iflet_=self.source{return}self.source=createSource(with:runloop)}publicfuncremove(fromrunloop:RunLoop,forModemode:RunLoop.Mode){self.cancel()self.source=nil}publicvarisPaused:Bool=false{didSet{isPaused?suspend():start()}}publicfuncinvalidate(){cancel()}deinit{ifrunning(){cancel()}}}extensionDisplayLink{/// Get the number of parameters contained in the Selector method.privateclassfuncselectorParameterNumbers(_sel:Selector)->Int{varnumber:Int=0forxinsel.descriptionwherex==":"{number+=1}returnnumber}/// Starts the timer.privatefuncstart(){guard!running(),lettimer=timerelse{return}CVDisplayLinkStart(timer)ifsource?.isCancelled??false{source?.activate()}else{source?.resume()}}/// Suspend the timer.privatefuncsuspend(){guardrunning(),lettimer=timerelse{return}CVDisplayLinkStop(timer)source?.suspend()}/// Cancels the timer, can be restarted aftewards.privatefunccancel(){guardrunning(),lettimer=timerelse{return}CVDisplayLinkStop(timer)ifsource?.isCancelled??false{return}source?.cancel()}privatefuncrunning()->Bool{guardlettimer=timerelse{returnfalse}returnCVDisplayLinkIsRunning(timer)}privatefunccreateSource(withrunloop:RunLoop)->DispatchSourceUserDataAdd?{guardlettimer=timerelse{returnnil}letqueue:DispatchQueue=runloop==RunLoop.main?.main:.global()letsource=DispatchSource.makeUserDataAddSource(queue:queue)varsuccessLink=CVDisplayLinkSetOutputCallback(timer,{(_,_,_,_,_,pointer)->CVReturninifletsourceUnsafeRaw=pointer{letsourceUnmanaged=Unmanaged<DispatchSourceUserDataAdd>.fromOpaque(sourceUnsafeRaw)sourceUnmanaged.takeUnretainedValue().add(data:1)}returnkCVReturnSuccess},Unmanaged.passUnretained(source).toOpaque())guardsuccessLink==kCVReturnSuccesselse{returnnil}successLink=CVDisplayLinkSetCurrentCGDisplay(timer,CGMainDisplayID())guardsuccessLink==kCVReturnSuccesselse{returnnil}// Timer setupsource.setEventHandler(handler:{[weakself]inguardlet`self`=self,lettarget=self.targetas?NSObjectProtocolelse{return}switchself.selParameterNumbers{case0whereself.selector.description.isEmpty==false:target.perform(self.selector)case1:target.perform(self.selector,with:self)default:self.callback?(self)break}})returnsource}}#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)

1.png

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


喜欢的老板们可以点个星🌟,谢谢各位老板!!!


✌️.

相关文章
|
10月前
|
Linux Android开发 iOS开发
基于.Net开发的ChatGPT客户端,兼容Windows、IOS、安卓、MacOS、Linux
基于.Net开发的ChatGPT客户端,兼容Windows、IOS、安卓、MacOS、Linux
156 0
|
iOS开发 MacOS 内存技术
解决 XtraFinder、TotalFinder 无法安装的问题(支持 macOS Monterey)
解决 XtraFinder、TotalFinder 无法安装的问题(支持 macOS Monterey)
921 0
|
2月前
|
安全 Ubuntu Linux
6 个受欢迎且好用的轻量级Linux桌面环境
Linux被认为是最安全的系统,但这并不意味着它不受恶意软件或其他安全漏洞的侵害。Linux系统的使用范围非常广泛,因此防范潜在威胁至关重要。在这里,将探索 2024 年适用于 Linux 的最佳防病毒软件。根据评级、功能以及与其他 Linux 发行版的兼容性列出了十款最佳防病毒软件,内容仅供分享,不做其它用途。
840 1
6 个受欢迎且好用的轻量级Linux桌面环境
|
Linux 编译器 开发工具
Linux基础开发工具之软件包管理器
Linux作为一款操作系统,其自然也和我们其他的操作系统一样需要安装对应得软件去满足我们的需求,因此为了更好的下载软件我们也就需要使用相应的软件包管理器。
137 0
|
存储 iOS开发 MacOS
开源 Python 发行版 Anaconda 适配苹果 M1 Mac
开源 Python 发行版 Anaconda 适配苹果 M1 Mac
392 0
开源 Python 发行版 Anaconda 适配苹果 M1 Mac
|
Ubuntu Linux 数据库
1.10 Linux桌面环境(桌面系统)大比拼[附带优缺点
早期的 Linux 系统都是不带界面的,只能通过命令来管理,比如运行程序、编辑文档、删除文件等。所以,要想熟练使用 Linux,就必须记忆很多命令。
735 0
1.10 Linux桌面环境(桌面系统)大比拼[附带优缺点
|
iOS开发 MacOS 容器
macOS开发之NSTableView的应用详解(二)
macOS开发之NSTableView的应用详解
524 0
|
iOS开发 MacOS
macOS开发之NSTableView的应用详解(三)
macOS开发之NSTableView的应用详解
508 0
|
开发者 iOS开发 MacOS
macOS开发之NSTableView的应用详解(一)
macOS开发之NSTableView的应用详解
682 0

相关课程

更多