【iOS】单例模式

【iOS】单例模式

什么是单例模式?

定义

单例模式,简单地说就是一个类只对应一个对象,每次使用这个类时,都只能获取到那一个对象。它的详细定义如下:

如果一个类始终只能创建一个实例,则这个类被称为单例类。 有一个全局的接口来访问这个实例。当第一次载入的时候,它通常使用延时加载的方法创建单一实例。 在程序中,一个单例类在程序中只能初始化一次,为了保证在使用中始终都是存在的,所以单例是在存储器的全局区域,在编译时分配内存,只要程序还在运行就会一直占用内存,在APP结束后由系统释放这部分内存 系统方法中,有未知的自动释放池,如果一个对象进行自动释放的话,又可能进入未知的自动释放池,出现内存问题。这就是单例模式不能自动释放的原因

特点

1.单例是一个类,创造出来的对象叫单例对象

2.单例对象使用类方法创建

3.单例一旦被创建,程序结束才会释放,也就是说,单例只初始化一次

4.单例不用程序员管理内存,内存随程序关闭而释放

为什么使用单例模式?

有时候我们需要一个全局的对象,而且要保证全局有且只有一份即可,这时候就需要用到单例设计模式,需要注意:在多线程的环境下做好线程保护。 一般在程序中,经常调用的类,如工具类、公共跳转类等都会采用单例模式

常见的单例模式

在OC语言中,有一些原生的单例类

UIApplication 应用程序实例类
NSNotificationCenter 消息中心类
NSFileManager 文件管理类
NSUserDefaults 应用程序设置
NSURLCache 请求缓存类
NSHTTPCookieStorage 应用程序cookies池

如何使用单例模式?

static关键字

在了解如何使用单例模式之前,我们先来了解一个与之相关的属性修饰关键字------static

static关键字用于修饰局部变量、全局变量和函数。修饰局部变量时,表示该局部变量存储在静态区,修饰全局变量时,表示限制该全局变量只能在当前文件中访问,修饰函数时,则表示限制函数只能在当前源文件中使用。

静态内存存储区在整个程序运行期间都存在

我们需要用static关键字来修饰单例变量,将变量限制在当前类的文件,否则在其他类中可以使用extern来拿到这个单例并修改

创建单例

我们来试着创建一个单例

objectivec 复制代码
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface singleton : NSObject
+ (id)instance;
@end

NS_ASSUME_NONNULL_END
  
  
#import "singleton.h"

@implementation singleton
static id instance = nil;

+ (id)instance {
    if (!instance) {
        instance = [[super allocWithZone:NULL] init];
    }
    return instance;
}

