Singleton in Objective-C, compatible with ARC and

2019-04-16 07:04发布

问题:

I want to have a thread safe, ARC compatible singleton, but is seems to me that the most common example of singleton that I find, an example pasted here:

+ (MyClass *)sharedInstance
{
    static MyClass *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
        // Do any other initialisation stuff here
    });
    return sharedInstance;
}

doesn't stops other developer from calling [[MyClass alloc] init] and overriding the desired flow. What is the proper way to handle it (apart from throwing exception in init)?

回答1:

you also have to override the +alloc method to avoid to allocate more than one instance of the singleton.

EDIT#3: well, I really know what the official documentation says about overriding the +alloc method, but to achieve the asked benefit there is no way to avoid it. personally I don't agree to do it but it can provide the desired result.

it would be like this:

static MyClass *_sharedInstance = nil;
static BOOL _bypassAllocMethod = TRUE;

+ (id)sharedInstance {
    @synchronized([MyClass class]) {
         if (_sharedInstance == nil) {
              _sharedInstance = [[MyClass alloc] init];
         }
    }
    return _sharedInstance;
}

+ (id)alloc {
    @synchronized([MyClass class]) {
         _bypassAllocMethod = FALSE; // EDIT #2
         if (_sharedInstance == nil) {
              _sharedInstance = [super alloc];
              return _sharedInstance;
         } else {
              // EDIT #1 : you could throw an exception here to avoid the double allocation of the singleton class
              @throw [NSException exceptionWithName:[NSString stringWithFormat:@"<%@: %p> Double allocation issue", [_sharedInstance class], _sharedInstance] reason:@"You cannot allocate the singeton class twice or more." userInfo:nil];
         }
    }
    return nil;
}

// EDIT #2 : the init method
- (id)init {
    if (_bypassAllocMethod)
        @throw [NSException exceptionWithName:@"invalid allocation" reason:@"invalid allocation" userInfo:nil];

    if (self = [super init]) {
    }

    return self
}

EDIT #1

you don't definitely need to throw an exception here but it is much more visual feedback for the developers of they use your class in wrong way, than sending back a simple nil pointer.

EDIT #2

I've added a simple trick to avoid the developers instantiate the class to bypass the modified +alloc method, in that case the allocation will work well but the -init will throw an exception.



回答2:

Use the Borg pattern instead of the Singleton pattern: Allow multiple instantiation of your class and have the instances share the same static state.

// Shared data
static NSDictionary *sharedData = nil;

+ (void) initialize {
  // Initialize shared data
  sharedData = [[NSDictionary alloc] init];
}

- (id) init {
  self = [super init];

  if (self) {
    self.data = sharedData;
  }
}

This way, clients may arbitrarily use static getInstance methods or init methods, and receive objects sharing the same state. They need not even be aware that it's a singleton.



回答3:

I tend to use the following: (with newer instancetype compiler syntax)

@implementation MyClass

+ (instancetype)myClass {
  static MyClass *singleton; // keep global variables in the most minimal scope

  if (singleton == nil) @synchronized (self) {
    singleton = [[MyClass alloc] initPrivate];
  }

  return singleton;
}

- (instancetype)initPrivate { // ARC requires the method start with "init…"
  self = [super init];

  return self;
}

- (instancetype)init {
  return nil;
}

@end

This also doesn't prevent others from calling [[MyClass alloc] privateInit] but it will warn them (unless they write their own hushing code).

This would also leak pre-ARC if someone calls [[MyClass alloc] init] - but you've got bigger problems if that happens. Optionally, you could throw an exception when init is called. (as in holex's answer)

Also, subclasses could theoretically get in a race condition. If you're worried about that change @synchronized (self) to @synchronized ([McClass class]). I prefer the self as cleaner code and I know there won't be subclasses.



回答4:

convert instance to class method and use the Class object as the singleton.

for example you have a singleton class like this

@interface MySingleton {
    int count;
}

+ (MySingleton *)sharedInstance;
- (int)getNext;

@end

I am suggesting you to convert it to

@interface MySingleton

+ (int)getNext;

@end

in MySingleton.m

static int count;

then you can use it like

[MySingleton getNext];

or

id obj = [MySingleton class]; // Class objects are singleton provided by runtime
[obj getNext];

EDIT

I just want to point out, there are already so many ObjC implementation for singleton pattern.

http://www.cocoawithlove.com/2008/11/singletons-appdelegates-and-top-level.html https://github.com/fadingred/objc-singleton

simple google search will find them. everything are considered. (much more than what I was expected)