Objective-C 单例模式应该如何实现 init 方法?

16

我读了几篇关于 Obj-C 单例的好资源:

  1. SO 问题:你的 Objective-C 单例是什么样的?
  2. Friday Q&A:单例的创建和维护
  3. Apple 文档:创建单例实例

但是这些资源没有明确地解决 init 方法的概念,因为作为 Obj-C 的新手,我不知道应该如何实现它。

到目前为止,我知道在 Obj-C 中将 init 设为私有是不可能的,因为它不提供真正的私有方法……所以用户可以调用 [[MyClass alloc] init],而不是使用我的 [MyClass sharedInstance]

我的其他选择是什么?我相信我还应该处理我的单例子类情况。

4个回答

25
很好,一个绕过init的简单方法是不编写init方法,这样它就会调用默认的NSObject实现(该实现仅返回self)。然后,对于您的sharedInstance函数,在实例化单例时定义和调用执行类似init的工作的私有函数。(这避免了用户意外重新初始化您的单例。) 但是!!!主要问题出现在代码用户调用alloc时!对此,我个人建议沿用苹果覆盖allocWithZone:的路线...
+ (id)allocWithZone:(NSZone *)zone
{
    return [[self sharedInstance] retain];
}

这意味着用户仍将获得您的单例实例,并且他们可能会错误地使用它,就像他们分配了它一样,并且可以安全地释放它一次,因为此自定义alloc对单例执行保留。 (注意: alloc 调用 allocWithZone:,不需要单独覆盖。)
希望能有所帮助!如果您需要更多信息,请告诉我〜
编辑:扩展答案以提供示例和更多详细信息 -
考虑到Catfish_Man的答案,创建强壮的单例通常并不重要,而是在您的头文件/文档中编写一些明智的注释并放入 assert
但是,在我的情况下,我想要一个线程安全的延迟加载单例 - 也就是说,在需要使用它之前不会分配它,而不是在应用程序启动时自动分配它。在学习如何安全地实现它之后,我认为我可能会一路走到底。
编辑#2:我现在使用GCD的 dispatch_once(...)来为应用程序生命周期仅分配一次单例对象的线程安全方法。请参阅Apple Docs:GCD dispatch_once。我还添加了Apple旧单例示例中的 allocWithZone:覆盖位,并添加了一个名为 singletonInit 的私有init,以防止它被意外调用多次:
//Hidden/Private initialization
-(void)singletonInit 
{
   //your init code goes here
}

static HSCloudManager * sharedInstance = nil;   

+ (HSCloudManager *) sharedManager {                                   
    static dispatch_once_t dispatchOncePredicate = 0;                  
    dispatch_once(&dispatchOncePredicate, ^{                           
        sharedInstance = [[super allocWithZone:NULL] init];          
        [sharedInstance singletonInit];//Only place you should call singletonInit 
    });                                                                
    return sharedInstance;                                                       
}

+ (id) allocWithZone:(NSZone *)zone {
    //If coder misunderstands this is a singleton, behave properly with  
    // ref count +1 on alloc anyway, and still return singleton!
    return [[HSCloudManager sharedManager] retain];
}

HSCloudManagerNSObject 的子类,没有重写 init 方法,因此只有 NSObject 中默认的实现,根据苹果文档,其只返回 self。这意味着 [[HSCloudManager alloc] init][[[HSCloud Manager sharedManager] retain] self] 是相同的,因此它是一个安全的懒加载单例,适用于混淆用户和多线程应用程序。

至于您对用户子类化单例的担忧,我建议清晰地进行注释/文档说明。任何盲目继承而不阅读类信息的人都会遭受痛苦!

编辑#3: 为了 ARC 兼容性,只需从 allocWithZone: 重写中删除保留部分,但保留重写。


@yAaak,听起来还不错,但我觉得自己陷入了无限循环,需要消化一下;)我如何确保通过私有方法创建这样的单例的过程是线程安全的?另一个问题是:如果我遵循苹果的 allocWithZone 的想法,默认的 NSObject 的 init 将被调用(只是返回 self 还是做其他事情?)...然后再次尝试使用 allocinit 实例化,这会对我的属性/实例变量初始化和 NSObject 再次获取 init 有影响吗? - matm
yAak,你说得对:你建议的理智方法现在足够了。我会使用你的代码继续尝试单例模式 :) 我认为你的答案非常详尽,并考虑了Catfish_man的反对意见,所以我接受了它。再次感谢! - matm
我建议使用dispatch_once()而不是OSAtomic*。 鲁棒性更好,速度相同或更快。 - Catfish_Man
不错的模式!谢谢。 - Chris Hatton
如果我们调用[alloc] init],那么init方法会被调用两次(一次是由sharedManager方法实现调用,另一次是由init本身调用)?这样可以吗?基本上我看到的是[[[super allocWithZone:NULL] init] init]; - nr5
@grisleyB 是的,只要您没有在单例或其超类中实现任何 init 方法,就是安全的。NSObject 的 init 是空的。(这就是为什么我建议使用私有的 +[singletonInit] 方法。)但是,尽管如此是安全的,如果您知道它是一个单例,您也不应该调用 [[.. alloc] init] -- 而应该使用它的 +[sharedInstance] 方法。 - MechEthan

3
说实话,对我来说,编写防弹单例类的整个风气似乎有些夸张。如果您真的那么担心,只需在第一次分配给它之前插入assert(sharedInstance == nil)。这样,如果有人使用不当,它就会崩溃,让他们及时知道他们是一个白痴。

2
部分我同意,但另一半的我认为:为什么要妥协呢?不过,我会尽力实现自己能做到的,并在头文件/文档中添加清晰的注释。 - matm

2

init 方法不应受影响。在单例类和常规类中,它都是相同的。您可能希望重写 allocWithZone:(由 alloc 调用)以避免创建多个类实例。


0

为了让调用者无法使用你的单例类的init/new方法,你可以在头文件中使用NS_UNAVAILABLE宏:

- (id)init NS_UNAVAILABLE; 
+ (id)new NS_UNAVAILABLE; 

+ (instancetype)sharedInstance;

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