@end
  
  
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "singleton.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        NSLog(@"singleton:%d",[singleton instance] == [singleton instance]);
        NSLog(@"%d",[singleton instance] == [[singleton alloc] init]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

运行后得到以下结果:

我们发现,当使用自定义的类方法创建对象时,的确实现了我们想要的效果,创建出了单例。但是,当使用alloc init创建对象时,得到的并不是单例,与使用类方法创建的对象也并不相同。

这个时候我们就需要重写alloc init方法,来保证无论使用什么方法创建,这个类始终只有一个对象。这时我们只需重写allocWithZone:方法就可以了。

我们平时在初始化对象时使用的方法[[Class alloc] init],其实做了两件事,alloc方法为对象分配内存空间,init是对对象的初始化(包括设置成员变量初值等工作)。而给对象分配内存空间,除了alloc方法,还有一个方法就是allocWithZone:。使用alloc方法时,其实也是调用了allocWithZone方法的,所以只需要在allocWithZone里重写,就可以覆盖所有会生成新的实例的方法

这里的allocWithZone:方法,它的参数是被忽略的,正确的做法是传nil或者NULL。

此外,为了防止copy方法生成新的实例,还要重写copyWithZone和mutableCopyWithZone。

objectivec 复制代码
​#import "singleton.h"

@implementation singleton
static id instance = nil;

+ (id)instance {
    if (!instance) {
        instance = [[super allocWithZone:NULL] init];
    }
    return instance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone{
    return instance;
}
-(id)copyWithZone:(NSZone *)zone{
    return instance;
}
 
-(id)mutableCopyWithZone:(NSZone *)zone{
    return instance;
}

@end

懒汉模式

创建单例有两种模式,分别是懒汉模式和饿汉模式。这两种模式的区别就在于在什么时候去创建对象,懒汉模式就是在第一次用到类的时候才会实例化,饿汉模式就是在类加载的时候就进行实例化。下面先介绍一下懒汉模式

懒汉模式一般用于访问量较小的时候,用时间换空间。通俗地说,懒汉模式就像一个大懒虫,这个懒虫现在有很多穿过的脏衣服,但是他却不打算洗这些衣服,等到实在没衣服穿了,他才会去洗这些衣服。

在使用懒汉模式的时候,要注意保证线程的安全性。

多线程和单线程

线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,也就是说不同的线程可以执行同样的函数。

多线程是指程序中包含多个执行流,也就是在一个程序中可以同时运行多个不同的线程来执行不同的任务

加锁

在单线程中,我们上面所用到的创建单例的方法其实就是懒汉模式,可以直接使用上面的方法来创建懒汉模式,而在多线程中,为了确保线程安全,我们可以用@synchronized来加锁,@synchronized可以防止不同的线程同时执行同一段代码,因此加锁后每次访问该代码就都只能一个行程进行访问。

objectivec 复制代码
#import "FirstMethod.h"

@implementation FirstMethod
static id instance = nil;
+ (instancetype)instance {
   @synchronized (self) {
       if (!instance) {
           instance = [[super allocWithZone:NULL] init];
       }
   }
   return instance;
}
@end

这一段代码可以在确保线程安全的情况下实现单例,但是我们细看会发现调用insrance方法后,如果instance已经存在,可以直接将instance作为返回值传递出去,没有必要进入锁。于是我们加锁之前先做一次判断

objectivec 复制代码
#import "FirstMethod.h"

@implementation FirstMethod
static id instance = nil;
+ (instancetype)instance {
    if (!instance) {
        @synchronized (self) {
            if (!instance) {
                instance = [[super allocWithZone:NULL] init];
            }
        }
    }
    return instance;
}
@end

**注意:**这里内层的if不能省略,因为如果省略了内层的if,只是对对象的创建和初始化加了锁,而不是对创建单例过程加锁,也就是说有可能会有两个线程同时进入外层的if,然后先后一个一个执行alloc init,这样就可能导致创建出两个不一样的对象。

GCD的dispatch_once

GCD:全称是Grand Central Dispatch,可译为"牛逼的中枢调度器",是苹果公司为多核的并行运算提出的解决方案,会自动利用更多的CPU内核(比如双核、四核),自动管理线程的生命周期(创建线程、调度任务、销毁线程),程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。

另一种确保线程安全的方法就是使用GCD的dispatch_once来创建单例

objectivec 复制代码
#import "singleton.h"

@implementation singleton
static id instance = nil;

+ (id)instance {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[super alloc] init];
    });
    return instance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone{
    return instance;
}
@end

下面解释一下dispatch_once是怎样确保线程安全并创建单例的。

我们看到上述代码中出现了dispatch_once_t变量和dispatch_once函数

dispatch_once_t变量

这里的intptr_t长度是所在平台的位数,用来存放地址。

dispatch_once函数

dispatch_once无论单线程还是多线程,都只执行一次,在安全的前提下也保证了性能,同时实现了单例的创建。

当onceToken为0时,线程执行dispatch_once的block中的代码

当onceToken为-1时,线程跳过dispatch_once的block中的代码

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

饿汉模式

饿汉模式就是像一个饿汉,在类加载时就完成这个单例的创建和初始化。

以下是饿汉模式的代码:

objectivec 复制代码
#import "singleton.h"

@implementation singleton
static id instance = nil;

+ (void)load {
    [super load];
    instance = [[self allocWithZone:NULL] init];
}

+ (id)instance {
    instance = [[self allocWithZone:NULL] init];
    return instance;
}
@end

load方法:当类加载到运行环境中时调用且仅调用一次,同时一个类只会加载一次(程序启动时,所有类加载一次,不管有没有在当前视图中用到)

相关推荐
legend_jz1 分钟前
【Linux】线程控制
linux·服务器·开发语言·c++·笔记·学习·学习方法
Komorebi.py2 分钟前
【Linux】-学习笔记04
linux·笔记·学习
weiabc1 小时前
学习electron
javascript·学习·electron
fengbizhe1 小时前
笔试-笔记2
c++·笔记
余为民同志1 小时前
mini-lsm通关笔记Week2Day4
笔记
墨染风华不染尘1 小时前
python之开发笔记
开发语言·笔记·python
徐霞客3201 小时前
Qt入门1——认识Qt的几个常用头文件和常用函数
开发语言·c++·笔记·qt
HackKong2 小时前
小白怎样入门网络安全?
网络·学习·安全·web安全·网络安全·黑客
Jinkey2 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
澜世2 小时前
2024小迪安全基础入门第三课
网络·笔记·安全·网络安全