什么是CADisplayLink
CADisplayLink是一個能讓我們以和屏幕刷新率相同的頻率將內(nèi)容畫到屏幕上的定時器。
CADisplayLink以特定模式注冊到runloop后,每當(dāng)屏幕顯示內(nèi)容刷新結(jié)束的時候,runloop就會向CADisplayLink指定的target發(fā)送一次指定的selector消息,CADisplayLink類對應(yīng)的selector就會被調(diào)用一次。
通常情況下,iOS設(shè)備的刷新頻率事60HZ也就是每秒60次,那么每一次刷新的時間就是1/60秒大概16.7毫秒。
iOS設(shè)備的屏幕刷新頻率是固定的,CADisplayLink 在正常情況下會在每次刷新結(jié)束都被調(diào)用,精確度相當(dāng)高。但如果調(diào)用的方法比較耗時,超過了屏幕刷新周期,就會導(dǎo)致跳過若干次回調(diào)調(diào)用機(jī)會。
如果CPU過于繁忙,無法保證屏幕 60次/秒 的刷新率,就會導(dǎo)致跳過若干次調(diào)用回調(diào)方法的機(jī)會,跳過次數(shù)取決CPU的忙碌程度。
DisplayLink概覽
對外開放方法屬性,簡單模擬iOS系統(tǒng)對應(yīng)的CADisplayLink。
// See: https://developer.apple.com/documentation/quartzcore/cadisplaylink
public protocol DisplayLinkProtocol: NSObjectProtocol {
/// 每幀之間的時間,60HZ的刷新率為每秒60次,每次刷新需要1/60秒,大約16.7毫秒
var duration: CFTimeInterval { get }
/// 返回每個幀之間的時間,即每個屏幕刷新之間的時間間隔
var timestamp: CFTimeInterval { get }
/// 定義每次之間必須傳遞多少個顯示幀
var frameInterval: Int { get }
/// 是否處于暫停狀態(tài)
var isPaused: Bool { get set }
/// 使用您指定的目標(biāo)和選擇器創(chuàng)建顯示鏈接
/// 將在“target”上調(diào)用名為“sel”的方法,該方法對應(yīng)``(void)selector:(CADisplayLink *)sender``
init(target: Any, selector sel: Selector)
/// 將接收器添加到給定的運(yùn)行循環(huán)和模式中
func add(to runloop: RunLoop, forMode mode: RunLoop.Mode)
/// 從運(yùn)行循環(huán)的給定模式中移除接收器
func remove(from runloop: RunLoop, forMode mode: RunLoop.Mode)
/// 銷毀計(jì)時器,并釋放“目標(biāo)”對象
func invalidate()
}
DisplayLink方法和屬性介紹
- 初始化
然后把 CADisplayLink 對象添加到 runloop 中后,并給它提供一個 target 和 select 在屏幕刷新的時候調(diào)用
/// 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(onScreenUpdate(_:)))
//displayLink.add(to: .main, forMode: RunLoop.Mode.common)
display.add(to: .current, forMode: RunLoop.Mode.default)
display.isPaused = true
return display
}()
- 停止方法
執(zhí)行 invalidate 操作時,CADisplayLink 對象就會從 runloop 中移除,selector 調(diào)用也隨即停止
deinit {
if displayLinkInitialized {
displayLink.invalidate()
}
}
- 開啟or暫停
開啟計(jì)時器或者暫停計(jì)時器操作,
/// 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)
}
- 定義每次之間必須傳遞多少個顯示幀
用來設(shè)置間隔多少幀調(diào)用一次 selector 方法,默認(rèn)值是1,即每幀都調(diào)用一次。如果每幀都調(diào)用一次的話,對于iOS設(shè)備來說那刷新頻率就是60HZ也就是每秒60次,如果將 frameInterval 設(shè)為2那么就會兩幀調(diào)用一次,也就是變成了每秒刷新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的重復(fù)繪制,如:下載進(jìn)度條,自定義動畫設(shè)計(jì),視頻播放渲染等;
/// 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設(shè)計(jì)實(shí)現(xiàn)
由于macOS不支持CADisplayLink,于是乎制作一款替代品,代碼如下可直接搬去使用;
//
// CADisplayLink.swift
// Harbeth
//
// Created by Condy on 2023/1/6.
//
import Foundation
#if os(macOS)
import AppKit
public typealias CADisplayLink = Harbeth.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 }
/// A Boolean value that indicates whether the system suspends the display link’s notifications to the target.
var isPaused: Bool { get set }
/// 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, selector sel: 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.
func add(to runloop: RunLoop, forMode mode: 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.
func remove(from 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) {
if let _ = self.source {
return
}
self.source = createSource(with: runloop)
}
public func remove(from runloop: RunLoop, forMode mode: RunLoop.Mode) {
self.cancel()
self.source = nil
}
public var isPaused: Bool = false {
didSet {
isPaused ? suspend() : 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)
if source?.isCancelled ?? false {
source?.activate()
} else {
source?.resume()
}
}
/// Suspend the timer.
private func suspend() {
guard running(), let timer = timer else {
return
}
CVDisplayLinkStop(timer)
source?.suspend()
}
/// Cancels the timer, can be restarted aftewards.
private func cancel() {
guard running(), let timer = timer else {
return
}
CVDisplayLinkStop(timer)
if source?.isCancelled ?? false {
return
}
source?.cancel()
}
private func running() -> Bool {
guard let timer = timer else { return false }
return CVDisplayLinkIsRunning(timer)
}
private func createSource(with runloop: RunLoop) -> DispatchSourceUserDataAdd? {
guard let timer = timer else {
return nil
}
let queue: DispatchQueue = runloop == RunLoop.main ? .main : .global()
let 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 nil
}
successLink = CVDisplayLinkSetCurrentCGDisplay(timer, CGMainDisplayID())
guard successLink == kCVReturnSuccess else {
return nil
}
// 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
}
})
return source
}
}
#endif
濾鏡動態(tài)圖GIF
- 注入靈魂出竅、rbga色彩轉(zhuǎn)換、分屏操作之后如下所展示;
let filters: [C7FilterProtocol] = [
C7SoulOut(soul: 0.75),
C7ColorConvert(with: .rbga),
C7Storyboard(ranks: 2),
]
let named = " ``GIF Link`` "
imageView.play(with: named, filters: filters)
[圖片上傳失敗...(image-5b500b-1690859963573)]
該類是在寫GIF使用濾鏡時刻的產(chǎn)物,需要的老鐵們直接拿去使用吧。另外如果對動態(tài)圖注入濾鏡效果感興趣的朋友也可以聯(lián)系我,郵箱yangkj310@gmail.com,喜歡就給我點(diǎn)個星??吧!
- 再附上一個圖像框架ImageXDemo地址
- 再附上一個Metal濾鏡庫HarbethDemo地址,目前包含
100+種濾鏡。 - 再附上一個開發(fā)加速庫KJCategoriesDemo地址
- 再附上一個網(wǎng)絡(luò)基礎(chǔ)庫RxNetworksDemo地址
- 喜歡的老板們可以點(diǎn)個星??,謝謝各位老板?。?!
??.