2

Start a new single view project and update the main ViewController's viewDidLoad. The intention is to retrieve and increment a value stored in NSUserDefaults and save it.

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSString *key = @"kTheKey";

    NSNumber *number = [[NSUserDefaults standardUserDefaults] objectForKey:key];
    NSLog(@"current value is %@", number);

    NSNumber *incremented = @(number.integerValue + 1);
    NSLog(@"new value will be %@", incremented);

    [[NSUserDefaults standardUserDefaults] setObject:incremented forKey:key];
    [[NSUserDefaults standardUserDefaults] synchronize];

    NSLog(@"reboot");
}

If I force quit the app from Xcode (or in practical use, reboot the device), the defaults are frequently not saved. Here is some sample output:

current value is (null)
new value will be 1
reboot
current value is 1
new value will be 2
reboot
current value is 1
new value will be 2
reboot

There appears to be some time component - if I wait 3+ seconds before rebooting, it is more likely the defaults will save. Note that the first execution was 'allowed' to save by waiting for a few seconds before stopping execution. The second execution was stopped in the first second or two, leading to the unchanged values logged in the the third run. This is re-produceable on my iPad Air 2 running iOS 8.1.

What could account for this?

Ben Packard
  • 26,102
  • 25
  • 102
  • 183
  • Are the last 3 lines of the log actually just copy paste of the previous 3? – Wain Jan 18 '15 at 14:43
  • Yes, I had trouble copying and pasting the content without exceeding the time component I mentioned. I will remove the timestamps to avoid confusion. – Ben Packard Jan 18 '15 at 14:53
  • 1
    Will it happen on device? I don't think so. – gabbler Jan 18 '15 at 15:42
  • 1
    The code looks OK and `NSUserDefaults` should persist across reboots. But see this SO question: http://stackoverflow.com/q/2622754/558933 . Also, is there some reason you are not using `integerForKey` and `setInteger:forKey`? – Robotic Cat Jan 18 '15 at 15:51
  • I just wrote an answer which stated that you should synchronize in applicationWillTerminate but then I realized that you're already synchronizing, so that might not be the issue... – Lyndsey Scott Jan 18 '15 at 16:01
  • The first 6 lines of your log look to me like what you're expecting, right? As @Wain asked, are those last three actually from a subsequent run of the app? – Ben Zotto Jan 18 '15 at 16:29
  • According to an engineer who improved the iOS 8 synchronize function at Apple, "On iOS 8 the delay after calling -[NSUserDefaults set*:forKey:] before the value is safely stored is about 1ms." (https://twitter.com/catfish_man/status/510122371995299840) so I don't know what's causing the issue yet, but "synchronize" should hypothetically make little difference... – Lyndsey Scott Jan 18 '15 at 16:31
  • @LyndseyScott: I believe the `-synchronize` call will always forcibly flush anything yet unwritten, so as soon as it returns you can assume the data is on disk. My guess is that David Smith's comment means that iOS 8 makes an explicit synchronization less necessary because it's now so fast without. – Ben Zotto Jan 18 '15 at 16:34
  • @BenZotto Yes, I'm saying that the 3 sec delay the OP is observing might be unrelated to synchronize. Something else might be going on. Or there could potentially be a bug if the function's in flux... – Lyndsey Scott Jan 18 '15 at 16:36
  • @LyndseyScott: Ah, I see. Yes, I agree-- the OP's code looks fine. – Ben Zotto Jan 18 '15 at 16:37
  • 1
    @BenPackard Your code works perfectly for me. – Lyndsey Scott Jan 18 '15 at 16:39
  • @LyndseyScott - are you stopping execution from XCode (command-.) quickly enough? Stop the app as soon as the console logs. I'm testing on an iOS 8.1 iPad fwiw. – Ben Packard Jan 18 '15 at 16:41
  • @BenZotto - yes, that's what I'm expecting. The first execution I allowed to save by not quitting for a few seconds. The second execution, I quit immediately (or within 1-2 seconds) to demonstrate the inconsistent nature of the issue. – Ben Packard Jan 18 '15 at 16:43
  • @gabbler - yes, I am testing this on a device. an iPad Air 2 running iOS 8.1 to be specific. – Ben Packard Jan 18 '15 at 16:43
  • 1
    @BenPackard Yes, I'm quitting immediately. I added `exit(0);` right after `NSLog(@"reboot");` and everything prints as expected. – Lyndsey Scott Jan 18 '15 at 16:45
  • @LyndseyScott thanks for trying it. Did you run on a device? Which? I will start testing whether I can get the same behavior on some other devices. – Ben Packard Jan 18 '15 at 16:49
  • 1
    @BenPackard Hm... You're right. I originally ran it on a simulator and got the expected results, but I just tried it on my device and it's not always saving. Bug perhaps? – Lyndsey Scott Jan 18 '15 at 16:53
  • @RoboticCat check my code - I am already calling `synchronize`. The link you suggest is not relevant. – Ben Packard Jan 19 '15 at 00:02

1 Answers1

3

This is normal behavior.

User defaults get queued to save. When you "force quit" the app you're not giving it time to do that.

I assume that on Xcode you mean hitting stop. And you mention exit(0);. Neither of things are "normal" to an iOS app. This type of force quitting should not be done in an iOS app.

When the user quits an app the normal way (multi-task view and sliding the app up) it doesn't actually quit right then. It removes from view as if it does. But the user defaults will then get written out after that. Up to several seconds after. Same when they hit the home button.

The documentation completely explains the app life cycle. Use the messages. And you should force flush out the defaults when these messages are received. Put in your initialization or ViewDidLoad like this:

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(movingToBackground:) name:UIApplicationWillResignActiveNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(movingToForeground:) name:UIApplicationDidBecomeActiveNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateDefaults:) name:UIApplicationWillTerminateNotification object:nil];

