CADisplayLink
CADisplayLink는 화면의 업데이트 주기(VSYNC 주기)로 동작하는 타이머 객체입니다.
CADisplayLink를 사용하면 디스플레이 주사율에 맞춰서 특정 함수를 호출시킬 수 있는데요.
애니메이션과 그래픽 렌더링을 정확한 타이밍에 수행할 수 있어서 더 부드럽고, 최적화할 수 있습니다.
또한, Actual Frame Rates와 Frames Per Second를 측정할 수 있습니다.
Actual Frame Rates는 기기가 출력하는 초당 프레임 수를, FPS는 실제로 표시되는 프레임 수를 나타냅니다.
마지막으로 preferredFrameRateRange를 설정하여 프레임 수를 제어할 수 있습니다.
OS가 알아서 최적화를 하지만 강제할 필요가 있는 경우 preferredFrameRateRange를 설정하면 됩니다.
다만, 완전히 제어가 가능한 것은 아니고 설정하 프레임과 근접한 프레임으로 제어된다는 점을 주의해야 합니다.
이번 포스팅에서는 CADisplayLink를 이용해 Actual Frame Rates와 FPS를 측정해 보겠습니다.
ProMotion 디스플레이에서 주사율이 변하는 모습도 살펴보겠습니다.
더 깊은 활용 방안은 더 공부해서 따로 포스팅을 해보겠습니다.
참고로 VSYNC에 대해서는 여기에서 확인할 수 있고,
FPS 측정은 순서상 거의 마지막에 배치되었습니다.
사용법
CADisplayLink를 생성하고 등록하는 방법을 알아봅시다.
let displayLink = CADisplayLink(target: self, selector: #selector(updateFPS))
displayLink.add(to: .main, forMode: .default)
// displayLink.add(to: .current, forMode: .common)
@objc func updateFPS(_ displayLink: CADisplayLink) {
...
}
CADisplayLink를 생성할 때 selector를 등록합니다.
VSYNC 주기에 맞춰서 해당 메서드가 호출됩니다.
func add(
to runloop: RunLoop,
forMode mode: RunLoop.Mode
)
add 메서드로는 어떤 RunLoop에서, 어떤 우선순위로 수행할지 결정합니다.
RunLoop.Mode에는 common, default, tracking가 있습니다. (macOS에서는 eventTracking과 modalPanel도 추가)
여기서 default는 스크롤 같은 User interation 상황에서 업데이트가 되지 않습니다.
따라서 스크롤할 때도 측정하고 싶다면 common 또는 tracking으로 설정하세요.
Actual Frame Rates
FPS를 측정하기 전에 Actual Frame Rates에 대해 먼저 알아봅시다.
Actual Frame Rates은 장치가 실제로 출력할 수 있는 초당 프레임 수를 의미합니다.
CADisplayLink의 targetTimestamp와 timestamp 속성을 이용해 구할 수 있습니다.
let actualFramesPerSecond = 1 / (displaylink.targetTimestamp - displaylink.timestamp)
targetTimestamp은 다음 프레임이 표시되는 시간을, timestamp는 마지막 프레임이 표시된 시간을 의미합니다.
1을 프레임과 프레임 사이의 시간으로 나누면 기기가 출력하는 초당 프레임 수가 되는 것입니다.
시뮬레이터 기준 60hz로 출력되는 것을 볼 수 있으며,
프로모션 실기기에서는 스크롤 시 80hz로 동작하는 것을 볼 수 있습니다.
간단한 스크롤 동작이라 그런지 120hz까지 높아지진 않더라고요.
그리고 시뮬레이터에서는 각종 설정을 해도 60hz까지만 측정되었습니다.
최적화를 위해 제한을 걸어둔 거 같은데...
시뮬레이터는 실기기와 다르게 동작한다는 점은 알고 있었는데 이런 식의 최적화가 들어갔을 줄은 몰랐네요;;
Frames Per Second
FPS는 표시되는 초당 프레임 수입니다.
앱의 성능이 나쁘면 기기가 출력하는 초당 프레임 수가 높아도 표시되는 수는 적을 수 있습니다.
앱이 버벅거리는 증상이 그 예시입니다.
if previousTimestamp == 0 {
previousTimestamp = displayLink.timestamp
return
}
frameCount += 1
let deltaTime = displayLink.timestamp - previousTimestamp
if deltaTime >= 1.0 {
let fps = Double(frameCount) / deltaTime
print("FPS: \(fps)")
frameCount = 0
previousTimestamp = displayLink.timestamp
}
위 코드는 FPS를 측정하는 코드로 Actual Frame Rates과 비슷하지만 다릅니다.
FPS는 표시되는 프레임 수를 카운팅해야 하기 때문에 임의의 변수를 사용하여 카운팅 합니다.
프레임 사이가 1초가 되었을 때 카운팅된 프레임 수를 deltaTime으로 나눠서 FPS를 구할 수 있습니다.
출력된 FPS입니다.
0 ~ 20까지는 Xcode로 앱을 실행시킬 때 엄청 버벅거리잖아요? 그 순간이고
60은 켜두기만 한 상태입니다.
마지막으로 60 이상은 스크롤 상태입니다. 이때는 프로모션이 적용되어 최대 120hz까지 올라갑니다.
preferredFrameRateRange
preferredFrameRateRange는 프레임 범위를 지정하는 속성입니다.
displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 80, maximum: 120, preferred: 120)
// displayLink.preferredFrameRateRange = CAFrameRateRange(minimum:8, maximum:15, preferred:0)
minimum은 최소 프레임 수를, maximum은 최대 프레임 수를, preferred는 초당 호출되는 메서드 횟수를 의미합니다.
참고로 minimum, maximum으로 설정한 프레임 범위는 공식문서인 여기에 나와 있습니다.
(80 ~ 120은 High-impact animations 범위이고, 8 ~ 15는 Small, low speed animations 범위입니다.)
주의점은 preferredFrameRateRange를 설정한 값과 실제 초당 프레임 수가 다를 수 있습니다.
기기의 최대 주사율보다 높게 설정한다면 기기의 최대 주사율로 설정되고,
나눈 값이 정수가 되지 않는다면 지원하는 프레임 수 중 가장 가까운 값으로 반올림됩니다.
지원되는 초당 프레임 수는 아래와 같습니다.
아이폰 프로모션 디스플레이
- 120Hz (8ms)
- 80Hz (12ms)
- 60Hz (16ms)
- 48Hz (20ms)
- 40Hz (25ms)
- 30Hz (33ms)
- 24Hz (41ms)
- 20Hz (50ms)
- 16Hz (62ms)
- 15Hz (66ms)
- 12Hz (83ms)
- 10Hz (100ms)
아이패드 프로모션 디스플레이
- 120Hz (8ms)
- 60Hz (16ms)
- 40Hz (25ms)
- 30Hz (33ms)
- 24Hz (41ms)
High-impact 범위로 설정하면 120Hz로 동작하고,
Small, low speed 범위로 설정하면 15Hz로 동작하는 모습을 볼 수 있습니다.
감사합니다.
참고
https://developer.apple.com/documentation/quartzcore/cadisplaylink
https://developer.apple.com/documentation/foundation/runloop/mode
https://ios-dev-skyline-23.tistory.com/entry/CADisplayLink?category=1044657
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.