You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 13:58:15