iOS设计模式 - 单例模式

IOS设计模式单例类

Made By HeYang

内容大纲:

  • 1、单例模式
  • 2、编写严格的单例
  • 3、利用单例模式优化本地存储
  • 4、重写allocWithZone实现单例
  • 5、使用异常处理强制避免init创建实例
  • 6、MRC下的单例模式,然后宏定义抽取单例类(兼顾ARC和MRC)
  • 7、附录

1.单例模式

单例模式的描述:

系统中的一个类只提供一个实例供外界使用。

下面举几个IOS开发中常用的几个单例:

[[UIApplication sharedApplication] statusBarFrame];

[NSNotificationCenter defaultCenter] addObserver:...
    
[NSUserDefaults standardUserDefaults] setObject:...
    
[NSFileManager defaultManager]

大部分单例模式的作用就是共享信息,管理中心。 缺点:破坏了策略模式当中的最少支持原则。(不懂,需要补充)

下面实现一个单例模式

UserInfoManagerCenter.h

 1 #import <Foundation/Foundation.h>
 2 
 3 @interface UserInfoManagerCenter : NSObject
 4 
 5 @property (nonatomic, strong) NSString *name;
 6 @property (nonatomic, strong) NSNumber *age;
 7 
 8 + (instancetype)managerCenter;
 9 
10 @end

UserInfoManagerCenter.m

#import "UserInfoManagerCenter.h"

@implementation UserInfoManagerCenter

+ (instancetype)managerCenter {

    static UserInfoManagerCenter *center = nil;
    
    if (center == nil) {
        
        center = [[UserInfoManagerCenter alloc] init];
    }
    
    return center;
}

@end

单例模式的定义是:一个类中应该只有一个实例 易于被外界访问。这里易于被外界访问我们做到了,但是要求产生"有且只有一个实例"这一点没有做到。因为在以上代码的这种情况下,我们可以在客户端通过:

[[UserInfoManagerCenter alloc] init];

却可以再创建新的实例对象。下面开始严格的单例模式。

2.严格的单例模式

要求:

  • 如何实现在自定义类方法中创建单例
  • 如何防止继承
  • 如何确保实例对象只出现一个

如何实现在自定义类方法中创建单例


内容关键词:static、dispatch_once_t、dispatch_once函数

我们先看下函数void dispatch_once( dispatch_once_t *predicate, dispatch_block_t block);其中第一个参数predicate,该参数是检查后面第二个参数所代表的代码块是否被调用的谓词,第二个参数则是在整个应用程序中只会被调用一次的代码块。dispach_once函数中的代码块只会被执行一次,而且还是线程安全的。 在这里作者自己创建了一个很简单的用OC控制台程序写的Person类,并使用了上面的三个创建单例类的关键词。

Person.

1 #import <Foundation/Foundation.h>
2 
3 @interface Person : NSObject
4 
5 +(Person*)getSingleton;
6 
7 @end

Person.m

 1 #import "Person.h"
 2 
 3 @implementation Person
 4 
 5 
 6 static Person *person;
 7 
 8 +(Person*)getSingleton{
 9     static dispatch_once_t once;
10     dispatch_once(&once,^{
11         person = [[Person alloc] init];
12     }
13                   );
14     return person;
15 }
16 
17 @end

main.m

 1 #import <Foundation/Foundation.h>
 2 #import "Person.h"
 3 
 4 int main(int argc, const char * argv[]) {
 5     @autoreleasepool {
 6         NSLog(@"Hello, World!");
 7         
 8         Person *p = [Person getSingleton];
 9         NSLog(@"%@",p);
10         
11         Person *p2 = [Person getSingleton];
12         NSLog(@"%@",p2);
13         
14         
15         Person *p3 = [[Person alloc] init];
16         NSLog(@"%@",p3);
17         
18     }
19     return 0;
20 }

输出结果是:

也就是说,通过类方法getSingleton可以实现单例模式,但是却是不严格的,因为可以通过调用init另外开辟内存空间并创建实例对象。所以,下面还需要继续避免init创建新的对象。

