您的当前位置:首页正文

「iOS」——单例模式

2024-10-31 来源:个人技术集锦

前言

在一开始学习OC的时候,我们初步接触过单例模式。在学习定时器与视图移动的控件中,我们初步意识到单例模式的重要性。对于我们需要保持的控件,使用单例可以有效的防止再次创建而造成的bug,现在我们深入学习一下单例模式。


单例模式的概念

如果一个类有且仅有一个实例,并提供一个类方法供全局调用,那么这个类就被称为单例类。
而使用这样的类,就是单例模式。

单例模式的优缺点

优点:

  • 全局访问:单例模式确保一个类只有一个实例,并提供全局访问点,方便在整个应用中共享数据或功能。
  • 节省资源:由于只创建一个实例,可以减少内存开销,避免重复创建相同对象。
  • 控制实例化:通过私有构造函数,防止外部代码创建多个实例,确保数据一致性。

缺点:

  • 隐藏依赖:使用单例可能导致代码中隐藏的依赖关系,增加了代码的耦合性,降低了可测试性。
  • 难以扩展:单例模式不易于扩展,若需要改变实例的行为,可能需要修改单例类的代码。
  • 线程安全问题:在多线程环境下,单例的实现需要特别注意线程安全,若处理不当可能导致数据不一致。

单例模式的两种模式

实现单例模式,我们需要保证永远只创建一个实例。那么创建实例的方法有四种,因此我们需要分别改写这四种方法。

  • 通过alloc init创建
  • 通过类方法创建
  • 通过copy创建
  • 通过mutableCopy 创建

懒汉模式

懒汉模式的主要特点是在需要时才会创建单例对象的实例,而不是在程序启动时就立即创建。通过延迟对象的初始化来节省资源和提高性能。
如同名字,除了在用到时“懒得创建”。


#import "Singletion.h"

@implementation Singletion
static id instance = nil;

+(instancetype)allocWithZone:(struct _NSZone *)zone
{
    if(instance == nil) {
        @synchronized (self) {
            if(instance == nil) {
                instance = [super allocWithZone:zone];
            }
        }
    }
    return instance;
}

+(instancetype)mySingletion {
    if (instance == nil) {
        @synchronized (self) {
            instance = [[self alloc] init];
        }
    }
    return instance;
}

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

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

验证代码:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import <Foundation/Foundation.h>
#import "Singletion.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        
        Singletion *singletion = [Singletion mySingletion];
        Singletion *copySingletion = [singletion copy];
        Singletion *mutableCopySingletion = [singletion mutableCopy];
        Singletion *allocSingletion = [[Singletion alloc] init];
        
        NSLog(@"%d",singletion == copySingletion);
        NSLog(@"%d",singletion == mutableCopySingletion);
        NSLog(@"%d",singletion == allocSingletion);
        NSLog(@"%d",copySingletion == mutableCopySingletion);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

结果:

饿汉模式

饿汉模式的特点是在一开始就创建单例对象的实例,提前创建单例对象,并且分配好内存空间。使用时就是将该对象拿出来。


#import "Singletion.h"

@implementation Singletion
static id instance = nil;

+ (void)load{
    instance = [[self alloc] init];
}

+(instancetype)mySingletion {
    if (instance == nil) {
        @synchronized (self) {
            instance = [[self alloc] init];
        }
    }
    return instance;
}

+(instancetype)allocWithZone:(struct _NSZone *)zone
{
    if(instance == nil) {
        @synchronized (self) {
            if(instance == nil) {
                instance = [super allocWithZone:zone];
            }
        }
    }
    return instance;
}



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

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

重点就是在程序加载开始,就创建这个单例对象。

判断代码:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import <Foundation/Foundation.h>
#import "Singletion.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        
        Singletion *singletion = [Singletion sharedInstance];
        Singletion *copySingletion = [singletion copy];
        Singletion *mutableCopySingletion = [singletion mutableCopy];
        Singletion *allocSingletion = [[Singletion alloc] init];
        
        NSLog(@"%d",singletion == copySingletion);
        NSLog(@"%d",singletion == mutableCopySingletion);
        NSLog(@"%d",singletion == allocSingletion);
        NSLog(@"%d",copySingletion == mutableCopySingletion);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

运行结果:

单例模式的写法

原本在书中的写法为:
这种方法在多线程调用时,并不能保证成功使用单例模式。

+(instancetype)mySingleton {
    if (instance == nil) {
           instance = [[self alloc] init];
       }
    return instance;
}

从而我们使用加锁,来保证多线程中也可以创建单例类

加锁写法:

+(instancetype)allocWithZone:(struct _NSZone *)zone
{
    if(instance == nil) {
        @synchronized (self) {
            if(instance == nil) {
                instance = [super allocWithZone:zone];
            }
        }
    }
    return instance;
}

还有一个GCD的写法,换一种加锁方式。
GCD写法:

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

GCD加锁相比于第二个加锁方法,在性能上更有优势。因为它省去了锁操作,代替的是大量的原子操作,直接利用了 lock 的汇编指令,靠底层 CPU 指令来支持的。

dispatch_once 主要是根据 onceToken 的值来决定怎么去执行代码。

1.当 onceToken = 0 时,线程执行 dispatch_once 的 block 中代码;

2.当 onceToken = -1 时,线程跳过 dispatch_once 的 block 中代码不执行;

3.当 onceToken 为其他值时,线程被阻塞,等待 onceToken 值改变。

当线程调用mySingleton方法时,此时 onceToken = 0,调用 block 中的代码,此时 onceToken =其他值。
当其他线程再调用 mySingleton 方法时,onceToken为其他值,线程阻塞。当 block 线程执行完 block之后,onceToken = -1,其他线程不再阻塞,跳过 block。下次再调用mySingleton方法 时, block 已经为-1,直接跳过 block。


总结

饿汉模式和懒汉模式都是实现单例模式的一种方法,各有优点。
饿汉模式用空间换取时间,先创建出来,这样再调用时就不需要花时间判断,节省运行时间。
懒汉模式用时间换空间,如果一直不需要创建这个单例,则节约了内存空间。一旦进行判断是否需要创建该实例,则会浪费判断的时间。
我们还需要关注这里通过加锁的方式创建单例,以保证线程安全。这是因为如果到了多线程的环境里,多个进程同时访问单例,该单例模式也有可能返回不同的对象。因此我们可以通过加锁和GCD模式,来实现保证线程安全。

Top