9

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

 3 years ago
source link: https://mp.weixin.qq.com/s/Y1AHFN1kJ9kCjXdFOnUviA
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

Original 酷酷的哀殿 酷酷的哀殿 3/12
收录于话题
#疑难问题 5
#性能优化 7
#iOS 10

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

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

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

一、NSUserDefaults 是什么?

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

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@"酷酷的哀殿" forKey:@"key"];

二、NSUserDefaults 的特性是什么?

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

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

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

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

可以通过 b os_unfair_lock_lock 设置断点

640?wx_fmt=jpeg
image

四、NSUserDefaults 的性能怎么样?

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

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

640?wx_fmt=jpeg
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

640?wx_fmt=jpeg
image

随后,会通过 xpc_connection_send_message_with_reply_sync 发送一个信息

640?wx_fmt=jpeg
image

[defaults setObject:@"酷酷的哀殿1" forKey:@"key"]; 执行时,同样会发送一个消息

640?wx_fmt=jpeg
image

经过测试,我们可以发现只有第一次初始化或者调用 set... forKey: 相关的方法时,才会触发多进程通信

所以,我们可以得到以下结论:

NSUserDefaults 写操作会触发 xpc 通信,读操作和 synchronize 不会触发;应该降低写操作频率

640?wx_fmt=jpeg
image

四、如何异步的持久化?

通过官方文档,我们可以发现 xpc 框架存在两个不会锁住当前的线程 API

  1. xpc_connection_send_message
  2. xpc_connection_send_message_with_reply

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

异步持久化 Demo

下面以笔者的 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 可以成功将内容持久化

640?wx_fmt=jpeg
image

本文通过分析 NSUserDefaults 的 3 个特性:1、多线程安全,2、内存级别缓存,3、写操作会触发 xpc 通信;可以得到以下结论:

只有在以下场景才适合选择 NSUserDefaults 作为数据持久化:

  1. 少量数据存储
  2. 多线程安全

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK