当前位置: 移动技术网 > 移动技术>移动开发>IOS > iOS开发-Objective-C单例在ARC环境下的实现和理解

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

2018年10月22日  | 移动技术网移动技术  | 我要评论

在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:

 

+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:返回的

 

+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。

 

-mutablecopy

 

returns the object returned bymutablecopywithzone:where the zone isnil.

与 -copy 同理

 

+mutablecopywithzone:

 

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环境下单例的理解和一点使用,有错误的地方,或者理解不够的地方,还请看官们指出来。欢迎交流~

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网