我的Objective-C单例应该长什么样?

333

我的单例访问方法通常是以下变体之一:

static MyClass *gInstance = NULL;

+ (MyClass *)instance
{
    @synchronized(self)
    {
        if (gInstance == NULL)
            gInstance = [[self alloc] init];
    }

    return(gInstance);
}

我应该做些什么来改进这个呢?


27
你的代码已经可以使用,但你可以将全局变量声明放到 +instance 方法中(除非你允许设置它,否则它只在此处使用),并使用像 +defaultMyClass 或 +sharedMyClass 这样的名称来命名你的方法。+instance 不明确表达意图。 - Chris Hanson
由于这个问题的“答案”不太可能在短时间内发生改变,我将在该问题上放置历史锁定。有两个原因:1)有很多浏览量、投票和好的内容;2)防止开/关状态的来回变化。这是一个非常好的问题,但这类问题不适合在Stack Overflow上提问。我们现在有Code Review可以检查工作代码。请将所有关于此问题的讨论转到此元问题 - George Stocker
26个回答

207

另一个选项是使用+(void)initialize方法。文档提到:

运行时在程序中第一次向类或继承自它的类发送消息之前,会向每个类发送initialize方法,确保该方法只被调用一次。(因此,如果不使用该类,则可能永远不会调用该方法。)运行时以线程安全的方式向类发送initialize消息。超类会在子类之前收到该消息。

因此,你可以做类似于这样的事情:

static MySingleton *sharedSingleton;

+ (void)initialize
{
    static BOOL initialized = NO;
    if(!initialized)
    {
        initialized = YES;
        sharedSingleton = [[MySingleton alloc] init];
    }
}

7
如果运行时只会调用一次,那么这个BOOL有什么作用?这是为了防止某人从他们的代码中显式调用此函数而采取的预防措施吗? - Aftermathew
5
是的,这是一种预防措施,因为该函数也可以直接调用。 - Robbie Hanson
33
这也是必需的,因为可能会存在子类。如果它们不覆盖 +initialize 方法,那么当第一次使用子类时将调用其父类的实现。 - Sven
3
@Paul你可以重写release方法并将其置为空。 :) - user142019
4
根据列出的文档,此代码已经是线程安全的。因此,该调用在运行时只会执行一次。这似乎是正确的、线程安全且效率最优的解决方案。 - lilbyrdie
显示剩余10条评论

95
@interface MySingleton : NSObject
{
}

+ (MySingleton *)sharedSingleton;
@end

@implementation MySingleton

+ (MySingleton *)sharedSingleton
{
  static MySingleton *sharedSingleton;

  @synchronized(self)
  {
    if (!sharedSingleton)
      sharedSingleton = [[MySingleton alloc] init];

    return sharedSingleton;
  }
}

@end

[来源]


7
通常情况下,这是你应该用于单例的全部内容。另外,保持类的可独立实例化性使得它们更易于测试,因为你可以测试单独的实例而不必担心如何重置它们的状态。 - Chris Hanson
3
Stig Brautaset:在这个例子中省略 @synchronized 是不可以的。@synchronized 的作用是为了处理两个线程同时执行这个静态函数,同时通过“if(!sharedSingleton)”测试的可能性产生的竞争条件,从而导致两个 [MySingleton alloc] 的结果...... @synchronized {范围块} 强制使假设的第二个线程在被允许进入它之前等待第一个线程退出 {范围块}。希望这可以帮到你!=) - MechEthan
3
有什么方法能够阻止其他人创建自己的对象实例吗?例如:MySingleton *s = [[MySingelton alloc] init]; - lindon fox
1
@lindonfox 你的问题的答案是什么? - khatchad
1
@Raffi - 对不起,我想我可能忘记粘贴我的答案了。无论如何,我得到了书籍Pro Objective-C Design Patterns for iOS,它详细介绍了如何创建“严格”的单例模式。基本上,由于您无法使初始化方法私有化,因此需要覆盖方法alloc和copy。因此,如果您尝试执行类似[[MySingelton alloc] init]的操作,您将收到运行时错误(不幸的是不会在编译时出错)。我不理解对象创建的所有细节,但您需要实现+ (id) allocWithZone:(NSZone *)zone,该方法在sharedSingleton中被调用。 - lindon fox
显示剩余4条评论

