如何在UIView子类中检测UIScreen的变化?
如何在UIView子类中检测UIScreen的变化?
我之前做过OpenGL渲染的自定义View,太懂你这种需求了——外接显示器或者屏幕镜像时,CADisplayLink的刷新率必须和当前屏幕匹配,不然轻则画面不同步,重则出现掉帧、撕裂的问题。下面给你几个靠谱的实现方案:
方案一:重写View的生命周期方法(最直接)
UIView的didMoveToWindow()方法会在这几种场景下被调用:View被添加到Window、从Window移除,或者所在的Window切换到了另一个屏幕。我们可以利用这个方法来检测屏幕变化:
class RenderView: UIView { private var currentScreen: UIScreen? private var displayLink: CADisplayLink? override func didMoveToWindow() { super.didMoveToWindow() guard let newScreen = self.window?.screen else { // View不在任何Window上,销毁DisplayLink displayLink?.invalidate() displayLink = nil currentScreen = nil return } if newScreen != currentScreen { // 屏幕发生变化,更新DisplayLink updateDisplayLink(for: newScreen) currentScreen = newScreen } } private func updateDisplayLink(for screen: UIScreen) { // 先销毁旧的DisplayLink displayLink?.invalidate() // 用新的屏幕创建新的DisplayLink displayLink = CADisplayLink(target: self, selector: #selector(renderFrame)) displayLink?.preferredFramesPerSecond = screen.maximumFramesPerSecond displayLink?.add(to: .main, forMode: .common) } @objc private func renderFrame() { // 这里执行你的OpenGL/Metal渲染逻辑 } }
这个方案的好处是不用手动管理KVO的观察者,避免了内存泄漏的风险,而且完全贴合UIKit的生命周期,代码也简洁。
方案二:使用KVO监听屏幕变化
如果你的场景更复杂,比如需要在屏幕变化的第一时间响应(比如还没触发didMoveToWindow的时候),可以监听View的screen属性或者Window的screen属性。不过要注意,KVO需要手动移除观察者,不然会有崩溃风险:
class RenderView: UIView { private var currentScreen: UIScreen? private var displayLink: CADisplayLink? override init(frame: CGRect) { super.init(frame: frame) setupKVO() } required init?(coder: NSCoder) { super.init(coder: coder) setupKVO() } private func setupKVO() { // 监听自身的screen属性 addObserver(self, forKeyPath: #keyPath(screen), options: [.old, .new], context: nil) } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard keyPath == #keyPath(screen), let newScreen = screen else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) return } if newScreen != currentScreen { updateDisplayLink(for: newScreen) currentScreen = newScreen } } deinit { // 必须移除观察者 removeObserver(self, forKeyPath: #keyPath(screen)) displayLink?.invalidate() } private func updateDisplayLink(for screen: UIScreen) { displayLink?.invalidate() displayLink = CADisplayLink(target: self, selector: #selector(renderFrame)) displayLink?.preferredFramesPerSecond = screen.maximumFramesPerSecond displayLink?.add(to: .main, forMode: .common) } @objc private func renderFrame() { // 渲染逻辑 } }
注意事项
- 一定要在屏幕变化时销毁旧的CADisplayLink再创建新的,直接复用旧的会导致刷新率不匹配。
- 所有UIKit相关的操作(比如创建DisplayLink、更新screen引用)都要在主线程执行,CADisplayLink的回调默认也是在主线程,所以不用额外切换线程。
- 如果你的View可能被频繁添加/移除到不同Window,方案一的
didMoveToWindow()会更省心,不用处理KVO的各种边界情况。
内容来源于stack exchange
相关产品推荐
相关产品推荐

