欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS 单例

程序员文章站 2022-07-13 23:38:28
...

单例模式可能是设计模式中最简单的形式了,这一模式的意图就是使得类中的一个对象成为系统中的唯一实例。它提供了对类的对象所提供的资源的全局访问点。因此需要用一种只允许生成对象类的唯一实例的机制。

下面让我们来看下单例的作用:

  • 可以保证的程序运行过程,一个类只有一个示例,而且该实例易于供外界访问
  • 从而方便地控制了实例个数,并节约系统资源。

方法一(误)

+ (instancetype)sharedInstance
{
    static Singleton *instance = nil;
    if (!instance) {
        instance = [[Singleton alloc] init];
    }
    return instance;
}

这种方式的单例不是线程安全的。

假设此时有两条线程:线程1和线程2,都在调用shareInstance方法来创建单例,那么线程1运行到if (instance == nil)出发现instance = nil,那么就会初始化一个instance,假设此时线程2也运行到if的判断处了,此时线程1还没有创建完成实例instance,所以此时instance = nil还是成立的,那么线程2又会创建一个instace。

为了解决线程安全问题,可以使用dispatch_once、互斥锁。

方法二 (误)

static Singleton *instance = nil;
+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[Singleton alloc] init];
    });
    return instance;
}

static Singleton *instance = nil;
+ (instancetype)sharedInstance
{
    @synchronized (self) {
        if (!instance) {
            instance = [[Singleton alloc] init];
        }
    }
    return instance;
}

上面的两个方法保证了线程安全,但是不够全面。如果使用其他方式创建,能创建出不同的对象,违背了单例的设计原则。

Singleton *s = nil;
s = [Singleton sharedInstance];
NSLog(@"%@", s);
s = [[Singleton alloc] init];
NSLog(@"%@", s);
s = [Singleton new];
NSLog(@"%@", s);

打印出三个不同的地址

2016-12-21 20:46:30.414 Singleton[28843:2198096] <Singleton: 0x6000000168c0>
2016-12-21 20:46:30.415 Singleton[28843:2198096] <Singleton: 0x610000016340>
2016-12-21 20:46:30.415 Singleton[28843:2198096] <Singleton: 0x6180000164a0>

方法三(误)

为了防止别人不小心利用alloc/init方式创建示例,也为了防止别人故意为之,我们要保证不管用什么方式创建都只能是同一个实例对象,这就得重写另一个方法。

在方法二的基础上增加重写下面的方法:

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [super allocWithZone:zone];
    });
    return instance;
}

再测试,发现打印出来的地址都一样了。

但是,还没结束。

我们添加一些属性,并在-init方法中进行初始化:

@property (assign, nonatomic)int height;
@property (strong, nonatomic)NSObject *object;
@property (strong, nonatomic)NSMutableArray *array;

然后重写-description方法:

- (NSString *)description
{
    NSString *result = @"";
    result = [result stringByAppendingFormat:@"<%@: %p>",[self class], self];
    result = [result stringByAppendingFormat:@" height = %d,",self.height];
    result = [result stringByAppendingFormat:@" array = %p,",self.array];
    result = [result stringByAppendingFormat:@" object = %p,",self.object];
    return result;
}

还用上面的方法,打印结果:

2016-12-21 20:58:03.523 Singleton[29239:2252268] <Singleton: 0x608000039d00> height = 10, arrayM = 0x60800005b150, object = 0x60800000b3e0,
2016-12-21 20:58:03.523 Singleton[29239:2252268] <Singleton: 0x608000039d00> height = 10, arrayM = 0x618000052540, object = 0x61800000b430,
2016-12-21 20:58:03.524 Singleton[29239:2252268] <Singleton: 0x608000039d00> height = 10, arrayM = 0x60800004ae00, object = 0x60800000b3e0,

可以看到,尽管使用的是同一个示例,可是他们的属性却不一样。

因为尽管没有为示例重新分配内存空间,但是因为又执行了init方法,会导致property被重新初始化。

