如何实现一个兼容ARC的Objective-C单例?

178

如何在使用Xcode 4.2的自动引用计数(ARC)时转换(或创建)一个编译并可以正确运行的单例类?


1
我最近发现了一篇来自Matt Galloway的文章,深入探讨了单例模式在ARC和手动内存管理环境下的应用。http://www.galloway.me.uk/tutorials/singleton-classes/ - cescofry
10个回答

403

以与你之前应该一样的方式:

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

9
你只需避免使用苹果曾经在http://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CocoaFundamentals/CocoaObjects/CocoaObjects.html#//apple_ref/doc/uid/TP40002974-CH4-SW32中推荐的内存管理技巧。请确保翻译后内容通俗易懂,但不要改变原意。 - Christopher Pickslay
6
在一个方法/函数中声明的static变量与在方法/函数外声明的static变量相同,只是它们只在该方法/函数的范围内有效。每次通过+sharedInstance方法(即使在不同的线程上)运行,都将“看到”相同的sharedInstance变量。 - Nick Forge
9
如果有人调用[[MyClass alloc] init]会怎样呢?那将创建一个新对象。除了在方法外声明静态的MyClass *sharedInstance = nil,我们如何避免这种情况? - Ricardo Sanchez-Saez
1
@RicardoSánchez-Sáez 您可以删除 +alloc~ 方法以完全防止外部实例化。我已经在答案中发布了一个示例。 - eonil
2
如果另一个程序员搞砸了,调用了init而不是应该调用sharedInstance或类似的方法,那就是他们的错误。为了防止其他人可能犯错而颠覆语言的基本原则和契约似乎是不对的。更多讨论请参见http://boredzo.org/blog/archives/2009-06-17/doing-it-wrong。 - occulus
显示剩余10条评论

8
如果您需要根据需要创建其他实例,请执行以下操作:
+ (MyClass *)sharedInstance
{
    static MyClass *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[MyClass alloc] init];
        // Do any other initialisation stuff here
    });
    return sharedInstance;
}

否则,你需要这样做:
+ (id)allocWithZone:(NSZone *)zone
{
    static MyClass *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [super allocWithZone:zone];
    });
    return sharedInstance;
}

1
真/假:dispatch_once() 这一部分意味着即使在第一个示例中,您也不会获得其他实例...? - Olie
4
@Olie:错误。因为客户端代码可以执行[[MyClass alloc] init] 并绕过 sharedInstance 访问。DongXu,你应该看一下Peter Hosey的单例模式文章。如果你要重写allocWithZone:以防止创建更多实例,你还应该重写 init 以防止共享实例被重新初始化。 - jscs
2
这完全违反了allocWithZone的约定。 - occulus
有关“使用Singleton还是不使用”的更多讨论,请参见http://boredzo.org/blog/archives/2009-06-17/doing-it-wrong。 - occulus
1
单例模式只是指“在任何时候内存中只有一个对象”,这是一回事,而重新初始化则是另一回事。 - DongXu
显示剩余2条评论

5

这是适用于ARC和非ARC的版本

使用方法:

MySingletonClass.h

@interface MySingletonClass : NSObject

+(MySingletonClass *)sharedInstance;

@end

MySingletonClass.m

#import "MySingletonClass.h"
#import "SynthesizeSingleton.h"
@implementation MySingletonClass
SYNTHESIZE_SINGLETON_FOR_CLASS(MySingletonClass)
@end

2

这是我在ARC下的设计模式。 使用GCD满足新的模式,同时也满足Apple旧的实例化预防模式。

@implementation AAA
+ (id)alloc
{
    return  [self allocWithZone:nil];
}
+ (id)allocWithZone:(NSZone *)zone
{
    [self doesNotRecognizeSelector:_cmd];
    abort();
}
+ (instancetype)theController
{
    static AAA* c1  =   nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^
    {
        c1  =   [[super allocWithZone:nil] init];

        // For confirm...       
        NSLog(@"%@", NSStringFromClass([c1 class]));    //  Prints AAA
        NSLog(@"%@", @([c1 class] == self));            //  Prints 1

        Class   real_superclass_obj =   class_getSuperclass(self);
        NSLog(@"%@", @(real_superclass_obj == self));   //  Prints 0
    });

    return  c1;
}
@end

1
这不会导致 c1 成为 AAA 超类的实例吗?你需要在 self 上调用 +alloc,而不是在 super 上。 - Nick Forge
@NickForge,“super”并不意味着超类对象。你不能获取超类对象。它只是意味着将消息路由到方法的超类版本。“super”仍然指向“self”类。如果你想获取超类对象,就需要获取运行时反射函数。 - eonil
@NickForge 而 -allocWithZone: 方法只是一个简单的链到运行时分配函数的方法,以提供重载点。因此,最终,self 指针 == 当前类对象将被传递给分配器,最终 AAA 实例将被分配。 - eonil
你说得对,我忘记了super在类方法中的微妙之处。 - Nick Forge
记得使用 #import <objc/objc-runtime.h>。 - Ryan Heitner

2
中译英:

阅读这个答案,然后去阅读另一个答案。

如果你不理解Singleton是什么以及它的要求,那么你根本不会理解解决方案!

