iOS單例設計模式詳細講解(單例設計模式不斷完善的過程)

在iOS中有很多的設計模式,有一本書《Elements of Reusable Object-Oriented Software》(中文名字爲《設計模式》)講述了23種軟件設計模式,這本書中的設計模式都是面向對象的,很多語言都有廣泛的應用,在蘋果的開發中,當然也會存在這些設計模式,我們所使用的無論是開發Mac OX系統的Cocoa框架還是開發iOS系統的Cocoa Touch框架,裏面的設計模式也是由這23種設計模式演變而來。本文着重詳細介紹在開發iOS時採用的單例模式,從設計過程的演變和細節的完善進行分析,相信大家能夠從中獲得重要的思路原理而不是僅僅知道應該這麼寫單例模式卻不知爲何這麼寫,當然,理解透徹後,爲了我們的開發效率,我們可以將單例模式的代碼封裝到一個類中然後定義成宏,適配於ARC和MRC模式,讓開發效率大大提高。這些操作在本文中都會一一講到,接下來就進入正題。


在講述之前,先說明本文的層次結構,本文分成了5個部分,下面依次羅列
1、單例模式中懶漢式的實現
2、單例模式中餓漢式的實現
3、使用GCD代替手動加鎖判斷處理
4、非ARC情況的單例模式
5、單例模式的代碼實用化(封裝便於開發直接使用)

前言:
所謂的單例模式,就是要實現在一個應用程序中,對應的一個類只會有一個實例,無論創建多少次,都是同一個對象。大家在開發過程中也見過不少的單例,比如UIApplication、UIAccelerometer(重力加速)、NSUserDefaults、NSNotificationCenter,當然,這些是開發Cocoa Touch框架中的,在Cocoa框架中還有NSFileManager、NSBundle等。在iOS中,懶加載幾乎是無處不在的,其實,懶加載在某種意義上也是採用了單例模式的思想(如果對象存在就直接返回,對象不存在就創建對象),那麼本文就從大家熟悉的懶加載入手進行講解(整個過程都用實際的代碼進行說明)。