如何防止继承

首先,我们可以写一个方法或者手段用于检测当前这个类UserInfoManagerCenter调用了(instancetype)managerCenter方法,
如果是其他类(主要是子类)调用这个方法的话,会直接导致这个类崩溃。
 1 @implementation UserInfoManagerCenter
 2 
 3 + (instancetype)managerCenter {
 4     static UserInfoManagerCenter *center = nil;
 5     
 6     static dispatch_once_t predicate;
 7     dispatch_once(&predicate, ^{
 8     
 9         center = [[UserInfoManagerCenter alloc] init];
10     });
11     
12     // 防止子类使用
13     NSString *classString = NSStringFromClass([self class]);
14     if ([classString isEqualToString:@"UserInfoManagerCenter"] == NO) {
15          //当类名不是UserInfoManagerCenter的话,让程序崩溃
16         NSParameterAssert(nil);
17     }
18     
19     return center;
20 }

以上程序,实现了防止子类继承这个单例类。

如何确保实例对象只出现一个

首先来分析,以上的模式为什么实例对象可以出现好几个?
一般而言,单例类往往用在开发管理中心工具类,一个管理工具类往往是功能很复杂很丰富的类,创建一个管理工具的实例对象,系统内存开销
是很大的,所以这种管理工具类往往要求只能创建出一个实例对象。所以就需要单例模式确保创建的对象只出现一个。
创建对象实例化对象的途径其实就是两种:一种是直接通过init创建,一种是通过该类封装init创建过程的自定义方法创建(例如前面
的managerCenter方法,内部返回通过init创建的实例对象).

所以,接下来就是要确保只能通过自定义创建单例对象的方法managerCenter中创建单例类,而init的直接创建实例化对象的方法要求无效。


思考一下

这里确实需要一定的技巧,不过我们还是先拿这个简单的Person类来思考一下,需求是:有且仅有获取实例对象的类方法能创建并返回有且仅唯一的实例对象,所以需要一定的技巧要让init创建实例对象的方法无效。也就是说,只要使用init就返回nil,而用类方法第一次能返回实例对象,第二次开始都返回第一次创建的实例对象。


接下来看看怎么做到这一点的:

首先是之前不严格的单例类的核心代码:

 1 #import "Person.h"
 2 
 3 @implementation Person
 4 
 5 
 6 static Person *person = nil;
 7 
 8 +(instancetype)getSingleton{//注意这里的返回值,由 Person* 改为 instancetype
 9     static dispatch_once_t once;
10     dispatch_once(&once,^{
11         
12         person = [[Person alloc] init];
13     });
14     return person;
15 }
16 
17 - (instancetype)init
18 {
19     self = [super init];
20     if (self) {
21         
22     }
23     return self;
24 }
25 @end

另外积累知识点: [string isKindOfClass:[NSString class]] == YES 当string指针指向nil的时候,isKindOfClass返回的一定是NO,当string指针指向的是NSString对象的时候,返回的是1。