Then create the methods movingToBackground, movingToForeground, and updateDefaults like this:

-(void) updateDefaults: (NSNotification *) notification {
     [[NSUserDefaults standardUserDefaults] synchronize];
}
badweasel
  • 2,349
  • 1
  • 19
  • 31
  • Did you miss the fact that I am calling `synchronize` manually? If not, could you provide a source for "User defaults get queued to save" please? My understanding is that you can rely on the methods you propose, but calling `synchronize` is supposed to trigger an immediate write. Furthermore, do the methods you mention get called if the app crashes? How about if the user force-quits? – Ben Packard Jan 18 '15 at 23:57
  • 1
    I didn't miss it. If you call that it will queue up to do it. If you exit(0) or hit stop in Xcode it probably won't save it out. Synchronize doesn't save it right that second. You said if I wait 3 seconds it saves it. If I don't it doesn't. There is your answer. It's not natural in iOS for apps to quit on their own. If the user quits the app or shuts the phone down, the os quits the app when it's done saving the defaults. It's seamless to the user. – badweasel Jan 19 '15 at 00:02
  • What you're doing is like pulling the plug on the app. It's not exiting gracefully. – badweasel Jan 19 '15 at 00:06
  • Again, do you have a source? This is contrary to almost everything I've ever read on persisting `NSUserDefaults`. And there are some circumstances (crashes) where 'waiting for the OS to do the right thing' might not cut it. – Ben Packard Jan 19 '15 at 00:07
  • https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSUserDefaults_Class/index.html - the implication here is that `synchronize` should be be immediate: "At runtime, you use an NSUserDefaults object to read the defaults that your application uses from a user’s defaults database. NSUserDefaults caches the information to avoid having to open the user’s defaults database each time you need a default value. The synchronize method, which is automatically invoked at periodic intervals, keeps the in-memory cache in sync with a user’s defaults database." – Ben Packard Jan 19 '15 at 00:09
  • I'm on mobile right now so I don't have s link to the apple docs. But the source is the apple docs and my 4.5 years experience making iOS apps. If your app crashes it might not save the defaults. If you app crashes you need to fix the crash. Not expect the iOS to behave as designed when your app doesn't. My answer is correct. I'll add the source in the docs when I can. Sorry if you don't like the answer. But your basically saying "my computer doesn't save my work if I hit save and immediately pull the plug" - yup that's going to be true. – badweasel Jan 19 '15 at 00:12
  • 1
    No, I'm saying "my computer doesn't save my work if I tell it to save my work". I look forward to your reference, thanks again. – Ben Packard Jan 19 '15 at 00:13
  • Ok down vote my answer then. Sorry. Report it as a bug. – badweasel Jan 19 '15 at 00:14
  • I likely will, looks like it might be a dupe: http://stackoverflow.com/questions/16861142/nsuserdefaults-synchronize-not-saving-on – Ben Packard Jan 19 '15 at 00:31
  • Check out this answer as well. There are more discussions and answers here that lead to the fact that what you're trying to do isn't something iOS wants you doing. http://stackoverflow.com/questions/25917106/nsuserdefaults-unreliable-in-ios-8 – badweasel Jan 19 '15 at 00:33
  • Thanks - those 10.10 docs describing the changes are exactly what I'm I'm looking for. PS what 'I'm trying to do' is just understand a recent change. I wouldn't propose to ship anything that does this. Thanks again for the link. – Ben Packard Jan 19 '15 at 02:04
  • Yet you'll down vote me and not accept my answer. I don't think this is the concept behind SO. But ok. – badweasel Jan 30 '15 at 08:19
  • My question wasn't 'please explain the intended use of nsuserdefauts'. It was 'why is this thing that always worked one way no longer doing as expected.' Your answer doesn't address that (the link in your comment does though). It instead outlines a strategy for persisting nsuserdefaults that I'm both a) already aware of and b) worked the same before and after the change. I'm sorry if your feelings are hurt. – Ben Packard Jan 30 '15 at 12:55
  • I don't have feelings. I just believe that it's always worked the same way. Although some improper use that maybe worked before doesn't now. That is the nature of an ever evolving os. But we can agree to disagree on that I guess. – badweasel Jan 30 '15 at 12:58