【问题标题】:NSUserDefaults Unreliable in iOS 8NSUserDefaults 在 iOS 8 中不可靠
【发布时间】:2014-11-13 01:20:48
【问题描述】:

我有一个使用 [NSUserDefaults standardUserDefaults] 来存储会话信息的应用程序。 通常,此信息会在应用启动时检查,并在应用退出时更新。 我发现它在 iOS 8 中似乎运行不可靠。

我目前正在 iPad 2 上进行测试,但如果需要,我可以在其他设备上进行测试。

有时,退出前写入的数据不会在应用启动时保留。同样,退出前移除的键有时在启动后似乎存在。

我编写了以下示例,以尝试说明问题:

- (void)viewDidLoad 
{
    [super viewDidLoad];

    NSData *_dataArchive = [[NSUserDefaults standardUserDefaults] 
                                            objectForKey:@"Session"];

    NSLog(@"Value at launch - %@", _dataArchive);

    NSString *testString = @"TESTSTRING";
    [[NSUserDefaults standardUserDefaults] setObject:testString 
                                           forKey:@"Session"];
    [[NSUserDefaults standardUserDefaults] synchronize];

    _dataArchive = [[NSUserDefaults standardUserDefaults] 
                     objectForKey:@"Session"];

    NSLog(@"Value after adding data - %@", _dataArchive);

    [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"Session"];
    [[NSUserDefaults standardUserDefaults] synchronize];

    _dataArchive = [[NSUserDefaults standardUserDefaults] 
                     objectForKey:@"Session"];

    NSLog(@"Value before exit - %@", _dataArchive);

    exit(0);
}

运行上面的代码,我(通常)得到下面的输出(这是我所期望的):

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - (null)

如果我随后注释掉删除密钥的行:

//[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"Session"];
//[[NSUserDefaults standardUserDefaults] synchronize];

并运行应用程序三遍,我希望看到:

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - TESTSTRING

Value at launch - TESTSTRING
Value after adding data - TESTSTRING
Value before exit - TESTSTRING

Value at launch - TESTSTRING
Value after adding data - TESTSTRING
Value before exit - TESTSTRING

但我实际看到的输出是:

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - TESTSTRING

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - TESTSTRING

Value at launch - (null)
Value after adding data - TESTSTRING
Value after deleting data - TESTSTRING

例如它似乎没有更新退出应用程序的值。

编辑:我在运行 iOS 7.1.2 的 iPad 2 上测试了相同的代码;它似乎每次都能正常工作。

TLDR - 在 iOS 8 中,[NSUserDefaults standardUserDefaults] 工作不可靠吗?如果是这样,是否有解决方法/解决方案?

【问题讨论】:

  • 请说明您正在运行的设备。如果它是模拟器 - 虽然它应该工作,但我并不感到特别惊讶它没有。它有很多错误!
  • 如果您在最初读取数据之前synchronize 怎么办?不确定这是否有必要,但关于NSUserDefaults 的Apple 文档确实说您可以同步“如果您想将用户默认值更新为磁盘上的内容”NSUserDefaults
  • @AirsourceLtd 我已经在上面更新了。这是在 iPad 2 上测试的。我有其他设备可用,所以我今天将尝试它们。
  • @Stonz2 感谢您的建议。刚才试过了,结果还是一样。
  • @Pavan 在我想出更好的解决方案之前,我编写了一个简单的类来保存/加载数据到/从 plist。

标签: ios objective-c ios8 nsuserdefaults


【解决方案1】:

iOS 8 对NSUserDefaults 进行了许多行为更改。虽然NSUserDefaults API 几乎没有变化,但其行为已经以可能与您的应用程序相关的方式发生了变化。例如,不鼓励使用-synchronize(并且一直如此)。对 Foundation 和 CoreFoundation 其他部分的额外更改(例如文件协调和与共享容器相关的更改)可能会影响您的应用程序和您对 NSUserDefaults 的使用。

因此,写信给NSUserDefaults 的内容尤其发生了变化。写入需要更长的时间,并且可能有其他进程竞争访问应用程序的用户默认存储。如果您在应用程序退出时尝试写入NSUserDefaults,则在某些情况下,您的应用程序可能会在写入提交之前终止。在您的示例中使用 exit(0) 强制终止很可能会刺激这种行为。通常,当应用程序退出时,系统可以执行清理并等待未完成的文件操作完成 - 当您使用 exit() 或调试器终止应用程序时,这可能不会发生。