59

根据我在下面的另一个回答中所述,我认为您应该执行以下操作:

+ (id)sharedFoo
{
    static dispatch_once_t once;
    static MyFoo *sharedFoo;
    dispatch_once(&once, ^ { sharedFoo = [[self alloc] init]; });
    return sharedFoo;
}

6
不要再费心处理你上面所做的一切。将您(希望非常少)的单例分别实例化,只需具有共享/默认方法即可。如果您真正仅想要一个类的单个实例,则需要执行您所做的操作。但您并不需要这样做,特别是对于单元测试。 - Chris Hanson
问题在于,这是创建“单例模式”的苹果示例代码。但是,没错,你绝对是正确的。 - Colin Barrett
1
若想要“真正”的单例(即只能实例化一次的对象),那么苹果示例代码是正确的。但正如克里斯所说,这并不是你通常想要或需要的,相反,你通常需要某种可设置的共享实例。 - Luke Redpath
这是上述方法的宏:https://gist.github.com/1057420。这是我使用的。 - Kobski
1
除了单元测试之外,没有任何反对这个解决方案的声音,对吧?而且它又快又安全。 - CodeSmile

58

自从Kendall发布了一个尝试避免锁定成本的线程安全单例,我也想发表一个:

#import <libkern/OSAtomic.h>

static void * volatile sharedInstance = nil;                                                

+ (className *) sharedInstance {                                                                    
  while (!sharedInstance) {                                                                          
    className *temp = [[self alloc] init];                                                                 
    if(!OSAtomicCompareAndSwapPtrBarrier(0x0, temp, &sharedInstance)) {
      [temp release];                                                                                   
    }                                                                                                    
  }                                                                                                        
  return sharedInstance;                                                                        
}

好的,让我解释一下这是如何工作的:

  1. 快速情况:在正常执行中,sharedInstance 已经被设置,因此 while 循环从未执行并且该函数在仅测试变量是否存在后返回;

  2. 慢速情况:如果 sharedInstance 不存在,则会分配一个实例并将其复制到其中,使用比较和交换('CAS');

  3. 争用情况:如果两个线程同时尝试调用 sharedInstance 且在同一时间 AND sharedInstance 不存在,则它们都将初始化单例的新实例并尝试将其 CAS 到位置。无论哪个线程赢得了 CAS 都会立即返回,输的线程则会释放它刚刚分配的实例并返回现在已设置的 sharedInstance。单个的 OSAtomicCompareAndSwapPtrBarrier 同时充当了写障碍和读障碍。


18
这种做法完全是杀鸡焉用牛刀,因为应用程序的生命周期中最多只会发生一次。尽管如此,这种做法十分准确,而且比较和交换技术是一种有用的工具,值得了解,因此点赞。 - Steve Madsen
不错的回答 - OSAtomic家族是值得了解的好东西。 - Bill
1
@Louis:太棒了,真是启发性的回答!不过我有一个问题:在你的方法中,我的init方法应该做什么?我认为在初始化sharedInstance时抛出异常并不是一个好主意。那么要怎么做才能防止用户直接多次调用init呢? - matm
2
通常我不会阻止它。通常有合理的理由允许一个单例被多次实例化,最常见的是某些类型的单元测试。如果我真的想强制使用单个实例,我可能会让init方法检查全局变量是否存在,如果存在,就释放self并返回全局变量。 - Louis Gerbarg
嗯,为什么这里需要使用 volatile 修饰符呢?由于 sharedInstance 只被初始化一次,我们如何通过使用 volatile 防止编译器将其缓存到寄存器中? - Tony
1
@Tony 回复有点晚了,但是 OSAtomicCompareAndSwapPtrBarrier 需要一个 volatile。也许 volatile 关键字是为了防止编译器优化掉检查?参见:https://dev59.com/mFXTa4cB1Zd3GeqP0Uwj#5334727 和 https://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man3/OSAtomicCompareAndSwapPtrBarrier.3.html。 - Ben Flynn