一、單例模式中懶漢式的實現
新建一個工程(本文是single view工程),創建一個繼承於NSObject的類,命名爲NTMoviePlayer,首先我們嘗試下使用懶加載,在viewController裏面導入NTMoviePlayer.h,定義一個NTMoviePlayer的對象,然後寫出懶加載代碼,這樣好像真的是可以做到在viewController裏面只有一個NTMoviePlayer對象,但是如果又創建一個類,然後進行同樣的操作,兩次創建的對象還會是一樣的嗎?答案很明顯,不一樣,我們可以從這個現象去推導問題發生的根本原因,那就是在不同的類中,創建NTMoviePlayer對象的時候都會進行一個alloc操作,那麼這個alloc實際上就是分配內存空間的一個操作,分配了不同的內存區域,那麼當然創建了不同的對象,所以,如果要保證應用中就只有一個對象,就應該讓NTMoviePlayer類的alloc方法只會進行一次內存空間的分配。這樣,找到了問題所在,就去實現代碼,重寫alloc方法,這裏提供了兩種方法,一種是alloc,一種是allocWithZone方法,其實在alloc調用的底層也是allocWithZone方法,所以在此,我們需要重寫allocWithZone方法:
id moviePlayer;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    if (moviePlayer == nil) {
        // 調用super的allocWithZone方法來分配內存空間
        moviePlayer = [super allocWithZone:zone];
    }
    return moviePlayer;
}
在這裏我們初步使用懶加載來控制保證只有一個單例,但是這種僅僅適合在單一線程中使用的情況,要是涉及到了多線程的話,那麼就會出現這樣的情況,當一個線程走到了if判斷時,判斷爲空,然後進入其中去創建對象,在還沒有返回的時候,另外一條線程又到了if判斷,判斷仍然爲空,於是又進入進行對象的創建,所以這樣的話就保證不了只有一個單例對象。於是,我們對代碼進行手動加鎖。
id moviePlayer;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    // 在這裏加一把鎖(利用本類爲鎖)進行多線程問題的解決
    @synchronized(self){
        if (moviePlayer == nil) {
            // 調用super的allocWithZone方法來分配內存空間
            moviePlayer = [super allocWithZone:zone];
        }
    }
    return moviePlayer;
}
這樣的話,就可以解決上述問題,但是,每一次進行alloc的時候都會加鎖和判斷鎖的存在,這一點是可以進行優化的(在java中也有對於這種情況的處理),於是在加鎖之前再次進行判斷,修改代碼如下:
id moviePlayer;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    // 在這裏判斷,爲了優化資源,防止多次加鎖和判斷鎖
    if (moviePlayer == nil) {
        // 在這裏加一把鎖(利用本類爲鎖)進行多線程問題的解決
        @synchronized(self){
            if (moviePlayer == nil) {
                // 調用super的allocWithZone方法來分配內存空間
                moviePlayer = [super allocWithZone:zone];
            }
        }
    }
    return moviePlayer;
}
到此,在allocWithZone方法中的代碼基本完善,接着,在我們進行開發中,也時常會使用到很多單例,我們在創建單例的時候都不是使用的alloc和init,而是使用的shared加上變量名這種創建方式,所以,我們自己寫單例的話,也應該向外界暴露這個方法。在.h文件中先聲明下方法
+ (instancetype)sharedMoviePlayer;
然後在.m文件中實現,邏輯上和allocWithZone方法是一樣的
+ (instancetype)sharedMoviePlayer
{
    if (moviePlayer == nil) {
        @synchronized(self){
            if (moviePlayer == nil) {
                // 在這裏寫self和寫本類名是一樣的
                moviePlayer = [[self alloc]init];
            }
        }
    }
    return moviePlayer;
}
這個對外暴露的方法完成之後,我們還需要注意一點,在使用copy這個語法的時候,是能夠創建新的對象的,如果使用copy創建出新的對象的話,那麼就不能夠保證單例的存在了,所以我們需要重寫copyWithZone方法,如果直接在.m文件中敲的話,會發現沒有提示,這是沒有聲明協議的原因,可以在.h文件中聲明NSCopying協議,然後重寫copyWithZone方法:
- (id)copyWithZone:(NSZone *)zone
{
    return moviePlayer;
}
在這裏沒有像上面兩個方法一樣實現邏輯是因爲:使用copy的前提是必須現有一個對象,然後再使用,所以既然都已經創建了一個對象了,那麼全局變量所代表的對象也就是這個單例,那麼在copyWithZone方法中直接返回就好了
到這裏,基本的代碼差不多都寫好了,還需要處理一些細節,首先,我們所聲明的全局變量是沒有使用static來修飾的,大家在開發過程中所遇見到的全局變量很多都是使用了static來修飾的,這裏進行一個小插曲,簡要說明下static的使用,有兩種用法
1、static修飾局部變量:
簡要來說,如果修飾了局部變量的話,那麼這個局部變量的生命週期就和不加static的全局變量一樣了(也就是隻有一塊內存區域,無論這個方法執行多少次,都不會進行內存的分配),不同的在於作用域仍然沒有改變
2、static修飾全局變量(這點是我們應該注意的):
如果不適用static的全局變量,我們可以在其他的類中使用extern關鍵字直接獲取到這個對象,可想而知,在我們所做的單例模式中,如果在其他類中利用extern拿到了這個對象,進行一個對象銷燬,例如:
extern id moviePlayer;
moviePlayer = nil;
這時候在這句代碼之前創建的單例就銷燬了,再次創建的對象就不是同一個了,這樣就無法保證單例的存在

所以對於全局變量的定義,需要加上static修飾符,到此,懶漢式的單例模式就寫好了(非ARC和GCD模式在後面討論),下面給出整合代碼
#import "NTMoviePlayer.h"
@implementation NTMoviePlayer
static id moviePlayer;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    // 在這裏判斷,爲了優化資源,防止多次加鎖和判斷鎖
    if (moviePlayer == nil) {
        // 在這裏加一把鎖(利用本類爲鎖)進行多線程問題的解決
        @synchronized(self){
            if (moviePlayer == nil) {
                // 調用super的allocWithZone方法來分配內存空間
                moviePlayer = [super allocWithZone:zone];
            }
        }
    }
    return moviePlayer;
}
+ (instancetype)sharedMoviePlayer
{
    if (moviePlayer == nil) {
        @synchronized(self){
            if (moviePlayer == nil) {
                // 在這裏寫self和寫本類名是一樣的
                moviePlayer = [[self alloc]init];
            }
        }
    }
    return moviePlayer;
}
- (id)copyWithZone:(NSZone *)zone
{
    return moviePlayer;
}
@end

