NSUserDefaults Unreliable in iOS 8

2019-01-16 08:26发布

问题:

I have an app that uses [NSUserDefaults standardUserDefaults] to store session information. Generally, this information is checked on app launch, and updated on app exit. I have found that it seems to be working unreliably in iOS 8.

I am currently testing on an iPad 2, although I can test on other devices if need be.

Some of the time, data written before exit will not persist on app launch. Equally, keys removed before exit sometimes appear to exist after launch.

I've written the following example, to try and illustrate the issue:

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

Running the above code, I (usually) get the output below (which is what I would expect):

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

If I then comment out the lines that remove the key:

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

And run the app three times, I would expect to see:

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

But the output I actually see is:

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

e.g. It seems to not be updating the value on exiting the app.

EDIT: I have tested the same code on an iPad 2 running iOS 7.1.2; and it appears to work correctly every time.

TLDR - In iOS 8 does [NSUserDefaults standardUserDefaults] work unreliably? And if so is there a workaround/solution?

回答1:

iOS 8 introduced a number of behavior changes to NSUserDefaults. While the NSUserDefaults API has changed little, the behavior has changed in ways that may be relevant to your application. For example, using -synchronize is discouraged (and always has been). Addition changes to other parts of Foundation and CoreFoundation such as File Coordination and changes related to shared containers may affect your application and your use of NSUserDefaults.

Writing to NSUserDefaults in particular has changed because of this. Writing takes longer, and there may be other processes competing for access to the application's user defaults storage. If you are attempting to write to NSUserDefaults as your application is exiting, your application may be terminated before the write is committed under some scenarios. Forcefully terminating using exit(0) in your example is very likely to stimulate this behavior. Normally when an application is exited the system can perform cleanup and wait for outstanding file operations to complete - when you are terminating the application using exit() or the debugger this may not happen.

In general NSUserDefaults is reliable when used correctly on iOS 8.

These changes are described in the Foundation release notes for OS X 10.10 (currently there is not a separate Foundation release note for iOS 8).



回答2:

It looks like iOS 8 does not like setting strings in NSUserDefaults. Try encoding the string into NSData before saving.

When saving:

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

When reading:

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

Hope this helps.



回答3:

As gnasher729 said, don’t call exit(). There may be an issue with NSUserDefaults in iOS8, but calling exit() simply won’t work.

You should see David Smith’s comments on NSUserDefaults (https://gist.github.com/anonymous/8950927):

Terminating an app abnormally (memory pressure kill, crash, stop in Xcode) is like git reset --hard HEAD, and leaving



回答4:

I found NSUserDefaults to behave nicely on iOS 8.4 when using a suite name to create an instance instead of relying on standardUserDefaults.

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



回答5:

I have the same problem with iOS 8 and the only solution worked for me is to delay perform of exit() function some duration (Ex.: 0.1 seconds) using:

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

or create a method and then call it using performSelector:withObject:afterDelay:

- (void)exitApp {
   exit(0);
}

[self performSelector:@selector(exitApp) withObject:nil afterDelay:0.1];


回答6:

Since this is an Enterprise App and not an App Store app, you can try:

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

and then call:

[UIApplication.sharedApplication _terminateWithStatus:0];

It's using an undocumented API so may not work in previous or future versions of iOS.



回答7:

It is bug in simulators.This bug also exist in prior to iOS8 beta4 on devices.But on devices this bug is resolved but it currently exist on simulators.They have also changed the simulator directories structure.If you reset your simulator it will work fine.On iOS8 devices it will also work fine.



回答8:

I found it in Foundation Framework Reference, think it will be useful:

The NSUserDefaults class provides convenience methods for accessing common types such as floats, doubles, integers, Booleans, and URLs. A default object must be a property list, that is, an instance of (or for collections a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. If you want to store any other type of object, you should typically archive it to create an instance of NSData. For more details, see Preferences and Settings Programming Guide.



回答9:

As others had pointed out using exit() and generaly exiting your app yourself is really bad idea in iOS.

But I probably know what do you have to deal with. We did develop an enterprise application as well and even though we tried to convince the client that in iOS it is against all the rules and best practices, they insisted that we close the app at one point.

Instead of exit() we used this piece of code:

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

As the name suggests it only suspends the app as if the user pressed home button. Therefore your saving methods might be able to finish correctly.

I haven't tested this solution for your particular case though, and I'm not sure if suspend is enough for you, but for us it worked.



回答10:

I have solved similar issues by making changes to NSUserDefaults only in the main thread.



回答11:

I faced the same issue. I solved it by calling

[[NSUserDefaults standardUserDefaults] synchronize];

before calling

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

It turns out one has to call synchronize not only after setting but before getting too.



回答12:

Calling exit () in an iOS application is a criminal offence, and as you noticed, it got punished. You never quit an iOS application yourself. Never.