runtime-閑聊內(nèi)存管理

前言

ARC作為一個老生常談的話題,基本被網(wǎng)上的各種博客說盡了。但是前段時間朋友通過某些手段對YYModel進行了優(yōu)化,提高了大概1/3左右的效率,在觀賞過他改進的源碼之后我又重新看了一遍ARC相關(guān)的實現(xiàn)源碼,主要體現(xiàn)ARC機制的幾個方法分別是retain、release以及dealloc,主要與strongweak兩者相關(guān)

ARC的內(nèi)存管理

來看看一段ARC環(huán)境下的代碼
- (void)viewDidLoad {
NSArray * titles = @[@"title1", @"title2"];
}
在編譯期間,代碼就會變成這樣:

- (void)viewDidLoad {
    NSArray * titles = @[@"title1", @"title2"];
    [titles retain];
    ///  .......
    [titles release];
}

簡單來說就是ARC在代碼編譯階段,會自動在代碼的上下文中成對插入retain以及release,保證引用計數(shù)能夠正確管理內(nèi)存。如果對象不是強引用類型,那么ARC的處理也會進行相應(yīng)的改變


下面會分別說明在這幾個與引用計數(shù)相關(guān)的方法調(diào)用中發(fā)生了什么

retain

強引用有retain、strong以及__strong三種修飾,默認(rèn)情況下,所有的類對象會自動被標(biāo)識為__strong強引用對象,強引用對象會在上下文插入retain以及release調(diào)用,從runtime源碼處可以下載到對應(yīng)調(diào)用的源代碼。在retain調(diào)用的過程中,總共涉及到了四次調(diào)用:

  • id _objc_rootRetain(id obj)
    對傳入對象進行非空斷言,然后調(diào)用對象的rootRetain()方法
  • id objc_object::rootRetain()
    斷言非GC環(huán)境,如果對象是TaggedPointer指針,不做處理。TaggedPointer是蘋果推出的一套優(yōu)化方案,具體可以參考深入了解Tagged Pointer一文
  • id objc_object::sidetable_retain()
    增加引用計數(shù),具體往下看
  • id objc_object::sidetable_retain_slow(SideTable& table)
    增加引用計數(shù),具體往下看

在上面的幾步中最重要的步驟就是最后兩部的增加引用計數(shù),在NSObject.mm中可以看到函數(shù)的實現(xiàn)。這里筆者剔除了部分不相關(guān)的代碼:

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1)
#define SIDE_TABLE_RC_ONE            (1UL<<2)
#define SIDE_TABLE_RC_PINNED         (1UL<<(WORD_BITS-1))

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}

id objc_object::sidetable_retain()
{
    // 獲取對象的table對象
    SideTable& table = SideTables()[this];

    if (table.trylock()) {

        // 獲取 引用計數(shù)的引用
        size_t& refcntStorage = table.refcnts[this];
        if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
            // 如果引用計數(shù)未越界,則引用計數(shù)增加
            refcntStorage += SIDE_TABLE_RC_ONE;
        }
        table.unlock();
        return (id)this;
    }
    return sidetable_retain_slow(table);
}
  • SideTable這個類包含著一個自旋鎖slock來防止操作時可能出現(xiàn)的多線程讀取問題、一個弱引用表weak_table以及引用計數(shù)表refcnts。另外還提供一個方法傳入對象地址來尋找對應(yīng)的SideTable對象

  • RefcountMap對象通過散列表的結(jié)構(gòu)存儲了對象持有者的地址以及引用計數(shù),這樣一來,即便對象對應(yīng)的內(nèi)存出現(xiàn)錯誤,例如Zombie異常,也能定位到對象的地址信息

  • 每次retain后以后引用計數(shù)的值實際上增加了(1 << 2) == 4而不是我們所知的1,這是由于引用計數(shù)的后兩位分別被弱引用以及析構(gòu)狀態(tài)兩個標(biāo)識位占領(lǐng),而第一位用來表示計數(shù)是否越界。

由于引用計數(shù)可能存在越界情況(SIDE_TABLE_RC_PINNED位的值為1),因此散列表refcnts中應(yīng)該存儲了多個引用計數(shù),sidetable_retainCount()函數(shù)也證明了這一點:

#define SIDE_TABLE_RC_SHIFT 2
uintptr_t objc_object::sidetable_retainCount()
{
    SideTable& table = SideTables()[this];
    size_t refcnt_result = 1;

    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}

引用計數(shù)總是返回1 + 計數(shù)表總計這個數(shù)值,這也是為什么經(jīng)常性的當(dāng)對象被釋放后,我們獲取retainCount的值總不能為0。至于函數(shù)sidetable_retain_slow的實現(xiàn)和sidetable_retain幾乎一樣,就不再介紹了

release

release調(diào)用有著跟retain類似的四次調(diào)用,前兩次調(diào)用的作用一樣,因此這里只放上引用計數(shù)減少的函數(shù)代碼:

uintptr_t objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.indexed);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    if (table.trylock()) {
        RefcountMap::iterator it = table.refcnts.find(this);
        if (it == table.refcnts.end()) {
            do_dealloc = true;
            table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
        } else if (it->second < SIDE_TABLE_DEALLOCATING) {
            // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
            do_dealloc = true;
            it->second |= SIDE_TABLE_DEALLOCATING;
        } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
            it->second -= SIDE_TABLE_RC_ONE;
        }
        table.unlock();
        if (do_dealloc  &&  performDealloc) {
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
        }
        return do_dealloc;
    }

    return sidetable_release_slow(table, performDealloc);
}

