Files
knowledge-kit/Chapter1 - iOS/1.106.md
2022-05-24 13:00:23 +08:00

257 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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
// <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);
// 注释1test.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);
```