“NSUserDefaults同步”如何运行如此之快?

时间:2016-12-15 05:14:09

标签: ios nsuserdefaults mmap virtual-memory

在我的应用程序中,我想在每个用户登录的plist文件中保存用户设置,我写one class called CCUserSettings,它具有与NSUserDefaults几乎相同的接口,并且它读取和写入相关的plist文件到当前用户ID。它有效,但性能不佳。每次用户拨打[[CCUserSettings sharedUserSettings] synchronize]时,我都会将NSMutableDictionary(保留用户设置)写入plist文件,下面的代码显示synchronize CCUserSettings省略一些细节。

- (BOOL)synchronize {
    BOOL r = [_settings writeToFile:_filePath atomically:YES];
    return r;
}

我认为NSUserDefaults应该在我们调用[[NSUserDefaults standardUserDefaults] synchronize]时写入文件,但它运行得非常快,我写了demo进行测试,关键部分在下面,运行1000次{在我的iPhone6上{1}}和[[NSUserDefaults standardUserDefaults] synchronize],结果是0.45秒对9.16秒。

[[CCUserSettings sharedUserSettings] synchronize]

结果显示,NSDate *begin = [NSDate date]; for (NSInteger i = 0; i < 1000; ++i) { [[NSUserDefaults standardUserDefaults] setBool:(i%2==1) forKey:@"key"]; [[NSUserDefaults standardUserDefaults] synchronize]; } NSDate *end = [NSDate date]; NSLog(@"synchronize seconds:%f", [end timeIntervalSinceDate:begin]); [[CCUserSettings sharedUserSettings] loadUserSettingsWithUserId:@"1000"]; NSDate *begin = [NSDate date]; for (NSInteger i = 0; i < 1000; ++i) { [[CCUserSettings sharedUserSettings] setBool:(i%2==1) forKey:@"_boolKey"]; [[CCUserSettings sharedUserSettings] synchronize]; } NSDate *end = [NSDate date]; NSLog(@"CCUserSettings modified synchronize seconds:%f", [end timeIntervalSinceDate:begin]); 几乎比NSUserDefaults快20倍。现在我开始怀疑&#34;每当我们调用CCUserSettings方法时,NSUserDefaults是否真的写入plist文件?&#34;,但如果它没有,那么它如何保证数据写入?在进程退出之前返回文件(因为进程可能随时被杀死)?

这些天我想出了一个改进synchronize的想法,它是CCUserSettings Memory-mapped I/O。我可以将虚拟内存映射到文件,每次用户调用mmap时,我都会使用synchronize方法创建NSData并将数据复制到该内存中,操作系统会将内存写回进程退出时的文件。但我可能无法获得良好的性能,因为文件大小不固定,每次数据长度增加时,我都需要重新NSPropertyListSerialization dataWithPropertyList:format:options:error:虚拟内存,我相信操作非常耗时。

对于我的多余详细信息感到抱歉,我只是想知道mmap如何才能取得如此优异的成绩,或者是否有人可以提出一些好的建议来改进我的NSUserDefaults

2 个答案:

答案 0 :(得分:2)

最后,我想出了一个解决方案,用mmap改善CCUserSettings的效果,我称之为CCMmapUserSettings

<强>前提条件

synchronizeCCUserSettings方法中的NSUserDefaults将plist文件写回磁盘,需要花费大量时间,但我们必须在应用进入后台的某些情况下调用它。即便这样,我们也会冒失去设置的风险:我们的应用程序可能会被系统杀死,因为它耗尽了内存或访问了一个它没有权限的地址,那时我们在最新设置之后设置了{{ 1}}可能会失败。

如果有一种方法可以在进程退出时将文件写入磁盘,我们可以随时修改内存中的设置,速度非常快。但有没有办法实现这一目标?

好吧,我找到一个,它是mmap,mmap将文件映射到内存区域。完成后,可以像程序中的数组一样访问该文件。所以我们可以像编写文件一样修改内存。当进程退出时,内存将写回文件。

有两个链接支持我:

Does the OS (POSIX) flush a memory-mapped file if the process is SIGKILLed?

mmap, msync and linux process termination

使用mmap的问题

正如我在问题中提到的那样:

  

这些天我提出了改进CCUserSettings的想法,它是mmap内存映射I / O.我可以将虚拟内存映射到一个文件,每次用户调用同步时,我用NSPropertyListSerialization dataWithPropertyList创建一个NSData:format:options:error:方法并将数据复制到该内存中,操作系统会在进程退出时将内存写回文件。但是我可能无法获得良好的性能,因为文件大小不固定,每次数据长度增加时,我都需要重新映射虚拟内存,相信操作非常耗时。

问题是:每当数据长度增加时,我必须重新synchronize虚拟内存,这是耗时的操作。

<强>解决方案

