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

如何让UserDefaults发布器在多线程环境下稳定工作?

问题描述

我正在开发一个系统,需要在UserDefaults值发生变化时同步至另一个系统。尝试用UserDefaults.standard.publisher监听特定值的变化,但最初连更新都收不到。之后在多线程环境下测试UserDefaults的线程安全性,编写了示例代码却无法解释运行行为。想请教:如何让发布器始终能接收到更新值,同时保证线程安全?


示例代码
import Combine
import Foundation

let key = "value"

private extension UserDefaults {
    
    @objc dynamic var value: Int {
        get { integer(forKey: key) }
        set { set(newValue, forKey: key) }
    }
}

print("Starting")

let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
dispatchGroup.enter()

DispatchQueue.global().async {
    
    UserDefaults.standard.value = 0

    let publisher = UserDefaults.standard.publisher(for: \.value, options: .new)

    let cancellable = publisher.sink { value in
        print("Received: \(value)")
    }

    UserDefaults.standard.value = 1
    
    UserDefaults.standard.setValue(2, forKey: key)

    DispatchQueue.global().async {
        UserDefaults.standard.setValue(3, forKey: key)
        dispatchGroup.leave()
    }
    
    dispatchGroup.leave()
}

dispatchGroup.wait()

print("Finished")

输出
Starting
Received: 1
Received: 1
Received: 2
Finished

问题分析与解决方案

核心问题拆解

1. 订阅过早被销毁

代码中cancellable是闭包内的局部变量,当外层异步闭包执行完毕后,该变量会被系统释放,订阅随之取消。这就是最后设置的值3无法被接收的原因。

2. 重复接收值的原因

你混合使用了自定义动态属性value和原生setValue(_:forKey:)方法修改数据:

  • 通过value = 1赋值时,会触发KVO通知,发布器收到1
  • 调用setValue(2, forKey:)时,绕过了自定义属性的setter,导致KVO通知逻辑出现异常,额外收到一次旧值1,之后才触发新值2的通知

解决方案

1. 保留订阅生命周期

将订阅对象存储在全局或类级别的容器中,避免被过早释放:

// 全局或类属性,用于保存订阅
var cancellables = Set<AnyCancellable>()

订阅时通过store(in:)方法保存:

publisher
    .sink { value in
        print("Received: \(value)")
    }
    .store(in: &cancellables)

2. 统一数据修改方式

避免混合使用自定义属性和原生方法,统一通过扩展的动态属性修改UserDefaults值,保证KVO通知的一致性:

// 所有修改都用这个属性
UserDefaults.standard.value = 3

3. 保证线程安全

苹果官方明确UserDefaults本身是线程安全的,但并发读写可能导致更新顺序混乱。可以用串行队列封装所有UserDefaults操作,严格保证执行顺序:

private let userDefaultsQueue = DispatchQueue(label: "com.yourapp.userdefaults")

// 线程安全的写操作
func setValueSafely(_ value: Int) {
    userDefaultsQueue.sync {
        UserDefaults.standard.value = value
    }
}

// 线程安全的读操作
func getValueSafely() -> Int {
    return userDefaultsQueue.sync {
        UserDefaults.standard.value
    }
}

修改后的完整代码
import Combine
import Foundation

let key = "value"
var cancellables = Set<AnyCancellable>()

private extension UserDefaults {
    @objc dynamic var value: Int {
        get { integer(forKey: key) }
        set { set(newValue, forKey: key) }
    }
}

private let userDefaultsQueue = DispatchQueue(label: "com.example.userdefaults")

func setValueSafely(_ value: Int) {
    userDefaultsQueue.sync {
        UserDefaults.standard.value = value
    }
}

print("Starting")

let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
dispatchGroup.enter()

DispatchQueue.global().async {
    setValueSafely(0)

    let publisher = UserDefaults.standard.publisher(for: \.value, options: .new)

    publisher
        .sink { value in
            print("Received: \(value)")
        }
        .store(in: &cancellables)

    setValueSafely(1)
    setValueSafely(2)

    DispatchQueue.global().async {
        setValueSafely(3)
        dispatchGroup.leave()
    }
    
    dispatchGroup.leave()
}

dispatchGroup.wait()

print("Finished")

修改后的输出
Starting
Received: 1
Received: 2
Received: 3
Finished

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 02:31:11