iOS开发-Objective-C单例在ARC环境下的实现和理解

来源:互联网 发布:网贷数据分析 编辑:程序博客网 时间:2024/06/07 01:47
在23种设计模式里面,单例是最好理解的那一类。追MM与设计模式是这么解释的:SINGLETON—俺有6个漂亮的老婆,她们的老公都是我,我就是我们家里的老公Singleton,她们只要说道“老公”,都是指的同一个人,那就是我…

在app开发中,单例的生命周期是从创建出来到app被kill掉。也就是说singleton自初始化对象之后,直到关闭应用,才会被释放。在生命周期期间,应该无论用什么方法初始化,拿到的都应该是地址相同的同一个singleton对象。

1.对象的构造

在oc中,构造方法很多,习惯上使用share后者manager命名的类方法来创建单例,但这并不能保证每个开发都知道这个习惯。为了确保无论用什么方法创建出来的对象都是同一个,要使得所有构造方法的出口一致。

1.1 oc中的构造方法
单例的父类一般都是NSObject,在此也只讨论这种情况

查看NSObject.h 官方文档
初始化对象的方法有:

alloc

    使用方法:
You must use an init... method to complete the initialization process. For example:
TheClass *newObject = [[TheClass alloc] init];

Do not override alloc to include initialization code. Instead, implement class-specific versions of init... methods.

For historical reasons, alloc invokes allocWithZone:.

结论:alloc方法必须与init方法合用,来完成初始化过程。不要复写alloc方法,可以复写init方法来实现特别的需求。由于历史原因,alloc会调用方法+ allocWithZone: 

allocWithZone:

    使用方法:

You must use an init... method to complete the initialization process. For example:

Code Listing 3
TheClass *newObject = [[TheClass allocWithZone:nil] init];

Do not override allocWithZone: to include any initialization code. Instead, class-specific versions of init... methods.

This method exists for historical reasons; memory zones are no longer used by Objective-C.

与alloc方法类似,此方法也需要与init方法合用。内存空间不再被oc使用,所以zone参数传nil即可。

init

Implemented by subclasses to initialize a new object (the receiver) immediately after memory for it has been allocated.
由子类实现该方法,在得到内存分配之后立即初始化一个新的对象。

    使用方法:
An init message is coupled with an alloc (or allocWithZone:) message in the same line of code:
与alloc或者allocWithZone组合使用。

