赞
踩
字节团队最近分享的 iOS 稳定性问题治理:卡死崩溃监控原理及最佳实践 提到:NSUserDefaults 底层实现中存在直接或者间接的跨进程通信,在主线程同步调用容易发生卡死。
随之而来的问题就是:NSUserDefaults 还能用吗?
经过对底层分析后,笔者的研究结论是:可以在理解 NSUserDefaults 的特性后再使用。
NSUserDefaults 是 iOS 开发者常用的持久化工具,通常用于存储少量的数据
示例:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@"酷酷的哀殿" forKey:@"key"];
根据本文后续的测试,我们可以发现 NSUserDefaults 共计以下 3 个特性:
NSUserDefaults 内部在读写时,会通过锁 lock 保证读写安全
可以通过 b os_unfair_lock_lock 设置断点
虽然 NSUserDefaults 是磁盘持久化存储,但是因为缓存的存在,所以,不会频繁的进行 磁盘 I/O
我们唯一需要考虑的因素避免数据过多导致内存压力过大
五、NSUserDefaults 触发 xpc 的场景是什么?
NSUserDefaults 与 如何监控 iOS 的启动耗时 提到的渲染过程类似,同样依赖 xpc 进行跨进程通信。
下面,我们通过添加合适的断点对相关流程进行简单的介绍
添加调试断点
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”]; 执行时,同样会发送一个消息
mage
经过测试,我们可以发现只有第一次初始化或者调用 set… forKey: 相关的方法时,才会触发多进程通信
所以,我们可以得到以下结论:
NSUserDefaults 写操作会触发 xpc 通信,读操作和 synchronize 不会触发;应该降低写操作频率
通过官方文档,我们可以发现 xpc 框架存在两个不会锁住当前的线程 API
xpc_connection_send_message
xpc_connection_send_message_with_reply
所以,我们可以尝试通过以上两个 API 发送持久化信息
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 可以成功将内容持久化
本文通过分析 NSUserDefaults 的 3 个特性:1、多线程安全,2、内存级别缓存,3、写操作会触发 xpc 通信;可以得到以下结论:
只有在以下场景才适合选择 NSUserDefaults 作为数据持久化:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。