mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 20:00:40 +00:00
257 lines
11 KiB
Markdown
257 lines
11 KiB
Markdown
# 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` 加锁进行多线程安全保护。
|
||
|
||

|
||
|
||
## 存储性能如何
|
||
|
||
查看 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` 可以看到下面的堆栈
|
||
|
||

|
||
|
||
执行 `[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
|
||
// <OS_xpc_dictionary: dictionary[0x7fa975908010]: { refcnt = 1, xrefcnt = 1, subtype = 0, count = 8 } <dictionary: 0x7fa975908010> { count = 8, transaction: 0, voucher = 0x0, contents =
|
||
// "CFPreferencesHostBundleIdentifier" => <string: 0x7fa9759080d0> { length = 9, contents = "test.demo" }
|
||
// "CFPreferencesUser" => <string: 0x7fa975908250> { length = 25, contents = "kCFPreferencesCurrentUser" }
|
||
// "CFPreferencesOperation" => <int64: 0x8ccdbf87dd7d7a91>: 1
|
||
// "Value" => <string: 0x7fa9759084b0> { length = 16, contents = "酷酷的哀殿2" }
|
||
// "Key" => <string: 0x7fa975908430> { length = 3, contents = "key" }
|
||
// "CFPreferencesContainer" => <string: 0x7fa9759083a0> { length = 169, contents = "/private/var/mobile/Containers/Data/Application/0C224166-1674-4D36-9CDB-9FCDB633C7E3/" }
|
||
// "CFPreferencesCurrentApplicationDomain" => <bool: 0x7fff80002fd0>: true
|
||
// "CFPreferencesDomain" => <string: 0x7fa975906ea0> { 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);
|
||
```
|