Files
knowledge-kit/Chapter1 - iOS/1.106.md
2022-05-30 15:03:36 +08:00

11 KiB
Raw Blame History

NSUserDefault 底层原理探究

最近看到字节一篇文章 卡死崩溃监控原理及最佳实践 ,里面写到 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 加锁进行多线程安全保护。

存储性能如何

查看 GUN 源码

- (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 可以看到下面的堆栈

执行 [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 去持久化数据

    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);