iOS 單例

單例模式可能是設(shè)計模式中最簡單的形式了,這一模式的意圖就是使得類中的一個對象成為系統(tǒng)中的唯一實例。它提供了對類的對象所提供的資源的全局訪問點。因此需要用一種只允許生成對象類的唯一實例的機(jī)制。

下面讓我們來看下單例的作用:

  • 可以保證的程序運行過程,一個類只有一個示例,而且該實例易于供外界訪問
  • 從而方便地控制了實例個數(shù),并節(jié)約系統(tǒng)資源。

方法一(誤)

+ (instancetype)sharedInstance
{
    static Singleton *instance = nil;
    if (!instance) {
        instance = [[Singleton alloc] init];
    }
    return instance;
}

這種方式的單例不是線程安全的。

假設(shè)此時有兩條線程:線程1和線程2,都在調(diào)用shareInstance方法來創(chuàng)建單例,那么線程1運行到if (instance == nil)出發(fā)現(xiàn)instance = nil,那么就會初始化一個instance,假設(shè)此時線程2也運行到if的判斷處了,此時線程1還沒有創(chuàng)建完成實例instance,所以此時instance = nil還是成立的,那么線程2又會創(chuàng)建一個instace。

為了解決線程安全問題,可以使用dispatch_once、互斥鎖。

方法二 (誤)

static Singleton *instance = nil;
+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[Singleton alloc] init];
    });
    return instance;
}

static Singleton *instance = nil;
+ (instancetype)sharedInstance
{
    @synchronized (self) {
        if (!instance) {
            instance = [[Singleton alloc] init];
        }
    }
    return instance;
}

上面的兩個方法保證了線程安全,但是不夠全面。如果使用其他方式創(chuàng)建,能創(chuàng)建出不同的對象,違背了單例的設(shè)計原則。

Singleton *s = nil;
s = [Singleton sharedInstance];
NSLog(@"%@", s);
s = [[Singleton alloc] init];
NSLog(@"%@", s);
s = [Singleton new];
NSLog(@"%@", s);

打印出三個不同的地址

2016-12-21 20:46:30.414 Singleton[28843:2198096] <Singleton: 0x6000000168c0>
2016-12-21 20:46:30.415 Singleton[28843:2198096] <Singleton: 0x610000016340>
2016-12-21 20:46:30.415 Singleton[28843:2198096] <Singleton: 0x6180000164a0>

方法三(誤)

為了防止別人不小心利用alloc/init方式創(chuàng)建示例,也為了防止別人故意為之,我們要保證不管用什么方式創(chuàng)建都只能是同一個實例對象,這就得重寫另一個方法。

在方法二的基礎(chǔ)上增加重寫下面的方法:

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [super allocWithZone:zone];
    });
    return instance;
}

再測試,發(fā)現(xiàn)打印出來的地址都一樣了。

但是,還沒結(jié)束。

我們添加一些屬性,并在-init方法中進(jìn)行初始化:

@property (assign, nonatomic)int height;
@property (strong, nonatomic)NSObject *object;
@property (strong, nonatomic)NSMutableArray *array;

然后重寫-description方法:

- (NSString *)description
{
    NSString *result = @"";
    result = [result stringByAppendingFormat:@"<%@: %p>",[self class], self];
    result = [result stringByAppendingFormat:@" height = %d,",self.height];
    result = [result stringByAppendingFormat:@" array = %p,",self.array];
    result = [result stringByAppendingFormat:@" object = %p,",self.object];
    return result;
}

還用上面的方法,打印結(jié)果:

2016-12-21 20:58:03.523 Singleton[29239:2252268] <Singleton: 0x608000039d00> height = 10, arrayM = 0x60800005b150, object = 0x60800000b3e0,
2016-12-21 20:58:03.523 Singleton[29239:2252268] <Singleton: 0x608000039d00> height = 10, arrayM = 0x618000052540, object = 0x61800000b430,
2016-12-21 20:58:03.524 Singleton[29239:2252268] <Singleton: 0x608000039d00> height = 10, arrayM = 0x60800004ae00, object = 0x60800000b3e0,

可以看到,盡管使用的是同一個示例,可是他們的屬性卻不一樣。

因為盡管沒有為示例重新分配內(nèi)存空間,但是因為又執(zhí)行了init方法,會導(dǎo)致property被重新初始化。

方法四

為了保證屬性的初始化只執(zhí)行一次,可以將屬性的初始化或者默認(rèn)值設(shè)置也限制只執(zhí)行一次。我們這里加上dispatch_once。

+ (instancetype)sharedInstance
{
    return [[Singleton alloc] init];
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [super allocWithZone:zone];
    });
    return instance;
}

- (instancetype)init
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [super init];
        if (instance) {
            instance.height = 10;
            instance.object = [[NSObject alloc] init];
            instance.array = [[NSMutableArray alloc] init];
        }
    });
    return instance;
}

這種方式保證了單例的唯一,也保證了屬性初始化的唯一。

關(guān)于線程安全

GCD的dispatch_once方式:保證程序在運行過程中只會被運行一次,那么假設(shè)此時線程1先執(zhí)行shareInstance方法,創(chuàng)建了一個實例對象,線程2就不會再去執(zhí)行dispatch_once的代碼了。從而保證了只會創(chuàng)建一個實例對象。