如果要复写,格式如下:
- (instancetype)init {    self = [super init];    if (self) {        // Initialize self    }    return self;}

copy

Returns the object returned by copyWithZone:.
该方法的返回值是从copyWithZone:返回的

copyWithZone:

This method exists so class objects can be used in situations where you need an object that conforms to the NSCopying protocol. For example, this method lets you use a class object as a key to an NSDictionary object. You should not override this method.
只有准守了NSCopying协议的类对象,才能调用该方法。例如,使用该方法可以将类对象作为字典对象的key。不能override。

mutableCopy

Returns the object returned by mutableCopyWithZone: where the zone is nil.
与 -copy 同理

mutableCopyWithZone:

This method exists so class objects can be used in situations where you need an object that conforms to the NSMutableCopying protocol. For example, this method lets you use a class object as a key to an NSDictionary object. You should not override this method.
与 -copyWithZone:同理。

new

This method is a combination of alloc and init. Like alloc, it initializes the isa instance variable of the new object so it points to the class data structure. It then invokes the initmethod to complete the initialization process.
该方法是alloc和init两个方法的结合版。和alloc方法类似,它初始化新对象的isa指针(指向类数据结构)实例变量。然后自动调用init方法来完成该实例化过程。

综上所述
单例的初始化只要保证,- alloc -init 、-new、-copy、mutableCopy、以上四个方法创建出来的对象是同一个就ok了。

Demo 代码如下 
#import <Foundation/Foundation.h>@interface SingleInstance : NSObject+ (instancetype)shareInstance;@property (nonatomic ,assign) NSInteger factor1;    //测试用@end

#import "SingleInstance.h"@implementation SingleInstancestatic SingleInstance *instance = nil;+ (instancetype)shareInstance{    static SingleInstance *instance;    static dispatch_once_t onceToken;//dispatch_once (If called simultaneously from multiple threads, this function waits synchronously until the block has completed. 由官方解释,该函数是线程安全的)    dispatch_once(&onceToken, ^{        instance = [[super allocWithZone:NULL] init];    });    return instance;}//保证从-alloc-init和-new方法返回的对象是由shareInstance返回的+ (instancetype)allocWithZone:(struct _NSZone *)zone{    return [SingleInstance shareInstance];}//保证从copy获取的对象是由shareInstance返回的- (id)copyWithZone:(struct _NSZone *)zone{    return [SingleInstance shareInstance];}//保证从mutableCopy获取的对象是由shareInstance返回的- (id)mutableCopyWithZone:(struct _NSZone *)zone{    return [SingleInstance shareInstance];}@end

以上:保证了无论用何种方式获取单例对象,获取到的都是同一个对象。

验证代码如下
    
    SingleInstance *single1 = [[SingleInstance alloc] init];    SingleInstance *single2 = [SingleInstance new];    SingleInstance *single3 = [SingleInstance shareInstance];    SingleInstance *single4 = [single1 copy];//调用此方法获取对象,需要该类重写过copyWithZone:方法,不然会crash    SingleInstance *single5 = [single1 mutableCopy];//需重写mutableCopyWithZone:,否则crash        single1.factor1 = 1;    single2.factor1 = 2;    single3.factor1 = 3;    single4.factor1 = 4;    single5.factor1 = 5;        NSLog(@"s1 value = %ld \n",single1.factor1);    NSLog(@"s2 value = %ld \n",single2.factor1);    NSLog(@"s3 value = %ld \n",single3.factor1);    NSLog(@"s4 value = %ld \n",single4.factor1);    NSLog(@"s5 value = %ld \n",single5.factor1);        NSLog(@"memory address \n %@ \n %@ \n %@  \n %@  \n %@",single1,single2,single3,single4,single5);

控制台打印结果为:
 ttt[44738:1478271] s1 value = 5
 ttt[44738:1478271] s2 value = 5
 ttt[44738:1478271] s3 value = 5
 ttt[44738:1478271] s4 value = 5
 ttt[44738:1478271] s5 value = 5
 ttt[44738:1478271] memory address
 <SingleInstance: 0x60000001c650>
 <SingleInstance: 0x60000001c650>
 <SingleInstance: 0x60000001c650>
 <SingleInstance: 0x60000001c650>
 <SingleInstance: 0x60000001c650>

可以看到,各种方法创建出来的对象的内存地址是一样的,并且属性值也是一样的。

2.线程安全问题

因为单例的对象只有一个,而且可以在应用的任何时机读写,所以很有可能在多个地方同时读写,会出现数据错乱。例如:将12306比作一个单例,a站-b站的票为单例的数据,全国有34个地方同时买a站到-b站的票。票的库存只有1张,假设同时获取12306的余票,都是1,那么都会交易成功,实际的1张票,卖出去34张,其中有33张都是并不存在的票。

转换成程序层面上就是,多线程对同一个对象进行操作,此处要有线程同步来保证数据的正确性。所谓线程同步就是:在一个线程操作的同时,其他线程只能等待,上一个线程操作完毕之后,才轮到下一个线程。

模拟代码:以火车票出售模拟
//开始模拟- (void)simulateStart {    SingleInstance *single1 = [[SingleInstance alloc] init];    single1.factor1 = 10;//总共有10张票    [self performSelectorInBackground:@selector(sellTickets) withObject:nil];    [self performSelectorInBackground:@selector(sellTickets) withObject:nil];    [self performSelectorInBackground:@selector(sellTickets) withObject:nil];}

- (void)sellTickets{    SingleInstance *single1 = [SingleInstance shareInstance];        NSThread *thread = [NSThread currentThread];    thread.name = [NSString stringWithFormat:@"%.6f",[[NSDate date] timeIntervalSince1970]];        dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{        //检查票数        for (int i = 0; i<10; i++) {            NSInteger leftTicketsCount = single1.factor1;            if (leftTicketsCount <= 0) {                NSLog(@"卖光了 \n");                break;            }            else            {                [NSThread sleepForTimeInterval:0.02];                NSInteger remain = single1.factor1;                single1.factor1--;                NSInteger left = single1.factor1;                NSLog(@"线程名:%@ 余票数 %ld , 卖出一张 , 剩余票数 %ld \n",thread.name,remain,left);            }        }    });}

控制台打印的结果为:
  线程名:1484826739.634151余票数 9 ,卖出一张 ,剩余票数 8
 
线程名:1484826739.634131余票数 10 ,卖出一张 ,剩余票数 9
 
线程名:1484826739.634141余票数 8 ,卖出一张 ,剩余票数 7
 
线程名:1484826739.634141余票数 7 ,卖出一张 ,剩余票数 6
 
线程名:1484826739.634151余票数 6 ,卖出一张 ,剩余票数 5
 
线程名:1484826739.634131余票数 5 ,卖出一张 ,剩余票数 4
 
线程名:1484826739.634141余票数 4 ,卖出一张 ,剩余票数 3
 
线程名:1484826739.634151余票数 3 ,卖出一张 ,剩余票数 2
 
线程名:1484826739.634131余票数 2 ,卖出一张 ,剩余票数 1
 
线程名:1484826739.634141余票数 1 ,卖出一张 ,剩余票数 0
 
卖光了
 
线程名:1484826739.634151余票数 0 ,卖出一张 ,剩余票数 -1
 
卖光了
 
线程名:1484826739.634131余票数 -1 ,卖出一张 ,剩余票数 -2
 
卖光了

可以看到,数据出现了错误,当票数为0时依然卖了票。因为多线程同时对同一个对象进行读写,导致获取票数的时候是正确的值,但是在卖票的过程中,有可能其他线程已经将票卖光了。
所以,在多线程读写数据的时候,要注意线程安全,保证在该线程操作数据的时候,其他线程只能看着,不能够同时读写

此处可以使用同步锁
@synchronized (<#token#>) {
        <#statements#>
    }


修改的代码如下
- (void)sellTickets{    SingleInstance *single1 = [SingleInstance shareInstance];        NSThread *thread = [NSThread currentThread];    thread.name = [NSString stringWithFormat:@"%.6f",[[NSDate date] timeIntervalSince1970]];        dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{        //注意一定要把线程对数据操作的代码放在同步锁的花括号里面        @synchronized (single1) {            //检查票数            for (int i = 0; i<11; i++) {                NSInteger leftTicketsCount = single1.factor1;                if (leftTicketsCount <= 0) {                    NSLog(@"卖光了 \n");                    break;                }                else                {                    [NSThread sleepForTimeInterval:0.02];                    NSInteger remain = single1.factor1;                    single1.factor1--;                    NSInteger left = single1.factor1;                    NSLog(@"线程名:%@ 余票数 %ld , 卖出一张 , 剩余票数 %ld \n",thread.name,remain,left);                }            }        }    });}

控制台打印数据为:
 线程名:1484827607.308329余票数 10 ,卖出一张 ,剩余票数 9
 
线程名:1484827607.308329余票数 9 ,卖出一张 ,剩余票数 8
 
线程名:1484827607.308329余票数 8 ,卖出一张 ,剩余票数 7
 
线程名:1484827607.308329余票数 7 ,卖出一张 ,剩余票数 6
 
线程名:1484827607.308329余票数 6 ,卖出一张 ,剩余票数 5
 
线程名:1484827607.308329余票数 5 ,卖出一张 ,剩余票数 4
 
线程名:1484827607.308329余票数 4 ,卖出一张 ,剩余票数 3
 
线程名:1484827607.308329余票数 3 ,卖出一张 ,剩余票数 2
 
线程名:1484827607.308329余票数 2 ,卖出一张 ,剩余票数 1
 
线程名:1484827607.308329余票数 1 ,卖出一张 ,剩余票数 0
 
卖光了
 
卖光了
 
卖光了

与预期的结果相同,没有出现数据错误。

以上为本人对OC语言ARC环境下单例的理解和一点使用,有错误的地方,或者理解不够的地方,还请看官们指出来。欢迎交流~






0 0
原创粉丝点击