二、單例模式中餓漢式的實現
在第一個模塊中,我們進行了單例模式中懶漢式的詳細說明,也從懶漢式的模式中知道了實現單例模式的思路,但懶漢式和餓漢式還是有很大的區別,不過是從實現原理,和代碼操作上。在這裏先介紹懶漢式和餓漢式和特點(其實這兩個名字都是很形象的)
1、懶漢式:實現原理和懶加載其實很像,如果在程序中不使用這個對象,那麼就不會創建,只有在你使用代碼創建這個對象,纔會創建。這種實現思想或者說是原理都是iOS開發中非常重要的,所以,懶漢式的單例模式也是最爲重要的,是開發中最常見的。
2、餓漢式:在沒有使用代碼去創建對象之前,這個對象已經加載好了,並且分配了內存空間,當你去使用代碼創建的時候,實際上只是將這個原本創建好的對象拿出來而已。
接下來介紹的就是餓漢式:
剛剛在分析餓漢式和懶漢式的特點時提到過,餓漢式是在使用代碼去創建對象之前就已經創建好了對象,這裏提到的使用代碼去創建對象實際上就是用alloc或者是對外暴露的shared方法,最根本上是調用了alloc方法,所以,換句話說,餓漢式也就是在我們手動寫代碼去alloc之前就已經將對象創建完畢了。此時我們就要思考了,什麼方法能夠實現這樣的效果呢?這裏介紹兩個方法,第一個是load方法,第二個是initialize方法
1、load方法:當類加載到運行環境中的時候就會調用且僅調用一次,同時注意一個類只會加載一次(類加載有別於引用類,可以這麼說,所有類都會在程序啓動的時候加載一次,不管有沒有在目前顯示的視圖類中引用到)
2、initialize方法:當第一次使用類的時候加載且僅加載一次
我們以load方法作爲示範,在工程中再次創建一個新的類NTMusicPlayer,做一些基本的相同操作,在.h文件中暴露出sharedMusicPlayer方法,在.m文件中利用static定義一個全局變量musicPlayer,接着我們需要寫出load方法
+ (void)load
{
    musicPlayer = [[self alloc]init];
}
接着我們仍然需要重寫allocWithZone方法,因爲在load方法中是用alloc來創建對象,分配內存空間的,但是在餓漢式中的邏輯就和在懶漢式中的邏輯有所區別了
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    if (musicPlayer == nil) {
        musicPlayer = [super allocWithZone:zone];
    }
    return musicPlayer;
}
在這裏,我們可以發現有簡潔了很多,去掉了多線程的問題的加鎖方案,我們來分析下原因,首先,在類被加載的時候會調用且僅調用一次load方法,而load方法裏面又調用了alloc方法,所以,第一次調用肯定是創建好了對象,而且這時候不會存在多線程問題。當我們手動去使用alloc的時候,無論如何都過不了判斷,所以也不會存在多線程的問題了。接下來需要實現shareMusicPlayer方法和copy方法
+ (instancetype)sharedMusicPlayer
{
    return musicPlayer;
}

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

代碼又變簡單,這裏連判斷都不用加,是因爲我們使用shareMusicPlayer方法和copy的時候必然全局變量是有值的,而alloc方法中不直接返回是因爲在load方法中調用了它,需要去創建一個對象
到這裏,餓漢式的講解也完成,下面是整合代碼
#import "NTMusicPlayer.h"
@implementation NTMusicPlayer
static id musicPlayer;
+ (void)load
{
    musicPlayer = [[self alloc]init];
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    if (musicPlayer == nil) {
        musicPlayer = [super allocWithZone:zone];
    }
    return musicPlayer;
}
+ (instancetype)sharedMusicPlayer
{
    return musicPlayer;
}
- (id)copyWithZone:(NSZone *)zone
{
    return musicPlayer;
}
@end

三、使用GCD代替手動加鎖判斷處理
再次新建一個類NTPicturePlayer,這裏將詳細說明適用GCD中的方法來代替我們手動加鎖的情況,還是依照慣例,在.h文件中聲明shared方法,然後在.m文件中使用static定義一個全局變量,首先,重寫alloc方法
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        picturePlayer = [[super alloc]init];
    });
    return picturePlayer;
}
dispatch_once方法是已經在方法的內部解決了多線程問題的,所以我們不用再去加鎖(開始定義了一個static常量,這句代碼不是自己寫的,敲dispatch_once有個提示的方法就會自動生成),dispatch_once在宏觀上面表示內部方法只會執行一次。接着是sharedPicturePlayer方法
+ (instancetype)sharedPicturePlayer
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        picturePlayer = [[self alloc]init];
    });
    return picturePlayer;
}
最後是copy方法的重寫
- (id)copyWithZone:(NSZone *)zone
{
    return picturePlayer;
}
這樣的話,GCD版的單例模式(這裏是懶漢模式爲例)就做好了,下面是整合代碼:
#import "NTPicturePlayer.h"
@implementation NTPicturePlayer
static id picturePlayer;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        picturePlayer = [[super alloc]init];
    });
    return picturePlayer;
}
+ (instancetype)sharedPicturePlayer
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        picturePlayer = [[self alloc]init];
    });
    return picturePlayer;
}
- (id)copyWithZone:(NSZone *)zone
{
    return picturePlayer;
}
@end
可以看出,GCD版本的單例模式比我們之前手動進行加鎖的單例模式要簡單很多,因此在實際開發中GCD版本的單例模式也是使用最多的