14
静态变量 `sharedInst` 被初始化为空值。
+ (id)sharedInstance { @synchronized( self ) { if ( sharedInst == nil ) { /* sharedInst在init中设置 */ [[self alloc] init]; } } return sharedInst; }
- (id)init { if ( sharedInst != nil ) { [NSException raise:NSInternalInconsistencyException format:@"[%@ %@] 不能被调用;请改用 +[%@ %@]", NSStringFromClass([self class]), NSStringFromSelector(_cmd), NSStringFromClass([self class]), NSStringFromSelector(@selector(sharedInstance))]; } else if ( self = [super init] ) { sharedInst = self; /* 任何类特定的内容都可以写在这里 */ } return sharedInst; }
/* 在GC应用程序中这些可能没有用。 保持单例模式在非GC应用程序中作为实际上的单例模式 */ - (NSUInteger)retainCount { return NSUIntegerMax; }
- (oneway void)release { }
- (id)retain { return sharedInst; }
- (id)autorelease { return sharedInst; }

3
如果你没有将[[self alloc] init]的结果分配给sharedInst,那么Clang会报告一个泄漏(leak)。 - pix0r
在我看来,像这样破坏init的方式相当丑陋。不要干扰init和/或对象的实际创建。如果您选择控制对共享实例的访问点,而不是将单例硬编码到对象中,那么以后编写测试等会更加愉快。硬单例被过度使用了。 - occulus

12

编辑:这个实现方式已经被 ARC 废弃了。请查看如何实现一个与 ARC 兼容的 Objective-C 单例?获取正确的实现。

我在其他答案中阅读到的所有 initialize 实现都有一个共同的错误。

+ (void) initialize {
  _instance = [[MySingletonClass alloc] init] // <----- Wrong!
}

+ (void) initialize {
  if (self == [MySingletonClass class]){ // <----- Correct!
      _instance = [[MySingletonClass alloc] init] 
  }
}

苹果文档建议您在初始化块中检查类类型,因为子类默认会调用初始化。存在一种不明显的情况,即子类可能通过KVO间接创建。例如,如果您在另一个类中添加以下行:

[[MySingletonClass getInstance] addObserver:self forKeyPath:@"foo" options:0 context:nil]

Objective-C会隐式地创建一个MySingletonClass的子类,从而导致第二次触发+initialize方法。

你可能认为应该在你的初始化块中隐式检查重复初始化,代码如下:

- (id) init { <----- Wrong!
   if (_instance != nil) {
      // Some hack
   }
   else {
      // Do stuff
   }
  return self;
}

但你会自食其果;或者更糟糕的是给其他开发人员自食其果的机会。

- (id) init { <----- Correct!
   NSAssert(_instance == nil, @"Duplication initialization of singleton");
   self = [super init];
   if (self){
      // Do stuff
   }
   return self;
}

简而言之,这是我的实现

@implementation MySingletonClass
static MySingletonClass * _instance;
+ (void) initialize {
   if (self == [MySingletonClass class]){
      _instance = [[MySingletonClass alloc] init];
   }
}

- (id) init {
   ZAssert (_instance == nil, @"Duplication initialization of singleton");
   self = [super init];
   if (self) {
      // Initialization
   }
   return self;
}

+ (id) getInstance {
   return _instance;
}
@end

(用我们自己的断言宏替换ZAssert;或者直接使用NSAssert。)


1
我会选择更简单的方式,避免进行初始化。 - Tom Andersen

10

9

我有一个有趣的sharedInstance变体,它是线程安全的,但在初始化后不会加锁。我还不确定是否足够可靠来修改顶部答案,但我提供它供进一步讨论:

// Volatile to make sure we are not foiled by CPU caches
static volatile ALBackendRequestManager *sharedInstance;

// There's no need to call this directly, as method swizzling in sharedInstance
// means this will get called after the singleton is initialized.
+ (MySingleton *)simpleSharedInstance
{
    return (MySingleton *)sharedInstance;
}

