什么是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)
该类是在写GIF使用滤镜时刻的产物,需要的老铁们直接拿去使用吧。另外如果对动态图注入滤镜效果感兴趣的朋友也可以联系我,邮箱yangkj310@gmail.com,喜欢就给我点个星🌟吧!
- 再附上一个图像框架 ImageXDemo地址
- 再附上一个Metal滤镜库 HarbethDemo地址,目前包含
100+
种滤镜,同时也支持CoreImage混合使用。 - 再附上一个开发加速库 KJCategoriesDemo地址
- 再附上一个网络基础库 RxNetworksDemo地址
喜欢的老板们可以点个星🌟,谢谢各位老板!!!
✌️.