通常NSUserDefaults 在 iOS 8 上正确使用时是可靠的。

Foundation release notes for OS X 10.10 中描述了这些更改(目前没有针对 iOS 8 的单独 Foundation 发行说明)。

【讨论】:

  • 调用 -synchronize 通常是必需的,例如在 iOS 8 应用程序和扩展程序之间共享首选项时。
  • 请查看我链接到的发行说明。您描述的场景是一种特别提到的场景,其中-synchronize 应该被使用。
  • 重新阅读它们。它说“如果你同步是因为你在设置后立即发送通知重新读取另一个程序中的首选项,并且你想确保它会选择它,那么它可能仍然值得调用同步。”在 iOS 中,扩展作为单独的进程运行。
  • 在 iOS 和 MacOS 上,应用程序不会修改首选项。它们总是由单独的进程访问。再次阅读发行说明:“如果您:同步以从您的流程之外读取更改的值。那么现在请不要这样做”。此外,当使用带有扩展的共享容器时,必须使用套件。使用+standardUserDefaults 在应用程序和扩展程序之间共享信息将无法正常运行。这在扩展编程指南以及至少一个技术问答中都有介绍。
【解决方案2】:

看起来 iOS 8 不喜欢在 NSUserDefaults 中设置字符串。在保存之前尝试将字符串编码为 NSData。

保存时:

[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:testString] forKey:@"Session"];

阅读时:

NSData *_data = [[NSUserDefaults standardUserDefaults] objectForKey:@"Session"];
NSString *_dataArchive = [NSKeyedUnarchiver unarchiveObjectWithData:_data];

希望这会有所帮助。

【讨论】:

  • 有这方面的证据吗?
  • 我的 8 个大量使用 NSUserDefaults 的应用程序已经停止为相当多的用户保存数据。这一切都是因为保存的变量之一是一个字符串。用上述方法替换后,保存恢复正常。
  • 感谢@Igor 的建议,但对我来说似乎没有什么不同。
【解决方案3】:

正如 gnasher729 所说,不要调用 exit()。 可能 iOS8 中的 NSUserDefaults 存在问题,但调用 exit() 根本不起作用。

您应该在 NSUserDefaults (https://gist.github.com/anonymous/8950927) 上看到 David Smith 的 cmets:

异常终止应用程序(内存压力杀死、崩溃、在 Xcode 中停止)就像 git reset --hard HEAD,然后离开

【讨论】:

  • 那么在您看来我应该怎么做呢?这不是应用商店应用。它是一个自定义应用程序,它需要关闭应用程序并退出到主屏幕,而不使用主页按钮。我最初的问题是“为什么这在 iOS 8 中有所不同”和“我怎样才能做到这一点”。我不认为“不要调用退出”可以帮助我找到解决方案
  • 在 iOS 应用程序中不使用 exit() 不仅仅是 Apple 政策的问题,而是让您的应用程序正常运行的问题。这不会进入应用商店这一事实无关紧要。
  • @HaemEternal AFAIK,没有正确的记录方式来退出 iOS 应用程序。您可以尝试私有 API,因为这是一个内部应用程序,但我鼓励您讨论“关闭应用程序的要求”。如您所见,这是不受支持的,但这也是“不是 iOS 方式”。如果你能让你的客户重新考虑这个要求,每个人都会更开心。
【解决方案4】:

我发现 NSUserDefaults 在 iOS 8.4 上使用套件名称创建实例而不是依赖于 standardUserDefaults 时表现良好。

NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"MySuiteName"];