+ (MySingleton*)sharedInstance
{
    @synchronized(self)
    {
        if (sharedInstance == nil)
        {
            sharedInstance = [[MySingleton alloc] init];
            // Replace expensive thread-safe method 
            // with the simpler one that just returns the allocated instance.
            SEL origSel = @selector(sharedInstance);
            SEL newSel = @selector(simpleSharedInstance);
            Method origMethod = class_getClassMethod(self, origSel);
            Method newMethod = class_getClassMethod(self, newSel);
            method_exchangeImplementations(origMethod, newMethod);
        }
    }
    return (MySingleton *)sharedInstance;
}

1
+1 这真的很有趣。我可以使用 class_replaceMethodsharedInstance 转换为 simpleSharedInstance 的克隆体。这样,你就再也不用担心获取 @synchronized 锁了。 - Dave DeLong
你可以制作一个线程安全版本,初始化后不需要支付锁定成本,而无需进行大量的运行时操作。我在下面发布了一个实现。 - Louis Gerbarg
1
+1 太棒了。我就是喜欢那些可以在运行时做的事情。但在大多数情况下,这可能是过早的优化。如果我真的必须摆脱同步成本,我可能会使用Louis的无锁版本。 - Sven
时间已经过去了,现在苹果有更好的方法使用GCD来设置单例...所以其他人可能想要寻找那个。当然,如果你仍然需要支持iOS4之前的东西... - Kendall Helmstetter Gelner
没错,就是 dispatch_once。如果我想要一个单例,现在几乎所有的情况下我都会使用它。 - Kendall Helmstetter Gelner
显示剩余3条评论

6

简短回答:非常棒。

长话短说:大概是这样的......

static SomeSingleton *instance = NULL;

@implementation SomeSingleton

+ (id) instance {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (instance == NULL){
            instance = [[super allocWithZone:NULL] init];
        }
    });
    return instance;
}

+ (id) allocWithZone:(NSZone *)paramZone {
    return [[self instance] retain];
}

- (id) copyWithZone:(NSZone *)paramZone {
    return self;
}

- (id) autorelease {
    return self;
}

- (NSUInteger) retainCount {
    return NSUIntegerMax;
}

- (id) retain {
    return self;
}

@end

请确保阅读dispatch/once.h头文件,以了解正在发生的事情。在这种情况下,头文件注释比文档或手册更适用。

5
我已将单例模式封装成一个类,这样其他类就可以继承单例属性。
Singleton.h:
static id sharedInstance = nil;

#define DEFINE_SHARED_INSTANCE + (id) sharedInstance {  return [self sharedInstance:&sharedInstance]; } \
                               + (id) allocWithZone:(NSZone *)zone { return [self allocWithZone:zone forInstance:&sharedInstance]; }

@interface Singleton : NSObject {

}

+ (id) sharedInstance;
+ (id) sharedInstance:(id*)inst;

+ (id) allocWithZone:(NSZone *)zone forInstance:(id*)inst;

@end

Singleton.m:
#import "Singleton.h"


@implementation Singleton


+ (id) sharedInstance { 
    return [self sharedInstance:&sharedInstance];
}

+ (id) sharedInstance:(id*)inst {
    @synchronized(self)
    {
        if (*inst == nil)
            *inst = [[self alloc] init];
    }
    return *inst;
}

+ (id) allocWithZone:(NSZone *)zone forInstance:(id*)inst {
    @synchronized(self) {
        if (*inst == nil) {
            *inst = [super allocWithZone:zone];
            return *inst;  // assignment and return on first allocation
        }
    }
    return nil; // on subsequent allocation attempts return nil
}

- (id)copyWithZone:(NSZone *)zone {
    return self;
}

- (id)retain {
    return self;
}

- (unsigned)retainCount {
    return UINT_MAX;  // denotes an object that cannot be released
}

- (void)release {
    //do nothing
}

- (id)autorelease {
    return self;
}


@end

这里是一个你想要变成单例模式的一些类的示例。
#import "Singleton.h"

@interface SomeClass : Singleton {

}

@end

@implementation SomeClass 

DEFINE_SHARED_INSTANCE;

@end

Singleton类的唯一限制是它必须是NSObject子类。但大多数情况下,我在代码中使用的单例实际上都是NSObject子类,因此这个类确实简化了我的生活并使代码更清晰。

你可能想使用其他锁定机制,因为 @synchronized 非常缓慢并且应该避免使用。 - DarkDust

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接