系列文章目录
第一章 OC之单例模式
第二章 OC之对象初始化
第三章 OC之类和协议
第四章 OC之main函数的操作和一些基础概念
前言
提示:本篇我们谈一下关于单例模式
一些关于单例的概念
定义:
如果一个类始终只能创建一个实例,则这个类被称为单例类。
有一个全局的接口来访问这个实例。当第一次载入的时候,它通常使用延时加载的方法创建单一实例。
在程序中,一个单例类在程序中只能初始化一次,为了保证在使用中始终都是存在的,所以单例是在存储器的全局区域,在编译时分配内存,只要程序还在运行就会一直占用内存,在APP结束后由系统释放这部分内存
系统方法中,有未知的自动释放池,如果一个对象进行自动释放的话,又可能进入未知的自动释放池,出现内存问题。这就是单例模式不能自动释放的原因
为什么要使用单例模式?
有时候我们需要一个全局的对象,而且要保证全局有且只有一份即可,这时候就需要用到单例设计模式,需要注意:在多线程的环境下做好线程保护。
一般在程序中,经常调用的类,如工具类、公共跳转类等都会采用单例模式
单例模式的结构:
单例类:包含一个实例且能自行创建这个实例的类
访问类:使用单例的类
单例模式的使用场景:
在整个应用程序中,共享一份资源,这份资源只需初始化一次即可。
单例模式的特点:
- 单例是一个类,创造出来的对象叫单例对象
- 单例对象使用类方法创建
- 单例一旦被创造出来,一直到程序结束才会释放。就是只初始化一次
- 不用程序员管理内存,随程序的关闭释放
static
在创建单例模式前,我们先来了解一下static关键字
static关键字只能修饰局部变量全局变量和函数
static修饰局部变量表示将该局部变量存储在静态区
修饰全局变量表示限制该全局变量只能在当前文件中访问
修饰函数用于限制函数只能在当前源文件中使用
特点
内存存储区:静态内存存储区在整个项目工程程序运行期间都存在
优点:
- 全区变量
不会被其他分支文件远程访问,修改
在其他文件中可以使用和static关键字修饰的相同字段,不会冲突
- 局部变量
作用域仍为局部作用域,当定义该静态局部变量符号或局部函数语句块结束时,作用域随之结束、
- 函数
静态函数不能被项目工程中其他文件所调用
单例类
UIApplication | 应用程序实例类 |
---|---|
NSNotificationCenter | 消息中心类 |
NSFileManager | 文件管理类 |
NSUserDefaults | 应用程序设置 |
NSURLCache | 请求缓存类 |
NSHTTPCookieStorage | 应用程序cookies池 |
创建
一个小例子:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FirstMethod : NSObject
+ (id)instance;
@end
NS_ASSUME_NONNULL_END
#import "FirstMethod.h"
@implementation FirstMethod
static id instance = nil;
+ (id)instance{
if (!instance){
instance = [[super allocWithZone:NULL] init];
}
return instance;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"FirstMethod:%d",[FirstMethod instance] == [FirstMethod instance]);
NSLog(@"%d",[[FirstMethod alloc]init] == [FirstMethod instance]);
return 0;
}
但是当我们测试一下却发现:
这种不能算是真正意义上的单例 ,因为类方法和alloc init创建的单例分配的内存可能不一样
因为alloc会执行allocWithZone,所以如果想只分配一次内存就要重写此方法,同时为了严谨,防止copy出现以上问题,还要重写copyWithZone,mutableCopyWithZone
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
return instance;
}
-(id)copyWithZone:(NSZone *)zone{
return instance;
}
-(id)mutableCopyWithZone:(NSZone *)zone{
return instance;
}
重写之后,我们可以看到两次创建的对象是一个对象
懒汉式和饿汉式
定义:
- 懒汉式:第一次用到类的时候才会去实例化。
- 饿汉式:在类的加载的时候就去实例化。
特点:
- 在访问量较大,或者访问线程较多时,才用饿汉式实现。以空间换时间。
- 访问量较小时,才用懒汉式,以时间换空间。
多线程和单线程:
线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,
即不同的线程可以执行同样的函数。
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,
也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
懒汉式
- 不使用GCD
这种方法适用单线程下的创建,但如果在多线程下,可能出现多个线程同时进入if语句,创造出多个对象
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FirstMethod : NSObject
+ (id)instance;
@end
NS_ASSUME_NONNULL_END
#import "FirstMethod.h"
@implementation FirstMethod
static id instance = nil;
+ (id)instance{
if (!instance){
instance = [[super allocWithZone:NULL] init];
}
return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
return instance;
}
@end
allocWithZone:方法。
我们需要保证单例类只有一个唯一的实例,而平时我们在初始化一个对象的时候, [[Class alloc] init],其实是做了两件事。 alloc 给对象分配内存空间,init是对对象的初始化,包括设置成员变量初值这些工作。而给对象分配空间,除了alloc方法之外,还有另一个方法: allocWithZone。
在NSObject 这个类的官方文档里面,allocWithZone方法介绍说,该方法的参数是被忽略的,正确的做法是传nil或者NULL参数给它。
使用alloc方法初始化一个类的实例的时候,默认是调用了 allocWithZone 的方法。于是覆盖allocWithZone方法的原因已经很明显了:为了保持单例类实例的唯一性,需要覆盖所有会生成新的实例的方法,如果有人初始化这个单例类的时候不走[[Class alloc] init] ,而是直接 allocWithZone, 那么这个单例就不再是单例了,所以必须把这个方法也堵上。
我们试试使用@synchronized来给它加锁
@synchronized 可以防止不同的线程同时执行同一段代码,加锁后每次访问该代码都只能一个线程进行访问
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FirstMethod : NSObject
//+ (id)instance;
+ (instancetype)instance2;
@end
NS_ASSUME_NONNULL_END
#import "FirstMethod.h"
@implementation FirstMethod
static id instance = nil;
+ (instancetype)instance2 {
@synchronized (self) {
if (!instance) {
instance = [[super allocWithZone:NULL] init];
}
}
return instance;
}
@end
但是这样的话,不论有没有创建该类的单例对象,都会锁一次,在次优化代码
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FirstMethod : NSObject
//+ (id)instance;
//+ (instancetype)instance2;
+ (instancetype)instance3;
@end
NS_ASSUME_NONNULL_END
#import "FirstMethod.h"
@implementation FirstMethod
static id instance = nil;
+ (instancetype)instance3 {
if (!instance) {
@synchronized (self) {
if (!instance) {
instance = [[super allocWithZone:NULL] init];
}
}
}
return instance;
}
@end
- 使用GCD的dispatch_once
在iOS开发中,我们也经常使用dispatch_once去定义一个单例类,来保证对象的唯一性。
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SecondMethod : NSObject
+ (instancetype)GCDInstance;
@end
NS_ASSUME_NONNULL_END
#import "SecondMethod.h"
@implementation SecondMethod
static id instance = nil;
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
return instance;
}
+ (instancetype)GCDInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
instance = [[self alloc]init];
});
return instance;
}
@end
dispatch_once是怎样在多线程的情况下保证生成对象的唯一的?
上边的代码中涉及到了dispatch_once_t变量和dispatch_once函数
dispatch_once_t变量
intptr_t是为了跨平台,其长度总是所在平台的位数,所以用来存放地址。其实就是一个long。所以dispatch_once_t实际上就是一个long
dispatch_once函数
将onceToken 值进行修改,其默认是0执行完后变为-1,然后每次进行判断这个值,为-1 就返回,同时做了一些编译期的优化
- dispatch_once无论使用多线程还是单线程,都只执行一次,在安全的前提下也保证了性能。
- 当onceToken为0时,线程执行dispatch_once的block中的代码
- 当onceToken为-1时,线程跳过dispatch_once的block中的代码
- 当onceToken为其他值时,线程被阻塞,等待onceToken值改变
当调用函数+ (instancetype)shareInstance;时,此时onceToken中的值为0,执行dispatch_once的block中的代码,onceToken中的值为其他值;
这时如果其他线程再次调用函数+ (instancetype)instance,由于onceToken中的值为其他值,线程被阻塞;
当block线程执行完block后,onceToken中的值变为-1,其他线程不在阻塞
再次调用函数+ (instancetype)instance时,onceToken为-1,跳过dispath_once中的block代码
饿汉式
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ThirdMethod : NSObject
+ (instancetype)HungryMethod;
@end
NS_ASSUME_NONNULL_END
#import "ThirdMethod.h"
@implementation ThirdMethod
static id instance;
+ (void)load {
[super load];
instance = [[self allocWithZone:NULL] init];
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
return instance;
}
@end
load方法:当类加载到运行环境中的时候就会调用且仅调用一次,同时注意一个类只会加载一次(类加载有别于引用类,可以这么说,所有类都会在程序启动的时候加载一次,不管有没有在目前显示的视图类中引用到
单例模式的优缺点
优点:
- 提供了对唯一实例的受控访问
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,提高系统性能
- 允许可变数量的实例
缺点:
- 由于单例模式中没有抽象层,无法继承,不能被重写或拓展(可以使用分类)
- 单例类的职责中,在一定程度上违背了“单一职责原则”
- 滥用单例类将带来一些负面问题
如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失