欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2024-01-13 23:21:52
在23种设计模式里面,单例是最好理解的那一类。追mm与设计模式是这么解释的:singleton—俺有6个漂亮的老婆,她们的老公都是我,我就是我们家里的老公singleton,她们只要说道...

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