@sychronized和dispatch_once,以及對單例的思考

在iOS開發(fā)中,經(jīng)常使用到單例。單例是Cocoa中被廣泛使用的設(shè)計(jì)模式之一。單例使得某個(gè)類在整個(gè)application的生命周期中只有一個(gè)實(shí)例,減少內(nèi)存開銷,統(tǒng)一了某些具體操作的邏輯,方便管理。開發(fā)中常用構(gòu)造單例的方法有兩種@sychronizeddispatch_once

@sychronized

使用@sychronized構(gòu)造單例通常如下:

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

@sychronized是一個(gè)編譯器指令,方便我們對臨界區(qū)提供互斥鎖(mutex locks )。也就是說在多線程并發(fā)訪問臨界區(qū)(@synchronized(self) {//臨界區(qū)})時(shí),它保證同一時(shí)刻只有一個(gè)線程處于臨界區(qū)中,其他線程阻塞等待。那么@sychronized如何標(biāo)識一個(gè)互斥鎖?蘋果文檔中說了,@sychronized可以使用任意的Objective-C對象作為互斥鎖的標(biāo)識符(lock token)。那么問題來了,如果互斥鎖的標(biāo)識符不一樣呢(動態(tài)的)?比如下面這樣:

- (void)instantMethod:(id)lockTocken
{
    @synchronized(lockTocken)
    {
        //臨界區(qū)
    }
}

假設(shè)現(xiàn)在有多個(gè)線程并發(fā)調(diào)用上面的實(shí)例方法- (void)instantMethod:(id)lockTocken;,它們分別為A、B、C和D線程,其中A、B調(diào)用該方法時(shí)候傳入的lockTocken一樣,C、D傳入的lockTocken一樣。那么答案是只有標(biāo)識變量(lockTocken)一樣的線程才會互斥,標(biāo)識變量(lockTocken)不一樣的線程相互之間沒有影響。回到最早的例子,其中使用了self(類對象)作為互斥鎖的標(biāo)識符,由此可見,多進(jìn)程并發(fā)訪問,使用的互斥鎖是一樣的,并且在第一個(gè)進(jìn)入臨界區(qū)的線程初始化instance后,其后進(jìn)入的線程就不會再次初始化(instance不再是nil),保證了SomeClass類只有一個(gè)實(shí)例。

好奇的你一定會想,既然@synchronized是編譯器指令,那么編譯器對這段代碼做了什么?

有如下代碼:

id obj = ...
@synchronized(obj) {
    //臨界區(qū)
}

clang -rewrite-objc之后:

id obj = ...
{ id _rethrow = 0; id _sync_obj = (id)obj; objc_sync_enter(_sync_obj);
    try {
        struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}
            ~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
            id sync_exit;
        } _sync_exit(_sync_obj);
        
        
    } catch (id e) {_rethrow = e;}
    { struct _FIN { _FIN(id reth) : rethrow(reth) {}
        ~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
        id rethrow;
    } _fin_force_rethow(_rethrow);}
}

即編譯器把@synchronized相關(guān)代碼轉(zhuǎn)成下面這一大坨東西。代碼的關(guān)鍵在兩個(gè)函數(shù)的調(diào)用:objc_sync_exitobjc_sync_enter。代碼的邏輯還是比較好理解,想深入了解的可以看下這篇博文


dispatch_once

使用dispatch_once構(gòu)造單例通常如下:

static SomeClass * instance = nil;
+ (instancetype)shareInstance {
    static dispatch_once_t onceTocken;
    dispatch_once(&onceTocken, ^{
        instance = [[SomeClass alloc] init];
    });
    return instance;
}

dispatch_once是GCD中提供的函數(shù),通常使用它來初始化全局?jǐn)?shù)據(jù)(單例),它接受兩個(gè)參數(shù),dispatch_once_t *類型的謂語和dispatch_block_t類型的block(block中的代碼就是臨界區(qū))。文檔中說到,dispatch_once_t *類型的謂語用于測試block是否已經(jīng)執(zhí)行結(jié)束或者還沒與執(zhí)行,它配合上dispatch_once函數(shù)保證了在applecation的生命周期中block只會運(yùn)行一次,并且是線程安全的。可以看到dispatch_once@synchronized一樣,是線程安全,不同指出在于@synchronized的臨界區(qū)代碼可能在application生命周期中多次調(diào)用,而dispatch_once只會調(diào)用一次(使用dispatch_once_t *類型的謂語做判斷)。因此@synchronized的臨界區(qū)代碼要判斷instance是否是nil,來判斷是實(shí)例是否已經(jīng)構(gòu)造了。

注意dispatch_once_t *類型的謂語必須是全局變量或者靜態(tài)變量,如果使用自動或者動態(tài)變量(包括Objective-C實(shí)例變量),dispatch_once的結(jié)果是無法預(yù)知的。


單例思考

單例用著用著就被濫用了。最近正在思考如何對公司APP的分享模塊重構(gòu)。其中有個(gè)類叫SSShareManager,作為的單例,它管理著所有模塊的分享邏輯。濫用點(diǎn)就在既然是單例,卻使用它來保存來自不同模塊的數(shù)據(jù)以及狀態(tài)(比如:分享到微信還是QQ、友盟統(tǒng)計(jì)的數(shù)據(jù)等),無形中增加了不同模塊之間的耦合。因?yàn)樵谀硞€(gè)模塊調(diào)用SSShareManager單例分享之前,還要記得清理其他模塊可能留下的數(shù)據(jù)=,=。這篇博文給我很大啟發(fā),其中印象深刻的一段話:

The lesson here is that singletons should be preserved only for state that is global, and not tied to any scope. If state is scoped to any session shorter than “a complete lifecycle of my app,” that state should not be managed by a singleton.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容