【OC】单例模式

发布于:2025-09-13 ⋅ 阅读:(21) ⋅ 点赞:(0)

前言

在之前我们已经学习过单例模式的有关内容,但是只是最简单的单例,无法胜任多线程或者稍微多一点的情况便无法确定单例的唯一性,于是更深度的学习了单例模式

概念

单例模式的定义:一个类有且只有一个实例,并且自行实例化向整个系统提供。

即他用自己内部方法进行创立的唯一对象实例,并且可以被全局访问

优缺点

优点

  1. 全局访问

    单例模式就像一个全局变量,可以通过统一的入口来获取,并可以更方便的共享一些全局的数据与资源

  2. 节省资源

    只创建一次实例,避免了频繁的new/alloc

  3. 控制实例化过程

    通过私有函数和静态方法控制对象的唯一性,保持了数据的唯一性

缺点

  1. 隐藏依赖,增加耦合

    很多地方都直接访问单例,形成一种“隐形依赖”,在更改或替换单例时导致牵一发而动全身

  2. 不利于扩展与测试

    通常通过静态方法提供实例,全局固定,难以继承或替换

  3. 多线程安全问题

    在并发环境下,如果单例初始化没有处理好线程安全(比如加锁或使用 dispatch_once),可能会创建出多个实例,违背单例的初衷,尤其在懒汉式单例实现中,这一点必须特别小心。

两种使用模式

一般来说,创建一个单例之后要保证唯一实例的话要分别改写四种方法,即:用alloc init创建;通过类方法创建;通过copy创建;通过mutableCopy创建

而在改写这四种方法时按照创建时间主要分为两种,即懒汉模式和饿汉模式

懒汉模式

即在我们需要用到这个单例的时候,我们才开始创建这个唯一的实例,通过延迟对象的初始化来节省资源和提高性能,这种也是比较常用的创建单例模式的方式:

实现代码

+ (instancetype)sharedInstance {
    static Singletion *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[super allocWithZone: NULL] init];
    });
    return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    return [self sharedInstance];
}
- (id)copyWithZone:(NSZone *)zone {
    return self;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
    return self;
}
#import <Foundation/Foundation.h>
#import "Singletion.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Singletion *s1 = [Singletion sharedInstance];
        Singletion *s2 = [Singletion sharedInstance];
        Singletion *s3 = [s1 copy];
        Singletion *s4 = [s1 mutableCopy];
        
        NSLog(@"%d", s1 == s2);
        NSLog(@"%d", s1 == s3);
        NSLog(@"%d", s2 == s3);
        NSLog(@"%d", s1 == s4);
    }
    return 0;
}

这里涉及到了一个新的东西,即dispatch_once,经学长博客学习,发现其主要是按照onceToken的值来进行代码执行的

  1. onceToken = 0时,线程执行里面block中的代码
  2. onceToken = -1时,线程跳过block里的代码不执行
  3. onceToken = 其他值时,线程即会被阻塞,等待onceToken的值改变

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

运行结果

请添加图片描述

饿汉模式

饿汉模式指的是我们在一开始加载时就直接创建这个单例对象的实例,使用时再把这个对象拿出来,用到的这种方式不是特别常用,因为性能不如上面的懒汉模式

饿汉有个优点就是,因为这个实例在加载时就已经创建完成,所以其不存在多线程创建的问题,因而一般来说也不需要用dispatch_once或者加锁方法,当然用了也行,不过好像是有点多余的写法

实现代码

#import "Singletion.h"

@implementation Singletion

static Singletion* instance = nil;

+ (void)load {
    instance = [[super allocWithZone: NULL] init];
    //NSLog(@"Singleton");
}
+ (instancetype)sharedInstance {
    return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    if (!instance) {
        instance = [super allocWithZone: zone];
    }
    return instance;
}
- (id)copyWithZone:(NSZone *)zone {
    return self;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
    return self;
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Singletion *s1 = [Singletion sharedInstance];
        Singletion *s2 = [Singletion sharedInstance];
        Singletion *s3 = [s1 copy];
        Singletion *s4 = [s1 mutableCopy];
        
        NSLog(@"%d", s1 == s2);
        NSLog(@"%d", s1 == s3);
        NSLog(@"%d", s2 == s3);
        NSLog(@"%d", s1 == s4);
    }
    return 0;
}

运行结果

请添加图片描述

在自定义类方法时的几种常见写法

首先我们知道饿汉一般是不需要担心其线程安全问题的,所以一般只考虑懒汉模式的几种写法,主要有两种

在懒汉模式中一般有两种写法,分别是GCD和加锁的写法,GCD的写法是现在写法更推荐的,因为其性能极快且第一次使用后后续基本无开销,而使用加互斥锁@synchronized的方式性能较慢,且每次使用时都有锁的开销所以不常用

GCD的写法在上面已经给出,下面我给出使用加锁方式的代码:

static Singletion* instance = nil;
+ (id)sharedInstance {
    if (instance == nil) {
        @synchronized (self) {
            if (instance == nil) {
                instance = [[super allocWithZone: NULL] init];
            }
        }
    }
    return instance;
}
+ (id)allocWithZone:(struct _NSZone *)zone {
    if (instance == nil) {
        @synchronized (self) {
            if (!instance) {
                instance = [[super allocWithZone: NULL] init];
            }
        }
    }
    return instance;
}
- (id)copyWithZone:(NSZone *)zone {
    return self;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
    return self;
}

总结

总而言之,懒汉模式一般用于需要延迟加载实例的情况,可以节省资源,提高性能,但是需要考虑线程安全的问题;饿汉模式适用于需要简单实现和线程安全的情况,但是不支持延迟加载,在程序开始时便加载完成了


网站公告

今日签到

点亮在社区的每一天
去签到