iOS开发-Objective-C单例在ARC环境下的实现和理解
在23种设计模式里面,单例是最好理解的那一类。追mm与设计模式是这么解释的:singleton—俺有6个漂亮的老婆,她们的老公都是我,我就是我们家里的老公singleton,她们只要说道“老公”,都是指的同一个人,那就是我…
在app开发中,单例的生命周期是从创建出来到app被kill掉。也就是说singleton自初始化对象之后,直到关闭应用,才会被释放。在生命周期期间,应该无论用什么方法初始化,拿到的都应该是地址相同的同一个singleton对象。
1.对象的构造
在oc中,构造方法很多,习惯上使用share后者manager命名的类方法来创建单例,但这并不能保证每个开发都知道这个习惯。为了确保无论用什么方法创建出来的对象都是同一个,要使得所有构造方法的出口一致。
1.1 oc中的构造方法
单例的父类一般都是nsobject,在此也只讨论这种情况
查看nsobject.h 官方文档
初始化对象的方法有:
使用方法:
you must use aninit...method to complete the initialization process. for example:
theclass *newobject = [[theclass alloc] init];
do not overrideallocto include initialization code. instead, implement class-specific versions ofinit...methods.
for historical reasons,allocinvokesallocwithzone:.
结论:alloc方法必须与init方法合用,来完成初始化过程。不要复写alloc方法,可以复写init方法来实现特别的需求。由于历史原因,alloc会调用方法+allocwithzone:
使用方法:
you must use aninit...method to complete the initialization process. for example:
code listing 3
theclass *newobject = [[theclass allocwithzone:nil] init];
do not overrideallocwithzone:to include any initialization code. instead, class-specific versions ofinit...methods.
this method exists for historical reasons; memory zones are no longer used by objective-c.
与alloc方法类似,此方法也需要与init方法合用。内存空间不再被oc使用,所以zone参数传nil即可。
implemented by subclasses to initialize a new object (the receiver) immediately after memory for it has been allocated.
由子类实现该方法,在得到内存分配之后立即初始化一个新的对象。
使用方法:
aninitmessage is coupled with an(orallocwithzone:) message in the same line of code:
与alloc或者allocwithzone组合使用。
如果要复写,格式如下:
- (instancetype)init { self = [super init]; if (self) { // initialize self } return self; }
returns the object returned bycopywithzone:.
该方法的返回值是从copywithzone:返回的
this method exists so class objects can be used in situations where you need an object that conforms to thenscopyingprotocol. for example, this method lets you use a class object as a key to annsdictionaryobject. you should not override this method.
只有准守了nscopying协议的类对象,才能调用该方法。例如,使用该方法可以将类对象作为字典对象的key。不能override。
returns the object returned bymutablecopywithzone:where the zone isnil.
与 -copy 同理
this method exists so class objects can be used in situations where you need an object that conforms to thensmutablecopyingprotocol. for example, this method lets you use a class object as a key to annsdictionaryobject. you should not override this method.
与 -copywithzone:同理。
this method is a combination ofand. like, it initializes theisainstance variable of the new object so it points to the class data structure. it then invokes themethod to complete the initialization process.
该方法是alloc和init两个方法的结合版。和alloc方法类似,它初始化新对象的isa指针(指向类数据结构)实例变量。然后自动调用init方法来完成该实例化过程。
综上所述
单例的初始化只要保证,- alloc -init 、-new、-copy、mutablecopy、以上四个方法创建出来的对象是同一个就ok了。
demo 代码如下
#import @interface singleinstance : nsobject + (instancetype)shareinstance; @property (nonatomic ,assign) nsinteger factor1; //测试用 @end
#import "singleinstance.h" @implementation singleinstance static 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
可以看到,各种方法创建出来的对象的内存地址是一样的,并且属性值也是一样的。
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环境下单例的理解和一点使用,有错误的地方,或者理解不够的地方,还请看官们指出来。欢迎交流~