NSUserDefaults 是 iOS、watchOS、tvOS 和 macOS 中的键值数据存储,用于保存应用程序的设置和配置。数据存储在应用程序沙盒中的 plist 文件中,并通过 NSUbiquitousKeyValueStore 自动与 iCloud 同步。根据官方文档 Apple Developer, 2025,NSUserDefaults 支持存储原始类型:String、Int、Bool、Float、Double、Data、Date、Array 和 Dictionary。从 Swift 3 开始,该类已重命名为 UserDefaults,但其 Objective-C 名称 NSUserDefaults 在代码库和 Apple 文档中仍广泛使用。
要点
NSUserDefaults(Swift 中的 UserDefaults)是 Apple 内置的以 plist 格式存储键值对的机制。它适用于所有 Apple 平台:iOS、iPadOS、watchOS、tvOS 和 macOS。主要用途是保存用户偏好、界面状态、首次启动标志、所选选项以及其他在应用程序重启后仍然存在的简单数据。
每个 iOS 应用程序都有一个隔离的沙盒,NSUserDefaults 保存在此沙盒内的 Library/Preferences 目录中,文件名为 Bundle Identifier。plist 文件包含键值对,其中键是字符串,值是支持的类型之一。文件大小没有限制,但 Apple 建议只在 UserDefaults 中存储设置,而不是大量数据。
从 iOS 8 开始,NSUserDefaults 支持 App Groups — 同一开发者应用程序及其扩展(小部件、watchOS 配套应用程序)之间的共享存储。为此使用带有 App Group 标识符的初始化器 init?(suiteName:)。例如,这允许 Today 屏幕上的小部件读取主应用程序的设置,而无需重复保存逻辑。
物理上,NSUserDefaults 存储在二进制 plist 文件中,路径为:{Sandbox}/Library/Preferences/com.example.myapp.plist。该文件使用二进制 plist 格式(NSPropertyListBinaryFormat_v1_0)以实现紧凑性和读取速度。在 macOS 上,文件可以是 XML 格式以实现兼容性。与 Android 上的 SharedPreferences 不同,UserDefaults 的 plist 文件可以通过 Dictionary 和 Array 包含嵌套结构。
NSUserDefaults 文件默认不加密。数据以明文形式存储,可以通过物理访问设备或通过备份读取。对于存储敏感数据(密码、令牌、加密密钥),Apple 强烈建议使用 Keychain,它在操作系统级别自动加密数据。
NSUserDefaults 基于内存缓存和定期磁盘同步的原理工作。首次访问标准实例 UserDefaults.standard 时,系统将 plist 文件以 Dictionary 形式加载到 RAM 中。所有后续读取都从内存中进行。写入也首先在内存中进行,磁盘同步在后台定期进行。
写入操作使用 set(_:forKey:) 方法,该方法接受可选的 Any? 类型值。值可以为 nil — 用于删除键。以前使用 synchronize() 方法立即写入磁盘,但从 iOS 7 和 OS X 10.9 开始不再需要 — 系统会定期自动同步数据。Apple 在其文档中已正式宣布 synchronize() 是多余的。
NSUserDefaults 使用注册(域)系统来组织值搜索。当应用程序按键请求值时,UserDefaults 按特定顺序依次检查域:首先 NSArgumentDomain(命令行参数),然后是应用程序域(Application),然后是 NSGlobalDomain(系统设置),然后是特定语言域,最后是 NSRegistrationDomain(通过 register(defaults:) 注册的默认值)。
import Foundation
// 标准 UserDefaults 实例
let defaults = UserDefaults.standard
// 写入值
defaults.set("安娜·彼得罗娃", forKey: "username")
defaults.set(28, forKey: "age")
defaults.set(true, forKey: "isLoggedIn")
// 注册默认值
defaults.register(defaults: [
"theme": "system",
"fontSize": 14
])
// 读取并返回默认值
let theme = defaults.string(forKey: "theme") ?? "system"
let fontSize = defaults.integer(forKey: "fontSize")
NSRegistrationDomain 域是一个软件域,仅存在于 RAM 中,不会保存到磁盘。它用于设置默认值,这些值在应用程序将自己的值写入应用程序域之前一直有效。这允许创建一个统一的默认设置配置点,可以在开发阶段集中更改。
NSUserDefaults 提供了一组类型化方法来读写数据:string(forKey:)、integer(forKey:)、bool(forKey:)、float(forKey:)、double(forKey:)、data(forKey:)、array(forKey:)、dictionary(forKey:) 和 object(forKey:)。每个读取方法都有相应的写入方法 set(_:forKey:),自动确定存储值的类型。Swift 版本的 UserDefaults 使用严格类型化,但 Objective-C 版本接受并返回 id。
| 读取方法 (Swift) | 数据类型 | 默认值 |
|---|---|---|
| string(forKey:) | String? | nil |
| integer(forKey:) | Int | 0 |
| bool(forKey:) | Bool | false |
| float(forKey:) | Float | 0.0 |
| double(forKey:) | Double | 0.0 |
| data(forKey:) | Data? | nil |
NSUserDefaults 中的 synchronize() 方法强制将所有更改从内存写入磁盘。在 iOS 早期版本中,每次写入后都需要调用此方法以保证数据保存。从 iOS 7 开始,系统在后台自动同步 UserDefaults,Apple 已在其文档中正式宣布 synchronize() 是多余的。调用此方法不会导致错误,但不会提供任何额外的保存保证。
为了监视更改,NSUserDefaults 提供了 UserDefaults.didChangeNotification 通知和 addObserver(_:forKeyPath:options:context:) KVO 观察方法。在 SwiftUI 中,可以使用 @AppStorage 属性包装器,它自动将 UserDefaults 中的值与 UI 更新同步。@AppStorage 支持与 UserDefaults 相同的类型,是在 SwiftUI 应用程序中处理设置的首选方式。
// 通过 KVO 观察变化
class SettingsViewModel: NSObject {
override func observeValue(
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?
) {
guard let keyPath else { return }
print("键已更改: \(keyPath)")
}
}
// SwiftUI - AppStorage
struct SettingsView: View {
@AppStorage("theme") private var theme: String = "system"
var body: some View {
Picker("主题", selection: $theme) {
Text("系统").tag("system")
Text("浅色").tag("light")
Text("深色").tag("dark")
}
}
}
对于 App Groups(应用程序和扩展之间的共享存储),使用带有 App Group 标识符的初始化器 UserDefaults(suiteName:)。例如,“group.com.example.myapp”。写入此实例的数据可从主应用程序、小部件、watchOS 配套应用程序和属于同一 App Group 的其他扩展中访问。每个套件实例都存储在单独的 plist 文件中。
尽管方便简单,但 NSUserDefaults 并不是 iOS 上所有数据类型的通用存储。根据数据量、关键性和安全要求,Apple 提供了多种替代方案,每种方案都针对特定使用场景进行了优化。
| 解决方案 | 何时使用 | 限制 |
|---|---|---|
| NSUserDefaults | 界面设置和配置 | 不适合大数据和秘密 |
| Keychain | 密码、令牌、加密密钥 | 使用更复杂,速度更慢 |
| CoreData | 结构化关系数据 | 对于 10-20 个设置来说过于庞大 |
| FileManager | 文档、图片、二进制数据 | 需要手动文件管理 |
| CloudKit | 设备间云同步 | 需要 iCloud 帐户和网络连接 |
Keychain 是 Apple 用于机密数据的受保护存储。与 NSUserDefaults 不同,Keychain 中的所有数据都在操作系统级别使用兼容设备上的 Secure Enclave 硬件加密进行加密。Keychain 随设备自动锁定和解锁,并支持通过 Keychain Access Groups 在同一开发者应用程序之间进行访问共享。
Keychain 的主要缺点是 API 的复杂性。要简单地存储一个字符串,需要创建 SecItemAdd 请求,指定属性:类别(kSecClassGenericPassword)、服务(kSecAttrService)、帐户(kSecAttrAccount)和数据本身(kSecValueData)。为了简化 Keychain 的使用,存在第三方包装器,如 KeychainAccess 和 SwiftKeychainWrapper,它们提供了类似于 UserDefaults 的便捷键值接口。
让我们看一个实际示例:使用 NSUserDefaults 在 iOS 应用程序中保存和恢复引导状态(欢迎屏幕)。首次启动时,用户看到引导屏幕,完成后标志保存在 UserDefaults 中。后续启动时跳过引导。对于 SwiftUI 使用 @AppStorage,对于 UIKit — 直接访问 UserDefaults.standard。
让我们创建一个 OnboardingManager 管理器,它封装了与 UserDefaults 的交互以存储引导状态。管理器提供 isOnboardingCompleted 属性用于检查状态,以及 markOnboardingCompleted 方法用于设置标志。存储键已提取到常量中以防止拼写错误。对于单元测试,管理器使用 UserDefaultsProtocol 协议,允许用 MockUserDefaults 替换实际存储。
class OnboardingManager {
private let defaults: UserDefaults
private let hasSeenKey = "has_seen_onboarding"
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
}
var isOnboardingCompleted: Bool {
defaults.bool(forKey: hasSeenKey)
}
func markOnboardingCompleted() {
defaults.set(true, forKey: hasSeenKey)
}
func resetOnboarding() {
defaults.removeObject(forKey: hasSeenKey)
}
}
// 在应用中使用
let onboardingManager = OnboardingManager()
if !onboardingManager.isOnboardingCompleted {
showOnboarding()
} else {
showMainScreen()
}
对于更复杂的设置,如结构化的 Profile 对象,建议使用 Codable 协议和 JSONEncoder/JSONDecoder。对象通过 JSONEncoder 序列化为 Data,通过 set(_:forKey:) 保存,读取时通过 JSONDecoder 从 Data 反序列化回对象。这种方法允许在 UserDefaults 中存储复杂结构而不损失类型安全。
struct UserProfile: Codable {
let name: String
let age: Int
let preferences: [String: String]
}
extension UserDefaults {
func save<T: Codable>(_ value: T, forKey key: String) {
if let data = try? JSONEncoder().encode(value) {
set(data, forKey: key)
}
}
func load<T: Codable>(_ type: T.Type, forKey key: String) -> T? {
guard let data = data(forKey: key) else { return nil }
return try? JSONDecoder().decode(type, from: data)
}
}
// 使用
let profile = UserProfile(name: "安娜", age: 28, preferences: ["theme": "dark"])
UserDefaults.standard.save(profile, forKey: "user_profile")
let loaded = UserDefaults.standard.load(UserProfile.self, forKey: "user_profile")
重要的是要记住,NSUserDefaults 并非设计用于存储大量数据。Apple 建议将存储数据的大小限制在几十 KB 以内。对于存储大型对象(图像、文档、序列化模型),应使用带有 Documents 目录的 FileManager 或 CoreData。此外,UserDefaults 不支持数据模式版本控制 — 当 Codable 模型结构更改时,旧数据可能无法反序列化,这需要在应用程序代码中处理。
常见问题
两者都是键值存储,但 NSUserDefaults 支持更多类型(Data、Date、Array、Dictionary)并自动与 iCloud 同步。SharedPreferences 将数据存储在 XML 中,NSUserDefaults 存储在 plist 格式中。NSUserDefaults 具有带级联搜索的域系统,SharedPreferences 使用带有文件名的简单平面结构。
不安全。NSUserDefaults 以明文形式存储数据而不加密。对于密码、令牌和加密密钥,请使用 Keychain,它在 Secure Enclave 级别加密数据。Keychain 还支持访问属性,如在读取秘密之前进行生物特征认证(Face ID / Touch ID)。
要在同一用户的设备之间同步,请使用 NSUbiquitousKeyValueStore — iCloud 云键值存储。在一个设备上写入此服务的数据会自动出现在同一 iCloud 帐户的所有其他设备上。最大容量 — 每个应用程序 1 MB,1024 个键。
要删除所有数据,请使用应用程序的 Bundle Identifier 调用 removePersistentDomain(forName:) 方法。要删除单个值,请使用 removeObject(forKey:)。要完全重置应用程序设置:UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)。所有删除立即应用于内存缓存。
Apple 没有对 NSUserDefaults 的大小设置严格限制,但建议所有存储数据的总量不要超过 100 KB。对于大量数据,请使用 CoreData 或 FileManager。当存储超过 1 MB 的数据时,启动应用程序时的读取性能可能会明显下降。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。