要成功创建Singleton,您必须能够执行以下3个操作:

  • 如果存在{{link1:竞争条件}},则不能同时创建多个SharedInstance实例!
  • 在多次调用之间记住并保留值。
  • 仅通过控制入口点创建一次。

dispatch_once_t通过只允许其块被调度一次来帮助您解决竞争条件问题。

{{link2:静态}}可帮助您在任意数量的调用中“记住”其值。 它如何记住? 它不允许再次创建具有相同名称的新实例,而是仅使用最初创建的实例。

不使用调用allocinit(即使我们仍然有allocinit方法,因为我们是NSObject子类,但我们不应该使用它们)在我们的sharedInstance类上,我们通过使用+(instancetype)sharedInstance来实现这一点,它仅限于被初始化一次,无论多个线程同时尝试多次,都会记住其值。
一些最常见的系统Singletons是Cocoa本身附带的:
  • [UIApplication sharedApplication]
  • [NSUserDefaults standardUserDefaults]
  • [NSFileManager defaultManager]
  • [NSBundle mainBundle]
  • [NSOperations mainQueue]
  • [NSNotificationCenter defaultCenter]
基本上,任何需要具有集中效应的东西都需要遵循某种Singleton设计模式。

1

另外,Objective-C为NSObject及其所有子类提供了+(void)initialize方法。它总是在类的任何方法之前调用。

我曾在iOS 6中设置一个断点,在堆栈帧中出现了dispatch_once。


0

单例模式:无论任何情况或途径,都不能创建多个类对象。

+ (instancetype)sharedInstance
{
    static ClassName *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[ClassName alloc] init];
        // Perform other initialisation...
    });
    return sharedInstance;
}
//    You need need to override init method as well, because developer can call [[MyClass alloc]init] method also. that time also we have to return sharedInstance only. 

-(MyClass)init
{
   return [ClassName sharedInstance];
}

1
如果有人调用init,init将调用sharedInstance,sharedInstance将再次调用init,init将第二次调用sharedInstance,然后崩溃!首先,这是一个无限递归循环。其次,第二次调用dispatch_once将崩溃,因为它不能从dispatch_once内部再次调用。 - Chuck Krutsinger

0

已经有一个已接受的答案,但它可能与您的目的相关或不相关,其中存在两个问题:

  1. 如果在init方法中再次调用sharedInstance方法(例如因为从中构造了使用单例的其他对象),将导致堆栈溢出。
  2. 对于类层次结构,只有一个单例(即在其中调用了sharedInstance方法的层次结构中的第一个类),而不是每个具体类层次结构都有一个单例。

以下代码解决了这两个问题:

+ (instancetype)sharedInstance {
    static id mutex = nil;
    static NSMutableDictionary *instances = nil;

    //Initialize the mutex and instances dictionary in a thread safe manner
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        mutex = [NSObject new];
        instances = [NSMutableDictionary new];
    });

    id instance = nil;

    //Now synchronize on the mutex
    //Note: do not synchronize on self, since self may differ depending on which class this method is called on
    @synchronized(mutex) {
        id <NSCopying> key = (id <NSCopying>)self;
        instance = instances[key];
        if (instance == nil) {
            //Break allocation and initialization into two statements to prevent a stack overflow, if init somehow calls the sharedInstance method
            id allocatedInstance = [self alloc];

            //Store the instance into the dictionary, one per concrete class (class acts as key for the dictionary)
            //Do this right after allocation to avoid the stackoverflow problem
            if (allocatedInstance != nil) {
                instances[key] = allocatedInstance;
            }
            instance = [allocatedInstance init];

            //Following code may be overly cautious
            if (instance != allocatedInstance) {
                //Somehow the init method did not return the same instance as the alloc method
                if (instance == nil) {
                    //If init returns nil: immediately remove the instance again
                    [instances removeObjectForKey:key];
                } else {
                    //Else: put the instance in the dictionary instead of the allocatedInstance
                    instances[key] = instance;
                }
            }
        }
    }
    return instance;
}

-2

如果你需要在Swift中创建单例,

class var sharedInstance: MyClass {
    struct Singleton {
        static let instance = MyClass()
    }
    return Singleton.instance
}

或者

struct Singleton {
    static let sharedInstance = MyClass()
}

class var sharedInstance: MyClass {
    return Singleton.sharedInstance
}

你可以使用这种方式

let sharedClass = LibraryAPI.sharedInstance

-2
#import <Foundation/Foundation.h>

@interface SingleTon : NSObject

@property (nonatomic,strong) NSString *name;
+(SingleTon *) theSingleTon;

@end

#import "SingleTon.h"
@implementation SingleTon

+(SingleTon *) theSingleTon{
    static SingleTon *theSingleTon = nil;

    if (!theSingleTon) {

        theSingleTon = [[super allocWithZone:nil] init
                     ];
    }
    return theSingleTon;
}

+(id)allocWithZone:(struct _NSZone *)zone{

    return [self theSingleTon];
}

-(id)init{

    self = [super init];
    if (self) {
        // Set Variables
        _name = @"Kiran";
    }

    return self;
}

@end

希望上述代码能帮到您。


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