然后在以上代码的基础上进行技巧实战,实现严格的单例类,我来了:

 1 #import "Person.h"
 2 
 3 @implementation Person
 4 
 5 
 6 static Person *person = nil;
 7 
 8 +(instancetype)getSingleton{//注意这里的返回值,由 Person* 改为 instancetype
 9     static dispatch_once_t once;
10     dispatch_once(&once,^{
11         //在第一次调用这个getSingleton类方法的时候,
12     //将NSString的实例"Person"强制转换为Person对象
13         //也就是说,让Person指针指向@"Person";
14         person = (Person*)@"Person";
15         person = [[Person alloc] init];
16     });
17     return person;
18 }
19 
20 -(instancetype)init
21 {
22     //1、首先将person对象强制转为NSString实例对象
23     NSString *string = (NSString*)person;
24     //2、然后通过,是不是NSString对象,然后判断NSString是不是@"Person"实例对象
25     
26     //第一次调用init方法,那么string对象一定是nil,如果调用init之前,调用过
27 //getSingleton,那么string-> @"Person" 对象。然后你分析分析接下来的if语句逻辑
28     if (([string isKindOfClass:[NSString class]] == YES) && [string isEqualToString:@"Person"]) {
29         self = [super init];//一般情况下,这里是调用NSObject对象的初始化方法
30         if (self) {
31             //防止子类使用
32             NSString *classString = NSStringFromClass([self class]);//获取父类的名
33             //3然后拿父类名来判断,假设是子类调用这个初始化方法,那么父类就是@"UserInfoManagerCenter"
34             if ([classString isEqualToString:@"UserInfoManagerCenter"] == NO) {
35                 NSLog(@"父类是严格的单例类,子类无法创建实例对象");
36                 NSParameterAssert(nil);//让程序奔溃
37             }
38         }
39         return self;
40         
41     }else{
42         
43         return nil;
44     }
45 }
46 //那么,再来思考,调用了第二次getSingleton,init的逻辑压根就不用考虑了,因为
47 //dispatch_once会让你第二次调用getSingleton无法再一次init实例化。
48 //总结:1、getSingleton -> 2、getSingleton 因为dispatch的缘故成功单例
49 //     1、init ->  2、getSingleton 因为string -> nil 所以成功单例
50 //     1、getSingleton -> 2、init 因为 [string isKindOfClass:[NSString class]] == NO 所以成功单例
51 
52 @end

下面是我的简化使用辅助变量

 1 #import "Person.h"
 2 
 3 @implementation Person
 4 
 5 static Person* person = nil;
 6 int isCreate = 0;
 7 
 8 +(instancetype)shareInstance
 9 {
10     static dispatch_once_t once;
11     dispatch_once(&once,^{
12         isCreate = 1;
13         person = [[Person alloc] init];
14     });
15     return person;
16 }
17 
18 - (instancetype)init
19 {
20     if (isCreate == 1) {
21         self = [super init];
22         if (self) {
23             isCreate = 0;
24             NSLog(@"初始化方法中%lu",(unsigned long)isCreate);
25         }
26         return self;
27         
28     }else{
29         return nil;
30     }
31 }

因为现在一般都是ARC模式,自动引用计数模式,所以关于release的方法也重载不了,所以就不需要考虑release了。

3.利用单例模式优化本地存储

接下来会讲解通过单例模式来优化本地存储,另外还会介绍本地存储优化的很好的开源代码:FastCoding

下载地址:https://github.com/nicklockwood/FastCoding


A faster and more flexible binary file format repalcement for NSCoding,Property Lists and JSON


FastCoding是用来替换NSCoding协议,Property Lists以及JSON数据的一种方案,用FastCoding可以直接将对象转换为NSData,然后我们将NSData存储在本地。这也是优化本地存储的一个核心的地方,也就是说不用写NSCoding协议,就能把对象存储在本地来使用。


用单例设计存储数据接口 + 用单例接口隔离实现细节

拷贝NSCoding到项目里之后,记得要单独对NSCoding源文件进行取消ARC模式

什么是用单例设计鵆数据接口 其实就是实现-(void)storeValue存储数据的方法和-(id)valueWithKey根据键值获取对应数据的方法。

下面先创建一个单例StoreValue:

StoreValue.h

 1 #import <Foundation/Foundation.h>
 2 
 3 @interface StoreValue : NSObject
 4 
 5 + (StoreValue *)sharedInstance;
 6 
 7 - (void)storeValue:(id)value withKey:(NSString *)key;
 8 - (id)valueWithKey:(NSString *)key;
 9 
10 @end