release中決定對象是否會被dealloc有兩個主要的判斷

  • 如果引用計數(shù)為計數(shù)表中的最后一個,標(biāo)記對象為正在析構(gòu)狀態(tài),然后執(zhí)行完成后發(fā)送SEL_dealloc消息釋放對象
  • 即便計數(shù)表的值為零,sidetable_retainCount函數(shù)照樣會返回1的值。這時計數(shù)小于宏定義SIDE_TABLE_DEALLOCATING == 1,就不進行減少計數(shù)的操作,直接標(biāo)記對象正在析構(gòu)

看到release的代碼就會發(fā)現(xiàn)在上面代碼中宏定義SIDE_TABLE_DEALLOCATING體現(xiàn)出了蘋果這個心機婊的用心之深。通常而言,即便引用計數(shù)只有8位的占用,在剔除了首位越界標(biāo)記以及后兩位后,其最大取值為2^5-1 == 31位。通常來說,如果不是項目中block不加限制的引用,是很難達(dá)到這么多的引用量的。因此占用了SIDE_TABLE_DEALLOCATING位不僅減少了額外占用的標(biāo)記變量內(nèi)存,還能以作為引用計數(shù)是否歸零的判斷

weak

最開始的時候沒打算講weak這個修飾,不過因為dealloc方法本身涉及到了弱引用對象置空的操作,以及retain過程中的對象也跟weak有關(guān)系的情況下,簡單的說說weak的操作

bool objc_object::sidetable_isWeaklyReferenced()
{
    bool result = false;

    SideTable& table = SideTables()[this];
    table.lock();

    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        result = it->second & SIDE_TABLE_WEAKLY_REFERENCED;
    }

    table.unlock();

    return result;
}

weakstrong共用一套引用計數(shù)設(shè)計,因此兩者的賦值操作都要設(shè)置計數(shù)表,只是weak修飾的對象的引用計數(shù)對象會被設(shè)置SIDE_TABLE_WEAKLY_REFERENCED位,并且不參與sidetable_retainCount函數(shù)中的計數(shù)計算而已

void objc_object::sidetable_setWeaklyReferenced_nolock()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.indexed);
#endif

    SideTable& table = SideTables()[this];

    table.refcnts[this] |= SIDE_TABLE_WEAKLY_REFERENCED;
}

另一個弱引用設(shè)置方法,相比上一個方法去掉了自旋鎖加鎖操作

dealloc

dealloc是重量級的方法之一,不過由于函數(shù)內(nèi)部調(diào)用層次過多,這里不多闡述。實現(xiàn)代碼在objc-object.h798行,可以自行到官網(wǎng)下載源碼后研讀

__unsafe_unretained

其實寫了這么多,終于把本文的主角給講出來了。在iOS5的時候,蘋果正式推出了ARC機制,伴隨的是上面的weak、strong等新修飾符,當(dāng)然還有一個不常用的__unsafe_unretained

  • weak
    修飾的對象在指向的內(nèi)存被釋放后會被自動置為nil
  • strong
    持有指向的對象,會讓引用計數(shù)+1
  • __unsafe_unretained
    不引用指向的對象。但在對象內(nèi)存被釋放掉后,依舊指向內(nèi)存地址,等同于assign,但是只能修飾對象

在機器上保證應(yīng)用能保持在55幀以上的速率會讓應(yīng)用看起來如絲綢般順滑,但是稍有不慎,稍微降到50~55之間都有很大的可能展現(xiàn)出卡頓的現(xiàn)象。這里不談及圖像渲染、數(shù)據(jù)大量處理等耳聞能詳?shù)男阅軔汗?,說說Model所造成的損耗。

如前面所說的,在ARC環(huán)境下,對象的默認(rèn)修飾為strong,這意味著這么一段代碼:

@protocol RegExpCheck

@property (nonatomic, copy) NSString * regExp;

- (BOOL)validRegExp;

@end

- (BOOL)valid: (NSArray<id<RegExpCheck>> *)params {
    for (id<RegExpCheck> item in params) {
        if (![item validRegExp]) { return NO; }
    }
    return YES;
}

把這段代碼改為編譯期間插入retainrelease方法后的代碼如下:

- (BOOL)valid: (NSArray<id<RegExpCheck>> *)params {
    for (id<RegExpCheck> item in params) {
        [item retain];
        if (![item validRegExp]) { 
            [item release];
            return NO;
        }
        [item release];
    }
    return YES;
}

遍歷操作在項目中出現(xiàn)的概率絕對排的上前列,那么上面這個方法在調(diào)用期間會調(diào)用params.countretainrelease函數(shù)。通常來說,每一個對象的遍歷次數(shù)越多,這些函數(shù)調(diào)用的損耗就越大。如果換做__unsafe_unretained修飾對象,那么這部分的調(diào)用損耗就被節(jié)省下來,這也是筆者朋友改進的手段

尾話

首先要承認(rèn),相比起其他性能惡鬼改進的優(yōu)化,使用__unsafe_unretained帶來的收益幾乎微乎其微,因此筆者并不是很推薦用這種高成本低回報的方式優(yōu)化項目,起碼在性能惡鬼大頭解決之前不推薦,但是去學(xué)習(xí)內(nèi)存管理底層的知識可以幫助我們站在更高的地方看待開發(fā)。

ps:在朋友的堅持下,可恥的取消了代碼鏈接

上一篇:消息機制
下一篇:分類為什么不生成setter和getter

轉(zhuǎn)載請注明本文作者和地址

最后編輯于
?著作權(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)容

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