# NSUserDefault 底层原理探究 最近看到字节一篇文章 [卡死崩溃监控原理及最佳实践](https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==&mid=2247488080&idx=1&sn=39d0386b97b9ac06c6af1966f48387fc&chksm=e9d0d9b2dea750a4a7d21fd383aefa014d63f0dc79f2e3a13c97ad52bba1578dca8b50d6a40a&scene=21&cur_album_id=1590407423234719749#wechat_redirect) ,里面写到 NSUserDefault 底层实现存在直接或者间接跨进程通信,在主线程同步调用容易卡死。以前只是用过,但是没有仔细研究,看到这里就有必要研究下底层实现啦。 ## 回顾 NSUserDefault 不安全。因为数据自动保存在沙盒的 `Libarary/Preferences` 目录下。 数据按照 plist (property list)格式存储在沙盒中。当攻击者破解 App 就可轻而易举拿到里面的数据(可能有些人会将 token、password、secret 明文存在里面) 另外 App 卸载重装会导致之前存储的数据丢失。这里推荐使用 Keychain。Keychain 是 iOS 提供的安全存储数据的方案,用于存储一些账号、密码等敏感信息。数据也不在沙盒中,即使删除 App,重新安装则可以继续从 Keychain 中获取数据。 NSUserDefaults 的原理和 plist 序列化不同。 iOS 上应用必须被沙盒化,各个不同 App 之间的 Defaults Domain 不一样,通常是 Bundle Identifier,或者是 App Group 中约定的 Suite Name。当使用 NSUserDefaults 的时候会按照下面 Domain 顺序: - NSArgumentDomain - 应用的 Bundle Identifier - NSGlobalDomain - 系统语言的标识符 - NSRegistrationDomain 任何应用,通过 NSUserDefaults 访问值都需要经历从上到下搜索各个 Domain 的过程,期间如何某个 Domain 有这个值,就会取出其对应的值。如果全部访问完还是没找到,则返回 undefined result。 ## 如何保证多线程安全 通过设置符号断点可以看出, NSUserDefaults 内部在读写时会通过 `os_unfair_lock` 加锁进行多线程安全保护。 ![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/NSUserDfault-lock.png) ## 存储性能如何 查看 GUN 源码 ```c - (BOOL) synchronize { BOOL isLocked = NO; BOOL wasLocked = NO; BOOL shouldLock = NO; BOOL defaultsChanged = NO; BOOL hasLocalChanges = NO; // 合法性校验 if ([removed count] || [added count] || [modified count]) { hasLocalChanges = YES; } if (YES == hasLocalChanges && NO == [owner _readOnly]) { shouldLock = YES; } if (YES == shouldLock && YES == [owner _lockDefaultsFile: &wasLocked]) { isLocked = YES; } NS_DURING { NSFileManager *mgr; NSMutableDictionary *disk; // 利用 NSFileManager 读取文件 mgr = [NSFileManager defaultManager]; disk = nil; if (YES == [mgr isReadableFileAtPath: path]) { NSData *data; // 文件存在,则将里面的内容读取出来 data = [NSData dataWithContentsOfFile: path]; if (nil != data) { id o; // 将文件数据利用 NSPropertyListSerialization 序列化为 NSDictionary 信息 o = [NSPropertyListSerialization propertyListWithData: data options: NSPropertyListImmutable format: 0 error: 0]; // 将之前已经持久化好的本地文件数据写入到 disk 可变字典 if ([o isKindOfClass: [NSDictionary class]]) { disk = AUTORELEASE([o mutableCopy]); } } } if (nil == disk) { disk = [NSMutableDictionary dictionary]; } loaded = YES; // 判断是否有新数据 if (NO == [contents isEqual: disk]) { defaultsChanged = YES; if (YES == hasLocalChanges) { NSEnumerator *e; NSString *k; // 从标记为待删除的数据中遍历,删除 disk 可变字典中的数据 e = [removed objectEnumerator]; while (nil != (k = [e nextObject])) { [disk removeObjectForKey: k]; } // 遍历需要添加的数据,添加到 disk 中 e = [added objectEnumerator]; while (nil != (k = [e nextObject])) { [disk setObject: [contents objectForKey: k] forKey: k]; } // 遍历需要修改的数据,添加到 disk 中 e = [modified objectEnumerator]; while (nil != (k = [e nextObject])) { [disk setObject: [contents objectForKey: k] forKey: k]; } } // 将 disk 数据拷贝到 contents ASSIGN(contents, disk); } if (YES == hasLocalChanges) { BOOL written = NO; if (NO == [owner _readOnly]) { if (YES == isLocked) { // 判断 contents 字典是否有值,没有则给指定路径写入 nil if (0 == [contents count]) { /* Remove empty defaults dictionary. */ written = writeDictionary(nil, path); } else { /* Write dictionary to file. */ // 判断 contents 字典有值,则将 contents 给指定路径写入 written = writeDictionary(contents, path); } } } // 写入成功删除内存缓存 if (YES == written) { [added removeAllObjects]; [removed removeAllObjects]; [modified removeAllObjects]; } } if (YES == isLocked && NO == wasLocked) { isLocked = NO; [owner _unlockDefaultsFile]; } } NS_HANDLER { fprintf(stderr, "problem synchronising defaults domain '%s': %s\n", [name UTF8String], [[localException description] UTF8String]); if (YES == isLocked && NO == wasLocked) { [owner _unlockDefaultsFile]; } } NS_ENDHANDLER return defaultsChanged; } ``` 会发现:性能也就那么回事,底层实现通过内存缓存 `contents` 来缓存数据写入文件。 ## NSUserDefaults 为什么触发 XPC 通信 通过对代码添加符号断点 `xpc_connection_send_message_with_reply_sync` 可以看到下面的堆栈 ![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/NSUserDefault-XPC.png) 执行 `[NSUserDefaults standardUserDefaults];` 可以发现是调用了 XPC,创建名称为 “com.apple.cfprefsd.daemon” 的 XPC Connection,且会发送一个 `xpc_connection_send_message_with_reply_sync` 的消息。 执行 `[defaults setObject:@"杭城小刘" forKey:@"name"];` 会调用 `xpc_connection_send_message_with_reply_sync`,发送一个消息。 通过 Demo 得出结论: - `NSUserDefaults` 调用 `set...forKey:`, 会触发 XPC 通信,调用 `...ForKey:` 、`synchronized` 不会调用 XPC 通信 - 为了提高性能,尽量减少调用 `set...forKey:` ## 异步持久化 XPC 该`xpc_connection_send_message_with_reply_sync` API 因为 XPC 同步通信,所以在主线程容易存在卡死。那么有没有异步调用的能力? 发现2个 API 可以用于异步发送 - xpc_connection_send_message - xpc_connection_send_message_with_reply 所以想异步持久化,则需要自定义 XPC Connection,然后将数据用 xpc_dictionary_create 创造出的 Dictionary 去接,最后调用 `xpc_connection_send_message_with_reply` 去持久化数据 ```objectivec xpc_connection_t conn = xpc_connection_create_mach_service("com.apple.cfprefsd.daemon", NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED); #pragma mark - 开始构建信息 // (lldb) po $rsi // { count = 8, transaction: 0, voucher = 0x0, contents = // "CFPreferencesHostBundleIdentifier" => { length = 9, contents = "test.demo" } // "CFPreferencesUser" => { length = 25, contents = "kCFPreferencesCurrentUser" } // "CFPreferencesOperation" => : 1 // "Value" => { length = 16, contents = "ÈÖ∑ÈÖ∑ÁöÑÂìÄÊÆø2" } // "Key" => { length = 3, contents = "key" } // "CFPreferencesContainer" => { length = 169, contents = "/private/var/mobile/Containers/Data/Application/0C224166-1674-4D36-9CDB-9FCDB633C7E3/" } // "CFPreferencesCurrentApplicationDomain" => : true // "CFPreferencesDomain" => { length = 9, contents = "test.demo" } // }> xpc_object_t hello = xpc_dictionary_create(NULL, NULL, 0); // 注释1:test.demo 是 bundleid。测试代码时,需要根据需要修改 xpc_dictionary_set_string(hello, "CFPreferencesHostBundleIdentifier", "test.demo"); xpc_dictionary_set_string(hello, "CFPreferencesUser", "kCFPreferencesCurrentUser"); // 注释2:存储值 xpc_dictionary_set_int64(hello, "CFPreferencesOperation", 1); // 注释3:存储的内容 xpc_dictionary_set_string(hello, "Value", "this is a test"); xpc_dictionary_set_string(hello, "Key", "key"); // 注释4:存储的位置 CFURLRef url = CFCopyHomeDirectoryURL(); const char *container = CFStringGetCStringPtr(CFURLCopyPath(url), kCFStringEncodingASCII); xpc_dictionary_set_string(hello, "CFPreferencesContainer", container); xpc_dictionary_set_bool(hello, "CFPreferencesCurrentApplicationDomain", true); xpc_dictionary_set_string(hello, "CFPreferencesDomain", "test.demo"); xpc_connection_set_event_handler(conn, ^(xpc_object_t object) { printf("xpc_connection_set_event_handler:收到返回消息: %sn", xpc_copy_description(object)); }); xpc_connection_resume(conn); #pragma mark - 异步方案一 (没有回应) // xpc_connection_send_message(conn, hello); #pragma mark - 异步方案二 (有回应) xpc_connection_send_message_with_reply(conn, hello, NULL, ^(xpc_object_t _Nonnull object) { printf("xpc_connection_send_message_with_reply:收到返回消息: %sn", xpc_copy_description(object)); NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; // mach_msg_trap printf("从 NSUserDefaults 读取:%s\n", [defaults stringForKey:@"key"].UTF8String); }); #pragma mark - 同步方案 // xpc_object_t obj = xpc_connection_send_message_with_reply_sync(conn, hello); // NSLog(@"xpc_connection_send_message_with_reply_sync:收到返回消息:%s", xpc_copy_description(obj)); NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; // mach_msg_trap printf("从 NSUserDefaults 读取:%s\n", [defaults stringForKey:@"key"].UTF8String); ```