互斥鎖方式:會把鎖內(nèi)的代碼當(dāng)做一個任務(wù),這個任務(wù)執(zhí)行完畢前,不會被其他線程訪問。

但是這種簡單的互斥鎖方式在每次調(diào)用單例時都會鎖一次,很影響性能,單例使用越頻繁,影響越大。

優(yōu)化互斥鎖方式

DCL(double check lock):雙重檢查模式是優(yōu)化了的互斥鎖方式,過程就是check-lock-check,是對靜態(tài)變量instance的兩次判空。第一次判空避免了不必要的同步,第二次判空是為了創(chuàng)建實例。

將上面的簡單互斥鎖方式修改一下:

 if (!instance) {
        @synchronized (self) {
            if (!instance) {
                instance = [super allocWithZone:zone];
            }
        }
    }
return instance;

DCL優(yōu)點是資源利用率高,第一次執(zhí)行時單例對象才被實例化,效率高。缺點是第一次加載時反應(yīng)稍慢一些,在高并發(fā)環(huán)境下也有一定的缺陷,雖然發(fā)生的概率很小。

效率:
GCD > DCL > 簡單互斥鎖

使用+load或+initialize

load方法與initialize方法都會被Runtime自動調(diào)用一次,并且在Runtime情況下,這兩個方法都是線程安全的。

根據(jù)這種特性,來實現(xiàn)單例類。

+ (void)initialize
{
    if ([self class] == [Singleton class] && instance == nil) {
        instance = [[Singleton alloc] init];
        instance.height = 10;
        instance.object = [[NSObject alloc] init];
        instance.array = [[NSMutableArray alloc] init];
    }
}

+ (instancetype)sharedInstance
{
    return instance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    if (instance == nil) {
        instance = [super allocWithZone:zone];
    }
    return instance;
}
  1. if([self class] == [Singleton class]...) 是為了保證 initialize方法只有在本類而非subclass時才執(zhí)行單例初始化方法。
  2. if (... && instance == nil) 是為了防止+initialize多次調(diào)用而產(chǎn)生多個實例(除了Runtime調(diào)用,我們也可以顯示調(diào)用+initialize方法)。經(jīng)過測試,當(dāng)我們將+initialize方法本身作為class的第一個方法執(zhí)行時,Runtime的+initialize會被先調(diào)用(這保證了線程安全),然后我們自己顯示調(diào)用的+initialize函數(shù)再被調(diào)用。 由于+initialize方法的第一次調(diào)用一定是Runtime調(diào)用,而Runtime又保證了線程安全性,因此這里只簡單的檢測 singalObject == nil即可。

最好不用+load來做單例是因為它是在程序被裝載時調(diào)用的,可能單例所依賴的環(huán)境尚未形成,它比較適合對Class做設(shè)置。(更多關(guān)于+load和+initialize的知識,看這里)

使用宏

如果我們需要在程序中創(chuàng)建多個單例,那么需要在每個類中都寫上一次上述代碼,非常繁瑣。

我們可以使用宏來封裝單例的創(chuàng)建,這樣任何類需要創(chuàng)建單例,只需要一行代碼就搞定了。

#define SingletonH(name) + (instancetype)shared##name;

#define SingletonM(name)    \
static id instance = nil;   \
+ (instancetype)sharedInstance  \
{   \
    static dispatch_once_t onceToken;   \
    dispatch_once(&onceToken, ^{    \
        instance = [[[self class] alloc] init];  \
    }); \
    return instance;    \
}   \

其他

當(dāng)然單例如果實現(xiàn)了NSCopying和NSMutableCopying協(xié)議,可以補充下面的方法:

- (id)copyWithZone:(NSZone *)zone
{
    return instance;
}

- (id)mutableCopyWithZone:(NSZone *)zone
{
    return instance;
}

結(jié)束語

有關(guān)iOS設(shè)計模式的全部示例在這里examples

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在開發(fā)中經(jīng)常會用到單例設(shè)計模式,目的就是為了在程序的整個生命周期內(nèi),只會創(chuàng)建一個類的實例對象,而且只要程序不被殺死...
    零度_不結(jié)冰閱讀 481評論 0 0
  • 在iOS中有很多的設(shè)計模式,有一本書《Elements of Reusable Object-Oriented S...
    鄭明明閱讀 2,620評論 3 26
  • 單例一般作為:工具類 單例命名:一般情況下如果一個類是單例,那么就要提供一個類方法用于快速創(chuàng)建單例對象,而且這個類...
    甘哲157閱讀 1,818評論 0 15
  • 在開發(fā)中經(jīng)常會用到單例設(shè)計模式,目的就是為了在程序的整個生命周期內(nèi),只會創(chuàng)建一個類的實例對象,而且只要程序不被殺死...
    Zsz丶少閱讀 437評論 0 0
  • 一、單例是什么?(apl??ke??(?)n 申請) 在 Foundation 和 Application Kit...
    甘哲157閱讀 6,267評論 6 22

友情鏈接更多精彩內(nèi)容