现在我有了一个解决方案:始终创建一个比我们需要的更大的大小,并将实际文件大小保留在文件的开头4个字节中,并在4个字节后写入实际数据。由于文件大于我们所需的文件,当数据平滑增加时,我们不需要在每次调用mmap时重新mmap内存。文件大小还有另一个限制:文件大小始终是synchronize的倍数(在我的应用中定义为4096)。

同步方法:

MEM_PAGE_SIZE

一个例子将有助于描述我的想法:假设plist数据大小是5000字节,我需要写的总字节数是4 + 5000 = 5004.我写4字节无符号整数,其值先是5004然后写5000字节数据。总文件大小应为8192(2 * MEM_PAGE_SIZE)。我创建一个更大的文件的原因是我需要一个大缓冲区来减少重新映射内存的时间。

<强>性能

- (BOOL)synchronize {
    if (!_changed) {
        return YES;
    }
    NSData *data = [NSPropertyListSerialization dataWithPropertyList:_settings format:NSPropertyListXMLFormat_v1_0 options:0 error:nil];
    // even if data.length + sizeof(_memoryLength) is a multiple of MEM_PAGE_SIZE, we need one more page.
    unsigned int pageCount = (unsigned int)(data.length + sizeof(_memoryLength)) / MEM_PAGE_SIZE + 1;
    unsigned int fileSize = pageCount * MEM_PAGE_SIZE;
    if (fileSize != _memoryLength) {
        if (_memory) {
            munmap(_memory, _memoryLength);
            _memory = NULL;
            _memoryLength = 0;
        }

        int res = ftruncate(fileno(_file), fileSize);
        if (res == -1) {
            // truncate file error
            fclose(_file);
            _file = NULL;
            return NO;
        }
        // re-map the file
        _memory = (unsigned char *)mmap(NULL, fileSize, PROT_READ|PROT_WRITE, MAP_SHARED, fileno(_file), 0);
        _memoryLength = (unsigned int)fileSize;
        if (_memory == MAP_FAILED) {
            _memory = NULL;
            fclose(_file);
            _file = NULL;
            return NO;
        }
#ifdef DEBUG
        NSLog(@"memory map file success, size is %@", @(_memoryLength));
#endif
    }

    if (_memory) {
        unsigned int length = (unsigned int)data.length;
        length += sizeof(length);
        memcpy(_memory, &length, sizeof(length));
        memcpy(_memory+sizeof(length), data.bytes, data.length);
    }
    return YES;
}

输出结果为:

{
    [[CCMmapUserSettings sharedUserSettings] loadUserSettingsWithUserId:@"1000"];
    NSDate *begin = [NSDate date];
    for (NSInteger i = 0; i < 1000; ++i) {
        [[CCMmapUserSettings sharedUserSettings] setBool:(i%2==1) forKey:@"_boolKey"];
        [[CCMmapUserSettings sharedUserSettings] synchronize];
    }
    NSDate *end = [NSDate date];
    NSLog(@"CCMmapUserSettings modified synchronize seconds:%f", [end timeIntervalSinceDate:begin]);
}

{
    [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"key"];
    NSDate *begin = [NSDate date];
    for (NSInteger i = 0; i < 1000; ++i) {
        [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"key"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }
    NSDate *end = [NSDate date];
    NSLog(@"NSUserDefaults not modified synchronize seconds:%f", [end timeIntervalSinceDate:begin]);
}

{
    NSDate *begin = [NSDate date];
    for (NSInteger i = 0; i < 1000; ++i) {
        [[NSUserDefaults standardUserDefaults] setBool:(i%2==1) forKey:@"key"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }
    NSDate *end = [NSDate date];
    NSLog(@"NSUserDefaults modified synchronize (memory not change) seconds:%f", [end timeIntervalSinceDate:begin]);
}

它表明CCMmapUserSettings modified synchronize seconds:0.037747 NSUserDefaults not modified synchronize seconds:0.479931 NSUserDefaults modified synchronize (memory not change) seconds:0.402940 的运行速度比CCMmapUserSettings !!!

我不确定

NSUserDefaults通过iPhone6(iOS 10.1.1)上的单位设置,但我真的不确定它是否适用于所有iOS版本,因为我还没有获得官方文档以确保内存用于映射文件的过程会在进程退出时立即写回磁盘,如果没有,是否会在设备关闭之前写入磁盘?

我想我必须研究CCMmapUserSettings的系统行为,如果有人知道,请分享。非常感谢。

答案 1 :(得分:2)

在现代操作系统(iOS 8 +,macOS 10.10+)上,NSUserDefaults在您调用synchronize时不会写入文件。当您调用-set *方法时,它会向名为cfprefsd的进程发送异步消息,该进程存储新值,发送回复,然后在稍后将文件写出。 All -synchronize确实等待所有未完成的消息到cfprefsd接收回复。

(编辑:如果您愿意,可以通过在xpc_connection_send_message_with_reply上设置符号断点然后设置用户默认值来验证这一点)