方法四

为了保证属性的初始化只执行一次,可以将属性的初始化或者默认值设置也限制只执行一次。我们这里加上dispatch_once。

+ (instancetype)sharedInstance
{
    return [[Singleton alloc] init];
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [super allocWithZone:zone];
    });
    return instance;
}

- (instancetype)init
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [super init];
        if (instance) {
            instance.height = 10;
            instance.object = [[NSObject alloc] init];
            instance.array = [[NSMutableArray alloc] init];
        }
    });
    return instance;
}

这种方式保证了单例的唯一,也保证了属性初始化的唯一。

关于线程安全

GCD的dispatch_once方式:保证程序在运行过程中只会被运行一次,那么假设此时线程1先执行shareInstance方法,创建了一个实例对象,线程2就不会再去执行dispatch_once的代码了。从而保证了只会创建一个实例对象。

互斥锁方式:会把锁内的代码当做一个任务,这个任务执行完毕前,不会被其他线程访问。

但是这种简单的互斥锁方式在每次调用单例时都会锁一次,很影响性能,单例使用越频繁,影响越大。

优化互斥锁方式

DCL(double check lock):双重检查模式是优化了的互斥锁方式,过程就是check-lock-check,是对静态变量instance的两次判空。第一次判空避免了不必要的同步,第二次判空是为了创建实例。

将上面的简单互斥锁方式修改一下:

 if (!instance) {
        @synchronized (self) {
            if (!instance) {
                instance = [super allocWithZone:zone];
            }
        }
    }
return instance;

DCL优点是资源利用率高,第一次执行时单例对象才被实例化,效率高。缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷,虽然发生的概率很小。

效率:
GCD > DCL > 简单互斥锁

使用+load或+initialize

load方法与initialize方法都会被Runtime自动调用一次,并且在Runtime情况下,这两个方法都是线程安全的。

根据这种特性,来实现单例类。

+ (void)initialize
{
    if ([self class] == [Singleton class] && instance == nil) {
        instance = [[Singleton alloc] init];
        instance.height = 10;
        instance.object = [[NSObject alloc] init];
        instance.array = [[NSMutableArray alloc] init];
    }
}

+ (instancetype)sharedInstance
{
    return instance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    if (instance == nil) {
        instance = [super allocWithZone:zone];
    }
    return instance;
}
  1. if([self class] == [Singleton class]...) 是为了保证 initialize方法只有在本类而非subclass时才执行单例初始化方法。
  2. if (... && instance == nil) 是为了防止+initialize多次调用而产生多个实例(除了Runtime调用,我们也可以显示调用+initialize方法)。经过测试,当我们将+initialize方法本身作为class的第一个方法执行时,Runtime的+initialize会被先调用(这保证了线程安全),然后我们自己显示调用的+initialize函数再被调用。 由于+initialize方法的第一次调用一定是Runtime调用,而Runtime又保证了线程安全性,因此这里只简单的检测 singalObject == nil即可。

最好不用+load来做单例是因为它是在程序被装载时调用的,可能单例所依赖的环境尚未形成,它比较适合对Class做设置。(更多关于+load和+initialize的知识,看这里)

使用宏

如果我们需要在程序中创建多个单例,那么需要在每个类中都写上一次上述代码,非常繁琐。

我们可以使用宏来封装单例的创建,这样任何类需要创建单例,只需要一行代码就搞定了。

#define SingletonH(name) + (instancetype)shared##name;

#define SingletonM(name)    \
static id instance = nil;   \
+ (instancetype)sharedInstance  \
{   \
    static dispatch_once_t onceToken;   \
    dispatch_once(&onceToken, ^{    \
        instance = [[[self class] alloc] init];  \
    }); \
    return instance;    \
}   \

其他

当然单例如果实现了NSCopying和NSMutableCopying协议,可以补充下面的方法:

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

- (id)mutableCopyWithZone:(NSZone *)zone
{
    return instance;
}

结束语

有关iOS设计模式的全部示例在这里examples

参考