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

使用@propertyWrapper实现UserDefaults遇异常:观察者提前释放

问题分析与解决方案

嗨,这个问题其实是观察者实例没有被强引用导致提前销毁,我来一步步帮你解决:

核心原因

你在viewWillAppear里调用AppData.$layout.observe后,返回的DefaultsObservation实例没有被任何变量持有——它只是个临时对象,当viewWillAppear方法执行完毕,ARC就会立刻回收这个实例,触发deinit方法移除观察者,自然后续的layout属性变化就无法被监听了。

而你注释掉deinit里的移除代码后,虽然观察者实例被销毁了,但UserDefaults还保留着对它的KVO引用,这会导致野指针崩溃风险(当UserDefaults发送通知时,会尝试访问已经释放的对象),绝对不能这么做。

正确解决方案

只需要在ViewController中强持有观察者实例,就能避免它提前被销毁,同时保证deinit能在合适的时机执行(移除观察者,避免内存泄漏)。

步骤1:修改ViewController,添加持有属性

在你的ViewController里声明一个属性来保存观察者实例:

class YourViewController: UIViewController {
    // 用这个属性强持有观察者,避免提前销毁
    private var layoutObservation: DefaultsObservation?
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // 将observe返回的实例赋值给属性,保持强引用
        layoutObservation = AppData.$layout.observe { old, new in
            print("Layout变化:旧值\(old ?? "空"),新值\(new ?? "空")")
        }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // 可选:手动置空,提前触发deinit移除观察者(也可以等ViewController销毁时自动释放)
        layoutObservation = nil
    }
}

步骤2:优化Storage的observe方法返回类型

把observe方法的返回值从NSObject改成DefaultsObservation,让类型更明确安全:

func observe(change: @escaping (T?, T?) -> Void) -> DefaultsObservation {
    return DefaultsObservation(key: key) { old, new in
        change(old as? T, new as? T)
    }
}

为什么这样有效?

  • 现在DefaultsObservation实例被ViewController的layoutObservation属性强引用,只要ViewController存在,观察者就不会被销毁,监听就能正常工作。
  • 当ViewController被销毁时,layoutObservation会被自动释放,触发DefaultsObservation的deinit方法,正确移除UserDefaults的观察者,既不会有内存泄漏,也不会有野指针风险。

额外优化建议

如果你使用的是iOS 13+,可以考虑用Combine框架来替代KVO,代码会更简洁且更安全:

// 修改Storage的projectedValue返回一个Publisher
var projectedValue: AnyPublisher<T, Never> {
    UserDefaults.standard.publisher(for: \.self[key], options: [.old, .new])
        .map { $0 as? T ?? defaultValue }
        .eraseToAnyPublisher()
}

// 然后在ViewController里监听:
private var cancellables = Set<AnyCancellable>()

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    AppData.$layout
        .sink { [weak self] newValue in
            // 处理变化
        }
        .store(in: &cancellables)
}

不过这是可选方案,如果你想继续用KVO的话,前面的解决方案已经完全能解决你的问题啦~

内容的提问来源于stack exchange,提问作者atalayasa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 07:13:11