- 跨平台
- 移动开发
- 开发工具
【免费下载链接】WinObjC
Objective-C for Windows
本篇技术指南以 docs/Foundation/NSUserDefaults.md 开发设计规格说明书为主体,结合 WinObjC(Objective-C for Windows)仓库中 Frameworks/Foundation/NSUserDefaults.mm 与 Frameworks/CoreFoundation/Preferences.subproj/CFPreferences.c 的源码实现,深入讲解 NSUserDefaults 在 Windows 上的持久化机制:它如何作为 Foundation 对 CFPreferences 的接口封装、偏好数据以 plist 形式存放在包 AppData 目录、异步同步队列的工作方式、读写路径与线程安全边界,以及当前实现的支持范围与已知限制。读完本文,你将能理解 WinObjC 中用户偏好存储的完整调用链,并掌握在迁移 iOS 代码到 Windows 平台时正确使用 NSUserDefaults 的注意事项。
一、功能概述:NSUserDefaults 在 WinObjC 中的定位
原设计规格文档的第一句话即给出了该类的核心定义:
NSUserDefaults is a Foundation interface to CFPreferences that stores a preference plist in the package appdata directory.
翻译过来即:NSUserDefaults 是 Foundation 层对 CoreFoundation 层 CFPreferences 的接口封装,其偏好数据以 plist(属性列表)文件形式存储于应用的 AppData(应用数据)目录中。
这句话包含了三个关键信息:
- 分层定位:NSUserDefaults 属于 Foundation 框架,本身不直接操作磁盘,而是把读写请求转发给 CoreFoundation 的 CFPreferences API;
- 存储形态:最终落盘的是 XML 格式的 property list 文件(
kCFPropertyListXMLFormat_v1_0); - 存储位置:位于 Windows 系统为应用分配的 AppData 目录内(对应
CSIDL_APPDATA)。
从公开 API 角度看,include/Foundation/NSUserDefaults.h 声明了standardUserDefaults、registerDefaults:、objectForKey:/setObject:forKey:、全部类型化读写方法(stringForKey:、arrayForKey:、dictionaryForKey:、boolForKey:、integerForKey:、floatForKey:、doubleForKey:、URLForKey:等)、synchronize、dictionaryRepresentation以及 domain 相关操作,并导出NSGlobalDomain、NSArgumentDomain、NSRegistrationDomain三个域名常量与NSUserDefaultsDidChangeNotification通知名。这些常量在 NSUserDefaults.mm 中均有对应的FOUNDATION_EXPORT实现。
二、架构决策:两层桥接与调用链全景
WinObjC 中一次典型的偏好读写,实际跨越了两个框架:
NSUserDefaults (Foundation) │ setObject:forKey: / objectForKey: ▼ CFPreferences (CoreFoundation) │ CFPreferencesSetAppValue / CFPreferencesCopyAppValue ▼ CFPreferencesDomain(内存字典缓存) │ synchronizeApplicationDomain → CFPropertyListCreateData ▼ AppData\Preferences\<BundleID>.plist(XML plist 文件)2.1 落盘位置与文件命名规则
在 Windows 目标(DEPLOYMENT_TARGET_WINDOWS)下,CFPreferences.c 中的_preferencesDirectoryForUserHostSafetyLevel负责构造偏好目录:
- 通过
_CFCreateApplicationRepositoryPath(alloc, CSIDL_APPDATA)获取应用的 AppData 根路径; - 追加
Preferences\子目录并确保目录存在(_CFCreateDirectory); - 若创建失败,则回退到用户主目录(
CFCopyHomeDirectoryURLForUser)。
文件名的构造在_CFPreferencesURLForStandardDomainWithSafetyLevel(CFPreferences.c)中完成:当域名是kCFPreferencesCurrentApplication时,优先取主 Bundle 的 Bundle Identifier(CFBundleGetIdentifier),取不到时回退到进程名(_CFProcessNameString),最终生成%@.plist形式的文件名,组合成Preferences\<BundleID>.plist的完整路径。
2.2 底层域的加载与同步回调
值得注意的细节是,WinObjC 为 CFPreferences 的 XML 属性列表域实现了自定义回调(CFPreferences.c):
createApplicationDomain:先在_domainContextCache中查找缓存字典;未命中则尝试从磁盘加载 plist(_CFDataCreateFromURL+CFPropertyListCreateWithData),文件不存在或解析失败时创建空的可变字典;synchronizeApplicationDomain:将内存字典序列化为 XML plist(CFPropertyListCreateData(..., kCFPropertyListXMLFormat_v1_0, ...)),再通过_CFWriteBytesToFile写回磁盘文件。
这印证了设计文档中"存储一个偏好 plist 在包 AppData 目录"的描述:内存中始终维护一份字典副本,只有 synchronize 时才真正序列化落盘。
三、写路径:非阻塞写入与异步同步队列
设计文档明确指出:
Changes to standardDefaults are written to cache and synchronized to disk as soon as possible on an asynchronous queue.
即:对 standardDefaults 的修改先写入缓存,随后在一个异步队列上尽快同步到磁盘。
3.1 setObject:forKey: 的内部流程
查看 NSUserDefaults.mm 中setObject:forKey:的实现,可以还原完整的写路径:
- (void)setObject:(id)value forKey:(NSString*)key { if (value == nil) { return; } CFTypeRef valueCopy = CFAutorelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, value, kCFPropertyListMutableContainersAndLeaves)); { std::lock_guard<std::mutex> lock(_cacheLock); [_cacheDict setObject:(id)valueCopy forKey:key]; _cacheIsDirty = YES; } [[NSNotificationCenter defaultCenter] postNotificationName:NSUserDefaultsDidChangeNotification object:self]; [self _scheduleSynchronize]; }流程分四步:
- 深拷贝:对传入值做
CFPropertyListCreateDeepCopy,保证存入缓存的对象不受调用方后续修改影响; - 写入缓存并置脏:在
_cacheLock(std::mutex)保护下写入_cacheDict,并置_cacheIsDirty = YES; - 广播通知:通过
NSNotificationCenter发出NSUserDefaultsDidChangeNotification,供 KVO 与观察者感知偏好变化; - 调度同步:调用
_scheduleSynchronize将落盘操作放入异步队列。
3.2 异步同步队列的工作机制
_scheduleSynchronize(NSUserDefaults.mm)的实现体现了"尽快但不阻塞"的设计:
- (void) _scheduleSynchronize { // Up to 2 synchronize operations are allowed in the queue, so that if an existing operation has not been removed from the queue, // we still get a synchronize after the CFPreferencesSetAppValue call. Any additional operations would result in an extra synchronize. if ([_synchronizeQueue operationCount] < 2) { [_synchronizeQueue addOperationWithBlock:^void(void) { [self synchronize]; }]; } }队列本身在initWithSuiteName:中创建并设置为最大并发数为 1(NSUserDefaults.mm),保证多个同步操作严格串行执行。队列中最多允许存在 2 个待执行/执行中的同步操作:即使上一次 synchronize 尚未出队,也能确保在最新的CFPreferencesSetAppValue之后有一次兜底同步;再多则只会产生无意义的重复落盘。dealloc中会调用waitUntilAllOperationsAreFinished等待所有同步操作完成后再释放对象。
3.3 synchronize:把缓存逐项刷入 CFPreferences
synchronize(NSUserDefaults.mm)是本实现中真正执行"缓存 → CFPreferences → 磁盘"的环节:
- (BOOL)synchronize { BOOL isDirty; { std::lock_guard<std::mutex> lock(_cacheLock); isDirty = _cacheIsDirty; if (isDirty) { [_cacheDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) { CFPreferencesSetAppValue(static_cast<CFStringRef>(key), obj, kCFPreferencesCurrentApplication); }]; [_cacheDict removeAllObjects]; _cacheIsDirty = NO; } } BOOL result = NO; if (isDirty) { result = CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication); } return result; }其语义是纯写操作:将缓存字典中的每一项通过CFPreferencesSetAppValue写入kCFPreferencesCurrentApplication对应的应用偏好域,清空缓存、复位脏标记,最后调用CFPreferencesAppSynchronize触发磁盘序列化。CFPreferencesSetAppValue与CFPreferencesAppSynchronize的实现分别在 CFApplicationPreferences.c 与 CFApplicationPreferences.c 中:前者定位到_CFStandardApplicationPreferences后写入标准应用域,后者对域缓存执行_CFSynchronizeDomainCache并刷新内存表示。
删除操作removeObjectForKey:(NSUserDefaults.mm)遵循同样的模式:从缓存移除 key、调用CFPreferencesSetAppValue(key, NULL, ...)(NULL 值表示删除)、置脏、广播通知并调度同步。
四、读路径:三级查找与类型化读取
设计文档强调读写均"非阻塞且线程安全",这与objectForKey:的三级查找结构直接对应(NSUserDefaults.mm):
- (id)objectForKey:(NSString*)defaultName { id obj; { // 1. 先在缓存中查找 std::lock_guard<std::mutex> lock(_cacheLock); obj = [_cacheDict objectForKey:defaultName]; } if (!obj) { // 2. 再查应用偏好存储 obj = [(id)CFPreferencesCopyAppValue(static_cast<CFStringRef>(defaultName), kCFPreferencesCurrentApplication) autorelease]; } if (!obj) { // 3. 最后回退到注册默认值 obj = [_registrationDict objectForKey:defaultName]; } return obj; }查找优先级为:内存缓存 → CFPreferences 应用偏好 → registerDefaults: 注册的默认值字典。底层CFPreferencesCopyAppValue通过_CFStandardApplicationPreferences与computeDictRep(CFApplicationPreferences.c)维护的合并字典提供服务,该字典按标准搜索列表(CFApplicationPreferences.c)的优先级顺序叠加各域的值。
在此之上,stringForKey:、arrayForKey:、dictionaryForKey:、dataForKey:等方法在拿到原始对象后做类型校验,类型不符返回nil;boolForKey:、integerForKey:、floatForKey:、doubleForKey:则兼容NSNumber与NSString两种存储形态并做转换(NSUserDefaults.mm)。stringArrayForKey:(NSUserDefaults.mm)还会进一步校验数组内每个元素均为NSString。
五、线程安全边界与使用禁忌
设计文档给出了两条明确的并发约束,这是本实现最重要的使用边界:
- setObjectForKey and objectForKey are non-blocking and threadsafe with other NSUserDefaults calls, but are not threadsafe against calls to CFPreferences. Thus mixed calls to NSUserDefaults and CFPreferences can result in race conditions and should be avoided.
约束一:NSUserDefaults 内部的读写互相线程安全。源码中,_cacheDict的所有访问都在_cacheLock(std::mutex)保护下进行,setObject:forKey:、objectForKey:、removeObjectForKey:、synchronize均通过锁保证对缓存的独占访问,因此多个线程同时调用 NSUserDefaults 方法是安全的,且调用不阻塞调用方线程。
约束二:NSUserDefaults 与 CFPreferences 混合调用会引发竞态,应当避免。原因在于两者维护的是不同的缓存副本:NSUserDefaults 有一份_cacheDict,CFPreferences 侧则有_domainContextCache/_dictRep等独立缓存。若一个线程通过NSUserDefaults setObject:写缓存,另一个线程直接调用CFPreferencesSetAppValue,两边各自加锁但锁不互斥,就会出现一个值被另一个值覆盖或读取到过期数据的竞态窗口。因此迁移代码时,同一偏好键应全程只走 NSUserDefaults API 或只走 CFPreferences API,不可混用。
此外,+standardUserDefaults是懒加载单例,通过@synchronized(self)保证只初始化一次(NSUserDefaults.mm);初始化时还会预置AppleLanguages(@"en")与AppleLocale(@"en_US")两个默认键(NSUserDefaults.mm)。
六、synchronize 的"只写不读"语义与外部修改覆盖风险
设计文档特别警示:
synchronize is write only and does not read from file (read occurs only at initialization). External changes to the preferences file will be overwritten.
即synchronize 只负责把内存中的改动写出去,绝不从磁盘读回;磁盘文件的读取只发生在初始化阶段(首次创建 domain 时)。结合源码可以还原这一行为的两个层面:
- NSUserDefaults 层:
synchronize只做"缓存 →CFPreferencesSetAppValue→CFPreferencesAppSynchronize",全程没有读文件逻辑; - CFPreferences 层:磁盘文件只在
createApplicationDomain(CFPreferences.c)首次加载域名时被解析进内存;synchronizeApplicationDomain(CFPreferences.c)则无条件把当前内存字典整体序列化写回文件。
由此推出的直接结论是:如果应用运行期间有其他进程(或外部工具)直接修改了偏好 plist 文件,只要本进程随后执行一次 synchronize(或异步队列触发同步),整个文件就会被本进程内存中的旧数据覆盖。这也解释了 NSUserDefaults.mm 中synchronize的注释 "Writes to file only - external changes to the preferences file are overwritten."。在 Windows 平台上,同机多实例或调试工具直接改 plist 的场景下尤其要留意这一点。
七、当前实现的支持范围与已知限制(Caveats)
设计文档的最后一条提醒:
CFPreferences currently has only minimal support necessary for NSUserDefaults functionality. Use with caution.
CFPreferences 目前只实现了支撑 NSUserDefaults 所必需的最小功能集。具体到 NSUserDefaults.mm 的实现,能力边界如下:
7.1 已完整实现(Interoperable)
init、standardUserDefaults、registerDefaults:、dictionaryRepresentation;- 全部基础读写:
objectForKey:/setObject:forKey:、removeObjectForKey:、dataForKey:、stringForKey:、arrayForKey:、dictionaryForKey:、boolForKey:、integerForKey:、floatForKey:、doubleForKey:、stringArrayForKey:、valueForKey:/setValue:forKey:; setBool:forKey:、setInteger:forKey:、setFloat:forKey:、setDouble:forKey:等类型化写入。
7.2 部分实现(Caveat)
initWithSuiteName::源码注释 "supports nil only for suitename",即仅支持传入nil(等价于init),传入真实 suite 名会触发UNIMPLEMENTED()并返回StubReturn()(NSUserDefaults.mm);setURL:forKey:/URLForKey:(NSUserDefaults.mm):- 非文件 URL(如
http://):用NSKeyedArchiver归档为NSData存储; - 文件路径 URL:直接保存
absoluteString,不做~波浪号缩写/展开(与 Apple 文档行为不同); - 文件引用 URL(file reference URL):不支持,触发
UNIMPLEMENTED()。
- 非文件 URL(如
7.3 未实现(Stub,调用即 UNIMPLEMENTED)
以下 API 目前是占位桩,调用会打印 UNIMPLEMENTED 并返回默认值,迁移代码时应避开:
+resetStandardUserDefaults;initWithUser:;persistentDomainForName:、setPersistentDomain:forName:、removePersistentDomainForName:、persistentDomainNames;volatileDomainForName:、setVolatileDomain:forName:、removeVolatileDomainForName:、volatileDomainNames;objectIsForcedForKey:及其带 domain 的变体;addSuiteNamed:、removeSuiteNamed:。
这些占位方法在头文件 include/Foundation/NSUserDefaults.h 中均以STUB_METHOD宏标注,实现侧则统一走UNIMPLEMENTED()/StubReturn()模式,便于调用方识别未完成能力。相关内部辅助方法(如_suspendSynchronize、_resumeSynchronize、_standardUserDefaultsNoInitialize)声明在 Frameworks/include/NSUserDefaultsInternal.h 中,供 Foundation 内部调度与测试使用。
八、测试验证与功能实测
仓库在 tests/functionaltests/Tests/NSUserDefaultsTests.mm 中提供了针对本模块的功能测试,可作为理解行为的可运行样例:
Basic:验证setURL:/URLForKey:往返一致性——非文件 URL(http://www.test.com/)与文件路径 URL(file://localhost/test1/test2/test3/)存入后取出均与原值相等;KVCArray:验证mutableArrayValueForKeyPath:与 NSUserDefaults 的联动,包括对不存在的 key 先创建可变数组再addObject:,随后能从objectForKey:读到新元素;Remove:演示"写入 → 读取 → 删除 → 读取为 nil"的完整生命周期;Perf:循环写入 500 个键并统计耗时,用于回归观测写入性能。
这些测试通过[NSUserDefaults standardUserDefaults]驱动真实实现路径,覆盖了上文讨论的缓存、类型化读取与删除语义,是验证行为与排查回归的参考入口。
九、实践建议:在 WinObjC 中安全使用 NSUserDefaults
综合设计规格与源码实现,给出如下落地建议:
- 统一 API 入口:读写一律走
NSUserDefaults方法,不要在同一键上混用CFPreferencesSetAppValue/CFPreferencesCopyAppValue,避免两类缓存间的竞态; - 依赖自动同步即可:
setObject:forKey:已自动调度异步同步,多数场景无需手动调用synchronize;仅在进程即将退出、需要确定性落盘时显式调用一次synchronize(或依赖dealloc中的队列等待); - 勿依赖外部改文件:不要试图在运行期手工编辑
AppData\Preferences\<BundleID>.plist,下一次 synchronize 会用内存旧数据覆盖; - 避开未实现 API:suite、persistent domain、volatile domain、强制键查询等接口仍是 Stub,迁移 iOS 代码时需先改写为
standardUserDefaults+ 普通键值对; - URL 存储注意:
setURL:对文件路径 URL 不做波浪号缩写,且不支持 file reference URL,若原 iOS 代码依赖这两点需自行适配; - 类型一致性:写入时保持值的类型稳定(如统一用
NSNumber而非混用字符串与数字),避免boolForKey:等类型化读取产生意外结果。
通过上述设计与实现细节,开发者可以准确评估 NSUserDefaults 在 WinObjC 上的行为边界,从而把 iOS 的用户偏好存储代码以最低成本、最安全的方式迁移到 Windows 平台。
- 跨平台
- 移动开发
- 开发工具
【免费下载链接】WinObjC
Objective-C for Windows
相关推荐
从单体到微服务:advanced-java 微服务架构迁移的三种渐进式策略详解
从单体到微服务:advanced java 微服务架构迁移的三种渐进式策略详解 本文基于本仓库 微服务架构 https://link.gitcode.com/i
跨平台移动开发开发工具微信聊天记录导出指南:用 WeChatMsg 把对话存成 HTML、Word 与 CSV
微信聊天记录导出指南:用 WeChatMsg 把对话存成 HTML、Word 与 CSV WeChatMsg 是一个开源的微信聊天记录导出工具:从 Mac 版微
Trippy核心架构解析:Rust多线程模型与异步I/O设计
Trippy核心架构解析:Rust多线程模型与异步I/O设计 引言:网络诊断工具的性能挑战 在网络诊断工具领域,传统实现常面临 并发探针管理 与 实时数据处理
网络CLI运维
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考