【讨论】:

    【解决方案5】:

    我在 iOS 8 上遇到了同样的问题,唯一对我有用的解决方案是延迟执行 exit() 函数一段时间(例如:0.1 秒),使用:

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC / 10), dispatch_get_main_queue(), ^{ exit(0); });
    

    或创建一个方法,然后使用 performSelector:withObject:afterDelay:

    调用它
    - (void)exitApp {
       exit(0);
    }
    
    [self performSelector:@selector(exitApp) withObject:nil afterDelay:0.1];
    

    【讨论】:

      【解决方案6】:

      我遇到了同样的问题。我通过调用解决了它

      [[NSUserDefaults standardUserDefaults] synchronize];

      打电话之前

      [[NSUserDefaults standardUserDefaults] stringForKey:@"my_key"].

      事实证明,不仅要在设置之后还要在获取之前调用synchronize

      【讨论】:

        【解决方案7】:

        由于这是一个企业应用而不是 App Store 应用,您可以尝试:

        @interface UIApplication()
        -(void) _terminateWithStatus:(int)status;
        @end
        

        然后调用:

        [UIApplication.sharedApplication _terminateWithStatus:0];
        

        它使用了未记录的 API,因此可能无法在以前或未来的 iOS 版本中使用。

        【讨论】:

          【解决方案8】:

          这是模拟器中的错误。这个错误也存在于iOS8 beta4之前的设备上。但是在设备上这个错误已经解决但它目前存在于模拟器中。他们还改变了模拟器目录结构。如果你重置你的模拟器可以正常工作。在 iOS8 设备上也可以正常工作。

          【讨论】:

          • 感谢您的建议。我正在设备上进行测试,而不是模拟器;在 iOS 8 上不是测试版
          【解决方案9】:

          我在 Foundation Framework Reference 中找到它,认为它会很有用:

          NSUserDefaults 类提供了方便的访问方法 常见类型,例如浮点数、双精度数、整数、布尔值和 URL。一种 默认对象必须是一个属性列表,即(或 对于集合实例的组合):NSData,NSString, NSNumber、NSDate、NSArray 或 NSDictionary。 如果您想存储任何 其他类型的对象,您通常应该将其归档以创建一个 NSData 的实例。 有关更多详细信息,请参阅首选项和设置 编程指南。

          【讨论】:

            【解决方案10】:

            正如其他人指出的那样,在 iOS 中使用 exit() 并且通常自己退出应用程序确实是个坏主意。

            但是我可能知道你要处理什么。我们确实也开发了一个企业应用程序,尽管我们试图让客户相信在 iOS 中这违反了所有规则和最佳实践,但他们坚持让我们在某一时刻关闭该应用程序。

            我们使用了这段代码,而不是 exit():

            UIApplication *app = [UIApplication sharedApplication];
            [app performSelector:@selector(suspend)];
            

            顾名思义,它只会暂停应用程序,就像用户按下主页按钮一样。因此,您的保存方法可能能够正确完成。

            不过,我还没有针对您的特定情况测试此解决方案,我不确定挂起是否对您来说足够,但对我们来说它有效。

            【讨论】:

              【解决方案11】:

              我通过仅在主线程中更改 NSUserDefaults 解决了类似的问题。

              【讨论】:

                【解决方案12】:

                在 iOS 应用程序中调用 exit() 是一种犯罪行为,正如您所注意到的,它会受到惩罚。您从不自己退出 iOS 应用程序。绝不。

                【讨论】:

                • 我完全同意。除了... 这是商业应用商店中不存在的企业应用程序。对于我们的用例,我们需要这样做。
                • @gnasher 这究竟是对 OP 问题的回答?这与他的问题无关
                • 一切都与他的问题有关。 exit 会立即杀死该应用程序。任何正在运行的线程也会被杀死。现在,如果您假设 synchronize 在您调用它时立即运行,而不是在后台线程上执行同步,那么这些假设会让您大吃一惊。
                • @HaemEternal:Apple 制定的规则是有原因的。你的产品经理是否比在 Apple 工作的数百名开发人员更聪明,更了解用户界面设计?我不这么认为。
                • 这是一个非常烦人的 API 限制。我们也想要这个,但从来没有想出一个好的方法来做到这一点。您可以通过打开 URL 打开另一个应用程序(例如 Safari),但无法返回主屏幕。由于它是企业应用程序而不是 App Store 应用程序,因此您可能能够在 NSApplication 上找到一个私有方法,该方法将通过 performSelector 工作。您可以通过stackoverflow.com/questions/2899716/… 列出它们
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2014-11-18
                • 2014-12-06
                • 1970-01-01
                • 2016-10-14
                • 1970-01-01
                • 1970-01-01
                • 2012-08-10
                相关资源
                最近更新 更多