四、非ARC情況的單例模式
我們知道,在MRC模式也就是非ARC模式中,我們是需要手動去管理內存的,因此,我們可以使用release去將一個對象手動銷燬,那麼這樣的話,我們的創建出來的單例對象也可以被很輕易的銷燬。所以在非ARC情況下的單例模式,我們將着重將目光放到內存管理的方法上去,首先我們可以先思考下,有哪些方法是用來進行內存管理的。這裏就列舉出來了:release、retain、retainCount、autorelease。下面就分別進行重寫並說明(以上述GCD版爲例):
1、首先是release方法,我們是不希望將我們的單例對象進行銷燬掉的,那麼很簡單,重寫release(需要將環境變爲MRC,不然使用這些方法會報錯)
- (oneway void)release
{
    
}
括號中的參數是系統生成的,我們只需要將這個方法重寫,然後不在裏面寫代碼就可以了
2、retain方法:在這裏面只需要返回這個單利本身就好了,不對引用計數做任何處理
- (instancetype)retain
{
    return picturePlayer;
}
3、retainCount方法,這個方法返回的是對象的引用計數,我們已經重寫了retain方法,不希望改變單例對象的引用計數,所以在這裏返回1就好了
- (NSUInteger)retainCount
{
    return 1;
}
4、autorelease方法,對這個方法的處理和retain方法類似,我們只需要將對象本身返回,不需要進行自動釋放池的操作
- (instancetype)autorelease
{
    return picturePlayer;
}
這樣一來,在非ARC下的單例模式就寫好了,下面是整合代碼:
#import "NTPicturePlayer.h"
@implementation NTPicturePlayer
static id picturePlayer;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        picturePlayer = [[super alloc]init];
    });
    return picturePlayer;
}
+ (instancetype)sharedPicturePlayer
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        picturePlayer = [[self alloc]init];
    });
    return picturePlayer;
}
- (id)copyWithZone:(NSZone *)zone
{
    return picturePlayer;
}
- (oneway void)release
{
    
}
- (instancetype)retain
{
    return picturePlayer;
}
- (NSUInteger)retainCount
{
    return 1;
}
- (instancetype)autorelease
{
    return picturePlayer;
}
@end

五、單例模式的代碼實用化(封裝便於開發直接使用)
我們或許會有這樣的思路,將單例類放到工程中,然後讓需要實現單例的類都繼承於這個類,這個想法表面上是不錯的,但是深入一點去研究的話,就會發現,這個單例類的所有子類所創建出來的單例都是一樣的,這就未免不可行了,造成這個的原因是:在子類創建單例對象,實際上最根本上是調用了父類的alloc方法,而在父類中,只會存在一次創建對象,創建之後則是直接返回了創建好的那個單例。通俗來說,當一個子類創建單例對象的時候,調用到了父類的創建方法,獲取到了這個單例對象,但如果第二個子類再創建單例對象,調用到父類的創建方法,這時候進行的操作不再是創建新的對象,而是返回第一個子類創建的對象。所以,這種利用繼承關係來簡化的方法是不可取的。
那麼這個時候我們便可以考慮利用宏定義來進行代碼的簡化,因爲我們比較剛剛寫的三個單例類來說,代碼有很大的相似度,我們可以抽取這些代碼將他們定義成宏。在工程中創建一個專門放置宏的.h文件,創建方法是,新建文件->在iOS模塊中選擇Other->Empty->在Save As中填寫類的名字,但是要記着加後綴.h->最後點擊Create
這時候我們需要去分析下應該怎麼去抽出代碼,畢竟從剛剛所寫的三個類還是有些差別,通過比較我們可以發現,有這些地方是不同的
在.h文件中:shared後面的名字是不同的
在.m文件中:定義的全局變量名字是不同的,shared後面的名字是不同的
所以我們不能夠在宏中將這幾個地方固定下來,可以發現這幾個地方的名字都是和單例類的名字是有聯繫的,這裏我們可以使用括號和#的關聯作用來書寫宏定義
// .h文件代碼
#define NTSingletonH(name) + (instancetype)shared##name;
// .m文件代碼
#define NTSingletonM(name)\
static id instance;\
+ (instancetype)allocWithZone:(struct _NSZone *)zone\
{\
    static dispatch_once_t onceToken;\
    dispatch_once(&onceToken, ^{\
        instance = [[super alloc]init];\
    });\
    return instance;\
}\
+ (instancetype)shared##name\
{\
    static dispatch_once_t onceToken;\
    dispatch_once(&onceToken, ^{\
        instance = [[self alloc]init];\
    });\
    return instance;\
}\
- (id)copyWithZone:(NSZone *)zone\
{\
    return instance;\
}
相比之前做了一些細節的優化,首先將全局變量的名字改爲了instance,這樣對於所有的類都是可以共用的,然後利用了括號和#號的聯繫來使宏定義變的靈活,我們使用的時候在宏定義的括號中敲出我們的單例對象名字就好了(注意由於name這個屬性是直接拼接在了shared後面,所以我們在括號中寫單例的名字的時候應該將首字母大寫),最後要注意一點細節,對於很大一段代碼,直接放到宏中是不能夠識別的,所以這裏我們需要使用 \ 這個符號,這個符號表示後面的一段是屬於宏定義中的,所以我們在每條代碼前面都添加上了這個符號。
這是ARC情況下的單例模式,那麼在非ARC情況下的單例模式我們也要將其定義出來,再次用上述方法創建一個.h文件NTSingleton_MRC.h
// .h文件代碼
#define NTSingletonH(name) + (instancetype)shared##name;