StoreValue.m

 1 #import "StoreValue.h"
 2 #import "FastCoder.h"
 3 
 4 @implementation StoreValue
 5 
 6 //分享获取 存储的数据
 7 + (StoreValue *)sharedInstance {
 8 
 9     static StoreValue *storeValue = nil;
10     
11     static dispatch_once_t predicate;
12     dispatch_once(&predicate, ^{
13         
14         storeValue = [[StoreValue alloc] init];
15     });
16     
17     return storeValue;
18 }
19 //存储->添加设置键值对
20 - (void)storeValue:(id)value withKey:(NSString *)key {
21 
22     //这两个方法的作用是,如果value和key为空的话,就直接奔溃。
23     NSParameterAssert(value);
24     NSParameterAssert(key);
25     
26     //将传入的value转为NSData数据
27     NSData *data = [FastCoder dataWithRootObject:value];
28     if (data) {
29         //然后存储这个NSData数据对象
30         [[NSUserDefaults standardUserDefaults] setObject:data forKey:key];
31     }
32 }
33 //查找->根据键获取对应值
34 - (id)valueWithKey:(NSString *)key {
35 
36     NSParameterAssert(key);
37     
38     NSData *data = [[NSUserDefaults standardUserDefaults] valueForKey:key];
39     
40     return [FastCoder objectWithData:data];
41 }
42 
43 @end

在单例提供接口的基础上进行上层封装

NSObject+StoreValue.h

1 #import <Foundation/Foundation.h>
2 
3 @interface NSObject (StoreValue)
4 
5 - (void)storeValueWithKey:(NSString *)key;
6 + (id)valueByKey:(NSString *)key;
7 
8 @end

NSObject+StoreValue.m

 1 #import "NSObject+StoreValue.h"
 2 #import "StoreValue.h"
 3 
 4 @implementation NSObject (StoreValue)
 5 
 6 - (void)storeValueWithKey:(NSString *)key {
 7 
 8     [[StoreValue sharedInstance] storeValue:self withKey:key];
 9 }
10 
11 + (id)valueByKey:(NSString *)key {
12 
13     return [[StoreValue sharedInstance] valueWithKey:key];
14 }
15 
16 @end

客户端运行:

 

以上所有相关的源代码百度云盘下载地址链接: http://pan.baidu.com/s/1pJ4A54Z 密码: akec

FastCoding源文件百度云盘下载地址链接: http://pan.baidu.com/s/1dDyjsw5 密码: i5x4

4、重写allocWithZone实现单例

初始化init方法都会调用allocWithZone方法创建实例,然后我们单例模式通常会自定义类方法获取实例对象,在这个自定义的类方法中
也是需要调用init方法,也就是说,为了能够实现通过自定义创建实例的类方法或者通过init方法不管执行的先后顺序都能够实现创建且
仅有一个实例对象——单例。
 
 关键代码
+(instancetype)allocWithZone:(struct _NSZone *)zone
person = [super allocWithZone:zone];
 Tools.h
 1 #import <Foundation/Foundation.h>
 2 
 3 @interface Tools : NSObject
 4 
 5 
 6 //一般情况下创建一个单例对象都有一个与之对应的类方法
 7 //一般情况下用于创建对象的方法名称都以share或者default开头
 8 
 9 +(instancetype)shareInstance;
10 
11 @end

Tools.m

 1 #import "Tools.h"
 2 
 3 @implementation Tools
 4 
 5 
 6 +(instancetype)shareInstance{
 7     Tools *tool = [[Tools alloc] init];
 8     return tool;
 9 }
10 
11 
12 static Tools *_instance = nil;
13 +(instancetype)allocWithZone:(struct _NSZone *)zone
14 {
15     NSLog(@"当前执行了的func:%s",__func__);
16     
17     //由于所有的创建方法都会调用该方法,所以只需要在该方法中控制当前对象,只创建一次即可
18     if (_instance == nil) {
19         NSLog(@"创建了一个对象。");
20         _instance = [[super allocWithZone:zone] init];
21     }
22     return _instance;
23 }
24 
25 @end

执行结果:

 如果不是多线程,可以直接使用上面的情况,但是以上的代码在多线中可能会出现问题。下面还是通过

内容关键词:static、dispatch_once_t、dispatch_once函数

 来创建多线程也能安全的单例模式:

Tool.h

 1 #import <Foundation/Foundation.h>
 2 
 3 @interface Tools : NSObject
 4 
 5 @property NSString* name;
 6 
 7 //一般情况下创建一个单例对象都有一个与之对应的类方法
 8 //一般情况下用于创建对象的方法名称都以share或者default开头
 9 
10 +(instancetype)shareInstance;
11 
12 @end

Tool.m

 1 #import "Tools.h"
 2 
 3 @implementation Tools
 4 
 5 
 6 +(instancetype)shareInstance{
 7     Tools *tool = [[Tools alloc] init];
 8     return tool;
 9 }
10 
11 
12 static Tools *_instance = nil;
13 +(instancetype)allocWithZone:(struct _NSZone *)zone
14 {
15     NSLog(@"当前执行了的func:%s",__func__);
16     
17     
18     static dispatch_once_t once;
19     dispatch_once(&once,^{
20         NSLog(@"创建了一个对象。");
21         _instance = [[super allocWithZone:zone] init];
22         
23     });
24     
25     return _instance;
26 }
27 
28 @end

然后main函数执行:

 以上都是ARC模式下的单例,下面使用MRC模式下的单例。

 

5、使用异常处理强制避免init创建实例

 


 

6、MRC下的单例模式,然后宏定义抽取单例类(兼顾ARC和MRC)

 首先,将工程设置为MRC,因为项目开发有的时候需要使用MRC模式,比如FastCoding的使用,就需要把其取消ARC设置为MRC,因为开源代码FastCoding在MRC下执行效率最高。所以熟悉MRC下的编程,如何处理内存管理是有必要的。

Tools.h

1 #import <Foundation/Foundation.h>
2 
3 @interface Tools : NSObject<NSCopying, NSMutableCopying>
4 // 一般情况下创建一个单例对象都有一个与之对应的类方法
5 // 一般情况下用于创建单例对象的方法名称都以share开头, 或者以default开头
6 + (instancetype)shareInstance;
7 
8 @end

Tools.m

 1 #import "Tools.h"
 2 
 3 @implementation Tools
 4 
 5 /*       1、单例模式       */
 6 
 7 + (instancetype)shareInstance
 8 {
 9     Tools *instance = [[self alloc] init];
10     return instance;
11 }
12 
13 static Tools *_instance = nil;
14 + (instancetype)allocWithZone:(struct _NSZone *)zone
15 {
16     NSLog(@"%s",__FUNCTION__);
17     // 以下代码在多线程中也能保证只执行一次
18     static dispatch_once_t onceToken;
19     dispatch_once(&onceToken, ^{
20         _instance = [[super allocWithZone:zone] init];
21     });
22     return _instance;
23 }
24 
25 /*       2、不可变拷贝 和 可变拷贝 都是返回单例本身       */
26 
27 // copyWithZone方法用什么调用? 对象
28 - (id)copyWithZone:(NSZone *)zone{
29 //    Tools *t = [[[self class] allocWithZone:zone] init];
30 //    return t;
31     return _instance;
32 }
33 
34 - (id)mutableCopyWithZone:(NSZone *)zone
35 {
36 //    Tools *t = [[[self class] allocWithZone:zone] init];
37 //    return t;
38     
39     return _instance;
40 }
41 
42 /*   3、MRC模式下,需要单例一直存在,所以需要重写release、retain、retainCount方法   */
43 
44 - (oneway void)release
45 {
46     // 为保证整个程序过程中只有一份实例, \
47     所以,在这个方法中什么都不做
48 }
49 
50 - (instancetype)retain
51 {
52     return _instance;
53 }
54 
55 - (NSUInteger)retainCount
56 {
57 //    return 1;
58     // 注意: 为了方便程序员之前沟通, 一般情况下不会在单例中返回retainCount = 1
59     // 而是返回一个比较大的值,比如NSString的retainCount就会返回一个很大的值
60     return  MAXFLOAT;
61 }
62 
63 
64 @end

下面通过宏定义抽取MRC和ARC都能使用的单例模式,首先简单讲解一下宏定义的作用:

1 // 如何判断当前是ARC还是MRC?
2 // 可以在编译的时候判断当前是否是ARC
3 #if __has_feature(objc_arc)
4     NSLog(@"ARC");
5 #else
6     NSLog(@"MRC");
7 #endif
8     return 0;
9 }

宏定义实在编译期间执行的代码,也就是说,如果以上这9行代码在编译的时候,如果是ARC模式,编译期间执行这段宏定义代码之后就会变成:

1     NSLog(@"ARC");
2 
3     return 0;

然后在运行期间执行NSLog(@"ARC") ,所以就能够起到根据环境是ARC还是MRC来决定编译ARC代码还是MRC代码。下面代码进行全面的实现:

Singleton.h

 1 /**
 2  *  单例模式宏抽取
 3  *
 4  *  @description
 5  *
 6  *  1、创建一个Singleton.h头文件然后输入以下所有的文件。
 7  *  2、使用:在需要设置为单例的类中,@interface里使用interfaceSingleton(类名)
 8  *                             @implementation里使用implementationSingleton(类名)
 9  *                      这样,即可直接就将所在的类设置为单例模式
10  *
11  */
12 // 以后就可以使用interfaceSingleton来替代后面的方法声明
13 // 这里宏抽取的是在interface的单例模式方法声明
14 #define interfaceSingleton(name)  +(instancetype)share##name
15 
16 
17 
18 //这里宏抽取的是在implementation的单例模式
19 #if __has_feature(objc_arc)
20 // ARC
21 #define implementationSingleton(name)  \
22 + (instancetype)share##name \
23 { \
24 name *instance = [[self alloc] init]; \
25 return instance; \
26 } \
27 static name *_instance = nil; \
28 + (instancetype)allocWithZone:(struct _NSZone *)zone \
29 { \
30 static dispatch_once_t onceToken; \
31 dispatch_once(&onceToken, ^{ \
32 _instance = [[super allocWithZone:zone] init]; \
33 }); \
34 return _instance; \
35 } \
36 - (id)copyWithZone:(NSZone *)zone{ \
37 return _instance; \
38 } \
39 - (id)mutableCopyWithZone:(NSZone *)zone \
40 { \
41 return _instance; \
42 }
43 #else
44 // MRC
45 
46 #define implementationSingleton(name)  \
47 + (instancetype)share##name \
48 { \
49 name *instance = [[self alloc] init]; \
50 return instance; \
51 } \
52 static name *_instance = nil; \
53 + (instancetype)allocWithZone:(struct _NSZone *)zone \
54 { \
55 static dispatch_once_t onceToken; \
56 dispatch_once(&onceToken, ^{ \
57 _instance = [[super allocWithZone:zone] init]; \
58 }); \
59 return _instance; \
60 } \
61 - (id)copyWithZone:(NSZone *)zone{ \
62 return _instance; \
63 } \
64 - (id)mutableCopyWithZone:(NSZone *)zone \
65 { \
66 return _instance; \
67 } \
68 - (oneway void)release \
69 { \
70 } \
71 - (instancetype)retain \
72 { \
73 return _instance; \
74 } \
75 - (NSUInteger)retainCount \
76 { \
77 return  MAXFLOAT; \
78 }
79 #endif 
 使用这个宏定义抽取的单例,首先在下面的小实例中,以上宏定义抽取的单例模式写在Singleton.h中:
 项目开发小贴士:
在项目开发中,单例类使用比较频繁,为了提高开发效率,可以将以上的Singleton.h所有的代码都备份在XCode的代码块中,以后只要新建一个Singleton.h的空头文件,然后输入代码块的关键字,立马敲出所有的这段宏定义抽取的单例模式代码块。
 
是不是听起来很流弊呀!

7、附录

 唐巧的技术博客:http://blog.devtang.com/blog/2013/01/13/two-stage-creation-on-cocoa/ 涉及到allocWithZone方法被遗弃,如果使用的时候出现bug,这个问题也要在考虑范围之内。

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
posted @ 2015-10-29 14:38  何杨  阅读(899)  评论(3编辑  收藏  举报