前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从底层分析一下存在跨进程通信问题的 NSUserDefaults 还能用吗?

从底层分析一下存在跨进程通信问题的 NSUserDefaults 还能用吗?

作者头像
酷酷的哀殿
发布2021-03-18 09:50:48
2.4K0
发布2021-03-18 09:50:48
举报
文章被收录于专栏:酷酷的哀殿

前言

字节团队最近分享的 iOS 稳定性问题治理:卡死崩溃监控原理及最佳实践 提到:NSUserDefaults 底层实现中存在直接或者间接的跨进程通信,在主线程同步调用容易发生卡死。

随之而来的问题就是:NSUserDefaults 还能用吗?

经过对底层分析后,笔者的研究结论是:可以在理解 NSUserDefaults 的特性后再使用

一、NSUserDefaults 是什么?

NSUserDefaults 是 iOS 开发者常用的持久化工具,通常用于存储少量的数据

示例:

代码语言:javascript
复制
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@"酷酷的哀殿" forKey:@"key"];

二、NSUserDefaults 的特性是什么?

根据本文后续的测试,我们可以发现 NSUserDefaults 共计以下 3 个特性:

  1. 多线程安全
  2. 内存级别缓存
  3. 写操作会触发 xpc 通信

三、NSUserDefaults 是如何保证多线程安全的?

NSUserDefaults 内部在读写时,会通过锁 lock 保证读写安全

可以通过 b os_unfair_lock_lock 设置断点

image

四、NSUserDefaults 的性能怎么样?

虽然 NSUserDefaults 是磁盘持久化存储,但是因为缓存的存在,所以,不会频繁的进行 磁盘 I/O

可以通过私有类 CFPrefsPlistSource 的实例获取所有缓存的内容

image

我们唯一需要考虑的因素避免数据过多导致内存压力过大

三、NSUserDefaults 触发 xpc 的场景是什么?

NSUserDefaults如何监控 iOS 的启动耗时 提到的渲染过程类似,同样依赖 xpc 进行跨进程通信。

下面,我们通过添加合适的断点对相关流程进行简单的介绍

添加调试断点

代码语言:javascript
复制
(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 会锁住当前线程

准备测试代码

代码语言:javascript
复制
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

  1. xpc_connection_send_message
  2. xpc_connection_send_message_with_reply

所以,我们可以尝试通过以上两个 API 发送持久化信息

异步持久化 Demo

下面以笔者的 iOS 14.3 系统为例进行演示

代码语言:javascript
复制
    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 作为数据持久化:

  1. 少量数据存储
  2. 偶尔修改
  3. 多线程安全
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 酷酷的哀殿 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、NSUserDefaults 是什么?
  • 二、NSUserDefaults 的特性是什么?
  • 三、NSUserDefaults 是如何保证多线程安全的?
  • 四、NSUserDefaults 的性能怎么样?
  • 三、NSUserDefaults 触发 xpc 的场景是什么?
    • 添加调试断点
      • 准备测试代码
        • 测试
        • 四、如何异步的持久化?
          • 异步持久化 Demo
          • 五、总结
          相关产品与服务
          数据保险箱
          数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档