// .m文件代碼
#define NTSingletonM(name)\
static id instance;\
+ (instancetype)allocWithZone:(struct _NSZone *)zone\
{\
    static dispatch_once_t onceToken;\
    dispatch_once(&onceToken, ^{\
        instance = [[super alloc]init];\
    });\
    return instance;\
}\
+ (instancetype)shared##name\
{\
    static dispatch_once_t onceToken;\
    dispatch_once(&onceToken, ^{\
        instance = [[self alloc]init];\
    });\
    return instance;\
}\
- (id)copyWithZone:(NSZone *)zone\
{\
    return instance;\
}\
- (oneway void)release\
{\
}\
- (instancetype)retain\
{\
    return instance;\
}\
- (NSUInteger)retainCount\
{\
    return 1;\
}\
- (instancetype)autorelease\
{\
    return instance;\
}
這樣,基本都做好的封裝,但是有些人仍然覺得帶上兩個類很麻煩,可不可以將兩個類封裝成一個類,答案是當然可以,我們可以通過條件編譯來進行處理,這裏簡要說明下條件編譯,條件編譯類似於if else的工作,但是原則上是有很大的不同,if else是在運行時進行處理,而條件編譯是在編譯時就進行處理,也就是說,使用條件編譯,可以去在編譯的時候檢查環境是MRC還是ARC,然後跳轉到相應的代碼進行執行.。
說到這裏,可能會想到對於MRC和ARC兩個封裝類來說,不同的地方就只是在於MRC添加了4個方法而已,那麼我們就可以這樣做,使用條件編譯將這四個方法包裝起來,檢測到ARC的時候就不執行。這種想法是好的,但是在宏定義中卻不是那麼實際,因爲在宏定義中#是有特殊的作用的,如果隨意亂使用#,就會報錯,所以我們還是老老實實在判斷中寫完兩套代碼吧,下面給出整個代碼(整個代碼可以收集,自己做一個類)
// .h文件的代碼
#define NTSingletonH(name) + (instancetype)shared##name;
// .m文件中的代碼(使用條件編譯來區別ARC和MRC)
#if __has_feature(objc_arc)

#define NTSingletonM(name)\
static id instance;\
+ (instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[super alloc]init];\
});\
return instance;\
}\
+ (instancetype)shared##name\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[self alloc]init];\
});\
return instance;\
}\
- (id)copyWithZone:(NSZone *)zone\
{\
return instance;\
}

#else

#define NTSingletonM(name)\
static id instance;\
+ (instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[super alloc]init];\
});\
return instance;\
}\
+ (instancetype)shared##name\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
instance = [[self alloc]init];\
});\
return instance;\
}\
- (id)copyWithZone:(NSZone *)zone\
{\
return instance;\
}\
- (oneway void)release\
{\
}\
- (instancetype)retain\
{\
return instance;\
}\
- (NSUInteger)retainCount\
{\
return 1;\
}\
- (instancetype)autorelease\
{\
return instance;\
}

#endif
如果你看到了這裏,我想,你對單例模式的掌握應該更深了^-^
















發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章