字节团队最近分享的 iOS 稳定性问题治理:卡死崩溃监控原理及最佳实践 提到:NSUserDefaults
底层实现中存在直接或者间接的跨进程通信,在主线程同步调用容易发生卡死。
随之而来的问题就是:NSUserDefaults
还能用吗?
经过对底层分析后,笔者的研究结论是:可以在理解 NSUserDefaults
的特性后再使用。
NSUserDefaults
是什么? NSUserDefaults
是 iOS 开发者常用的持久化工具,通常用于存储少量的数据
示例:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@"酷酷的哀殿" forKey:@"key"];
NSUserDefaults
的特性是什么? 根据本文后续的测试,我们可以发现 NSUserDefaults
共计以下 3 个特性:
xpc
通信NSUserDefaults
是如何保证多线程安全的? NSUserDefaults
内部在读写时,会通过锁 lock
保证读写安全
可以通过
b os_unfair_lock_lock
设置断点
image
NSUserDefaults
的性能怎么样? 虽然 NSUserDefaults
是磁盘持久化存储,但是因为缓存的存在,所以,不会频繁的进行 磁盘 I/O
可以通过私有类
CFPrefsPlistSource
的实例获取所有缓存的内容
image
我们唯一需要考虑的因素避免数据过多导致内存压力过大
NSUserDefaults
触发 xpc
的场景是什么? NSUserDefaults
与 如何监控 iOS 的启动耗时 提到的渲染过程类似,同样依赖 xpc 进行跨进程通信。
下面,我们通过添加合适的断点对相关流程进行简单的介绍
(lldb) breakpoint set -n xpc_connection_create_mach_service -C "x/s $x0" -C "p $x1" -C "p $x2"
Breakpoint 6: where = libxpc.dylib`xpc_connection_create_mach_service, address = 0x00000001d3d4e944
(lldb) breakpoint set -n xpc_connection_send_message_with_reply_sync -C "po $x0" -C "po $x1" -C "bt"
Breakpoint 7: where = libxpc.dylib`xpc_connection_send_message_with_reply_sync, address = 0x00000001d3d4f028
xpc_connection_send_message_with_reply_sync 会锁住当前线程
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@"酷酷的哀殿1" forKey:@"key"]; //xpc_connection_send_message_with_reply_sync
[defaults setObject:@"酷酷的哀殿2" forKey:@"key"]; //xpc_connection_send_message_with_reply_sync
printf("从 NSUserDefaults 读取:%s\n", [defaults stringForKey:@"key"].UTF8String);
[defaults synchronize];
NSUserDefaults *domain = [[NSUserDefaults alloc] initWithSuiteName:@"someDomain"];
[domain setObject:@"酷酷的哀殿3" forKey:@"key"]; // xpc_connection_send_message_with_reply_sync
[domain setObject:@"酷酷的哀殿4" forKey:@"key"]; // xpc_connection_send_message_with_reply_sync
printf("从 NSUserDefaults 读取:%s\n", [domain stringForKey:@"key"].UTF8String);
[domain synchronize];
通过运行测试代码,我们可以发现 +[NSUserDefaults(NSUserDefaults) standardUserDefaults] + 68
执行时,会创建名为 "com.apple.cfprefsd.daemon"
的 xpc_connection
image
随后,会通过 xpc_connection_send_message_with_reply_sync
发送一个信息
image
[defaults setObject:@"酷酷的哀殿1" forKey:@"key"];
执行时,同样会发送一个消息
image
经过测试,我们可以发现只有第一次初始化或者调用 set... forKey:
相关的方法时,才会触发多进程通信
所以,我们可以得到以下结论:
NSUserDefaults
写操作会触发 xpc
通信,读操作和 synchronize
不会触发;应该降低写操作频率
image
通过官方文档,我们可以发现 xpc
框架存在两个不会锁住当前的线程 API
所以,我们可以尝试通过以上两个 API 发送持久化信息
下面以笔者的 iOS 14.3
系统为例进行演示
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);
通过控制台,我们可以发现通过 xpc
存储的数据 this is a test
可以通过 NSUserDefaults
读取出来
证明 xpc_connection_send_message_with_reply
可以成功将内容持久化
image
本文通过分析 NSUserDefaults
的 3 个特性:1、多线程安全,2、内存级别缓存,3、写操作会触发 xpc
通信;可以得到以下结论:
只有在以下场景才适合选择 NSUserDefaults
作为数据持久化: