1

UPDATE 1: Issue does NOT occur when iOS' data protection (i.e. Passcode Lock) is off!

UPDATE 2: Issue does NOT occur when ARC is disabled.

UPDATE 3: Issue does NOT occur when app is killed prior to restarting iOS.

My Settings class implements the Singleton pattern using +initialize (plus code to see what was going on):

@implementation Settings

static Settings*        sharedSettings;
static NSUserDefaults*  userDefaults;

+ (void)initialize
{
    if ([Settings class] == self)
    {
        sharedSettings = [self new];
        userDefaults = [NSUserDefaults standardUserDefaults];


        // Code to see what's going on here ...
        if (userDefaults == nil)
        {
            [[[UIAlertView alloc] initWithTitle:@"userDefaults == nil"
                                        message:nil
                                       delegate:nil
                              cancelButtonTitle:@"Close"
                              otherButtonTitles:nil] show];
        }
        else
        {
            if ([userDefaults objectForKey:@"Hello"] == nil)
            {
                [[[UIAlertView alloc] initWithTitle:@"Hello == nil"
                                            message:nil
                                           delegate:nil
                                  cancelButtonTitle:@"Close"
                                  otherButtonTitles:nil] show];
            }
            else if ([userDefaults boolForKey:@"Hello"] == NO)
            {
                [[[UIAlertView alloc] initWithTitle:@"Hello == NO"
                                            message:nil
                                           delegate:nil
                                  cancelButtonTitle:@"Close"
                                  otherButtonTitles:nil] show];
            }

            [userDefaults setBool:YES forKey:@"Hello"];
            if ([userDefaults synchronize] == NO)
            {
                [[[UIAlertView alloc] initWithTitle:@"synchronize == NO"
                                            message:nil
                                           delegate:nil
                                  cancelButtonTitle:@"Close"
                                  otherButtonTitles:nil] show];
            }
        }
    }
}

+ (id)allocWithZone:(NSZone*)zone
{
    if (sharedSettings && [Settings class] == self)
    {
        [NSException raise:NSGenericException format:@"Duplicate Settings singleton creation"];
    }

    return [super allocWithZone:zone];
}

+ (Settings*)sharedSettings
{
    return sharedSettings;
}

@end

I trigger this code in my AppDeletate.m (completely stripped down):

#import "AppDelegate.h"
#import "Settings.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
    [Settings        sharedSettings];

    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    [self.window makeKeyAndVisible];

    return YES;
}

@end

The very weird thing now is that when running this empty app right after restarting iOS, I get both "synchronize == NO" and "Hello == nil" popups. When I then kill the app and run it again, everything is perfectly fine. It turns out that when I delay the userDefaults = [NSUserDefaults standardUserDefaults] and subsequent statements using GCD's dispatch_after(), a little (I did 2 seconds, but much less would probably be equally fine) the problem goes away. Can someone tell me why this is, or is this simple some corner-case iOS bug or side-effect???

ADDITION: It turns out that this issue goes away when I kill the app prior to restarting iOS. This to me proves that it's something in iOS.

Thanks for listening!

Cornelis

ps: You can trust me, I stripped my app completely, there are only three modules: main.mm (the standard XCode generated one-liner), AppDelegate.m (complete code shown above), and Settings.m (complete code shown above). So there is no other code running at all. When I stripped it even further, leaving just AppDelegate.m, the problem remained.

meaning-matters
  • 21,929
  • 10
  • 82
  • 142

1 Answers1

3

You should not reference other classes during +initialize. There is no promises about when it runs and what other classes exist at that point. +initialize should only do internal-to-the-class work.

This singleton pattern has been superseded since the addition of GCD. You should use the GCD pattern for various reasons, one of which being that it runs when you think it runs.

How do I implement an Objective-C singleton that is compatible with ARC?

As a note, I am curious about your main.mm. Making the top-level ObjC++ is almost always a bad idea, and can lead to some surprising side-effects. (ObjC++ is a wacky glue language with many odd behaviors. You should use it in as few classes as possible.) I would personally see if changing this back to .m fixes the problem, though I'd still recommend a GCD-Singleton rather than using +initialize.

Community
  • 1
  • 1
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • iOS documentation: "The runtime sends `initialize` to each class in a program exactly one time just before the class, or any class that inherits from it, is sent its first message from within the program." Ergo, when `+initialize` runs has been precisely defined. In my case when I do `[Settings sharedSettings]`, at fairly standard point of app setup. – meaning-matters Mar 16 '13 at 15:50
  • It is not guaranteed that this is the first message sent to the class. The class may, for instance, receive a `+load` message earlier depending on how it's loaded. It is also undefined what order `+initialize` methods are called. You should not assume that another class has already been configured when your `+initialize` is called. – Rob Napier Mar 16 '13 at 15:53
  • I've removed my `Setting.m` and moved the `NSUserDefaults` code to `AppDelegate` (replacing `[Settings sharedSettings]`). Also renamed `main.mm` (which was needed for some external lib to pull in C++ stuff) back to `main.m`. The problem still persists. So it seems to be something coming from iOS. – meaning-matters Mar 16 '13 at 16:12
  • 1
    I understand [here](http://stackoverflow.com/questions/13326435/nsobject-load-and-initialize-what-do-they-do) that `load` will have most probably already been called once AppDelegate's `didFinishLaunchingWithOptions` get it's chance. And that does not seem to be a problem for `initialize`, which I seem to have used in a correct manner. The only class I referenced is NSUserDefaults, which is in good shape when `didFinishLaunchingWithOptions` is invoked. Then I think that order of `initialize` invocations can be fully tracked by sufficient detailed analysis of ones code; it's not a random order. – meaning-matters Mar 16 '13 at 16:27
  • 1
    Regarding your update, you should wait for `applicationProtectedDataDidBecomeAvailable:`. – Rob Napier Mar 20 '13 at 13:04
  • Very good suggestion @RobNapier I placed the setup code in a method which is called from the delegate method, or from `didFinishLaunchingWithOptions:` when `protectedDataAvailable` is already true. I had good hopes ... Now the `synchronize` never fails again, but after restarting iOS the user defaults are still wiped/corrupted. – meaning-matters Mar 21 '13 at 12:37