iOS-底層原理 33:內(nèi)存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底層分析

iOS 底層原理 文章匯總

本文主要是分析內(nèi)存管理中的內(nèi)存管理方案,以及retain、retainCount、release、dealloc的底層源碼分析

ARC & MRC

iOS中的內(nèi)存管理方案,大致可以分為兩類:MRC(手動(dòng)內(nèi)存管理)和ARC(自動(dòng)內(nèi)存管理)

MRC

  • MRC時(shí)代,系統(tǒng)是通過(guò)對(duì)象的引用計(jì)數(shù)來(lái)判斷一個(gè)是否銷(xiāo)毀,有以下規(guī)則

    • 對(duì)象被創(chuàng)建時(shí)引用計(jì)數(shù)都為1

    • 當(dāng)對(duì)象被其他指針引用時(shí),需要手動(dòng)調(diào)用[objc retain],使對(duì)象的引用計(jì)數(shù)+1

    • 當(dāng)指針變量不再使用對(duì)象時(shí),需要手動(dòng)調(diào)用[objc release]來(lái)釋放對(duì)象,使對(duì)象的引用計(jì)數(shù)-1

    • 當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)為0時(shí),系統(tǒng)就會(huì)銷(xiāo)毀這個(gè)對(duì)象

  • 所以,在MRC模式下,必須遵守:誰(shuí)創(chuàng)建,誰(shuí)釋放,誰(shuí)引用,誰(shuí)管理
    ARC

  • ARC模式是在WWDC2011和iOS5引入的自動(dòng)管理機(jī)制,即自動(dòng)引用計(jì)數(shù)。是編譯器的一種特性。其規(guī)則與MRC一致,區(qū)別在于,ARC模式下不需要手動(dòng)retain、release、autorelease。編譯器會(huì)在適當(dāng)?shù)奈恢貌迦雛elease和autorelease

內(nèi)存布局

我們?cè)?a href="http://www.itdecent.cn/p/e5a54813b93d" target="_blank">iOS-底層原理 24:內(nèi)存五大區(qū)文章中,介紹了內(nèi)存的五大區(qū)。其實(shí)除了內(nèi)存區(qū),還有內(nèi)核區(qū)保留區(qū),以4GB手機(jī)為例,如下所示,系統(tǒng)將其中的3GB給了五大區(qū)+保留區(qū),剩余的1GB給內(nèi)核區(qū)使用

內(nèi)存布局

  • 內(nèi)核區(qū):系統(tǒng)用來(lái)進(jìn)行內(nèi)核處理操作的區(qū)域

  • 五大區(qū):這里不再作說(shuō)明,具體請(qǐng)參考上面的鏈接

  • 保留區(qū):預(yù)留給系統(tǒng)處理nil等

這里有個(gè)疑問(wèn),為什么五大區(qū)的最后內(nèi)存地址是從0x00400000開(kāi)始的。其主要原因是0x00000000表示nil,不能直接用nil表示一個(gè)段,所以單獨(dú)給了一段內(nèi)存用于處理nil等情況

內(nèi)存布局相關(guān)面試題

面試題1:全局變量和局部變量在內(nèi)存中是否有區(qū)別?如果有,是什么區(qū)別?

  • 有區(qū)別

  • 全局變量保存在內(nèi)存的全局存儲(chǔ)區(qū)(即bss+data段),占用靜態(tài)的存儲(chǔ)單元

  • 局部變量保存在中,只有在所在函數(shù)被調(diào)用時(shí)才動(dòng)態(tài)的為變量分配存儲(chǔ)單元

面試題2:Block中可以修改全局變量,全局靜態(tài)變量,局部靜態(tài)變量,局部變量嗎?

  • 可以修改全局變量,全局靜態(tài)變量,因?yàn)槿肿兞?和 靜態(tài)全局變量是全局的,作用域很廣

  • 可以修改局部靜態(tài)變量,不可以修改局部斌量

    • 局部靜態(tài)變量(static修飾的) 和 局部變量,被block從外面捕獲,成為 __main_block_impl_0這個(gè)結(jié)構(gòu)體的成員變量

    • 局部變量是以值方式傳遞到block的構(gòu)造函數(shù)中的,只會(huì)捕獲block中會(huì)用到的變量,由于只捕獲了變量的值,并非內(nèi)存地址,所以在block內(nèi)部不能改變局部變量的值

    • 局部靜態(tài)變量是以指針形式,被block捕獲的,由于捕獲的是指針,所以可以修改局部靜態(tài)變量的值

  • ARC環(huán)境下,一旦使用__block修飾并在block中修改,就會(huì)觸發(fā)copy,block就會(huì)從棧區(qū)copy到堆區(qū),此時(shí)的block是堆區(qū)block

  • ARC模式下,Block中引用id類型的數(shù)據(jù),無(wú)論有沒(méi)有__block修飾,都會(huì)retain,對(duì)于基礎(chǔ)數(shù)據(jù)類型,沒(méi)有__block就無(wú)法修改變量值;如果有__block修飾,也是在底層修改__Block_byref_a_0結(jié)構(gòu)體,將其內(nèi)部的forwarding指針指向copy后的地址,來(lái)達(dá)到值的修改

內(nèi)存管理方案

內(nèi)存管理方案除了前文提及的MRCARC,還有以下三種

  • Tagged Pointer:專門(mén)用來(lái)處理小對(duì)象,例如NSNumber、NSDate、小NSString等

  • Nonpointer_isa:非指針類型的isa,主要是用來(lái)優(yōu)化64位地址,這個(gè)在iOS-底層原理 07:isa與類關(guān)聯(lián)的原理一文中,已經(jīng)介紹了

  • SideTables散列表,在散列表中主要有兩個(gè)表,分別是引用計(jì)數(shù)表、弱引用表

這里主要著重介紹Tagged PointerSideTables,我們通過(guò)一個(gè)面試題來(lái)引入Tagged Pointer

面試題

以下代碼會(huì)有什么問(wèn)題?

//*********代碼1*********
- (void)taggedPointerDemo {
  self.queue = dispatch_queue_create("com.cjl.cn", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"CJL"];  // alloc 堆 iOS優(yōu)化 - taggedpointer
             NSLog(@"%@",self.nameStr);
        });
    }
}

//*********代碼2*********
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"來(lái)了");
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"CJL_越努力,越幸運(yùn)?。?!"];
            NSLog(@"%@",self.nameStr);
        });
    }
}

運(yùn)行以上代碼,發(fā)現(xiàn)taggedPointerDemo單獨(dú)運(yùn)行沒(méi)有問(wèn)題,當(dāng)觸發(fā)touchesBegan方法后。程序會(huì)崩潰,崩潰的原因是多條線程同時(shí)對(duì)一個(gè)對(duì)象進(jìn)行釋放,導(dǎo)致了 過(guò)渡釋放所以崩潰。其根本原因是因?yàn)?code>nameStr在底層的類型不一致導(dǎo)致的,我們可以通過(guò)調(diào)試看出

調(diào)試NSString

  • taggedPointerDemo方法中的nameStr類型是 NSTaggedPointerString,存儲(chǔ)在常量區(qū)。因?yàn)?code>nameStr在alloc分配時(shí)在堆區(qū),由于較小,所以經(jīng)過(guò)xcode中iOS的優(yōu)化,成了NSTaggedPointerString類型,存儲(chǔ)在常量區(qū)

  • touchesBegan方法中的nameStr類型是 NSCFString類型,存儲(chǔ)在堆上

NSString的內(nèi)存管理

我們可以通過(guò)NSString初始化的兩種方式,來(lái)測(cè)試NSString的內(nèi)存管理

  • 通過(guò) WithString + @""方式初始化

  • 通過(guò) WithFormat方式初始化

#define KLog(_c) NSLog(@"%@ -- %p -- %@",_c,_c,[_c class]);

- (void)testNSString{
    //初始化方式一:通過(guò) WithString + @""方式
    NSString *s1 = @"1";
    NSString *s2 = [[NSString alloc] initWithString:@"222"];
    NSString *s3 = [NSString stringWithString:@"33"];
    
    KLog(s1);
    KLog(s2);
    KLog(s3);
    
    //初始化方式二:通過(guò) WithFormat
    //字符串長(zhǎng)度在9以內(nèi)
    NSString *s4 = [NSString stringWithFormat:@"123456789"];
    NSString *s5 = [[NSString alloc] initWithFormat:@"123456789"];
    
    //字符串長(zhǎng)度大于9
    NSString *s6 = [NSString stringWithFormat:@"1234567890"];
    NSString *s7 = [[NSString alloc] initWithFormat:@"1234567890"];
    
    KLog(s4);
    KLog(s5);
    KLog(s6);
    KLog(s7);
}

以下是運(yùn)行的結(jié)果


運(yùn)行結(jié)果

所以,從上面可以總結(jié)出,NSString的內(nèi)存管理主要分為3種

  • __NSCFConstantString:字符串常量,是一種編譯時(shí)常量,retainCount值很大,對(duì)其操作,不會(huì)引起引用計(jì)數(shù)變化,存儲(chǔ)在字符串常量區(qū)

  • __NSCFString:是在運(yùn)行時(shí)創(chuàng)建的NSString子類,創(chuàng)建后引用計(jì)數(shù)會(huì)加1,存儲(chǔ)在堆上

  • NSTaggedPointerString:標(biāo)簽指針,是蘋(píng)果在64位環(huán)境下對(duì)NSString、NSNumber等對(duì)象做的優(yōu)化。對(duì)于NSString對(duì)象來(lái)說(shuō)

    • 當(dāng)字符串是由數(shù)字、英文字母組合且長(zhǎng)度小于等于9時(shí),會(huì)自動(dòng)成為NSTaggedPointerString類型,存儲(chǔ)在常量區(qū)

    • 當(dāng)有中文或者其他特殊符號(hào)時(shí),會(huì)直接成為__NSCFString類型,存儲(chǔ)在堆區(qū)

Tagged Pointer 小對(duì)象

由一個(gè)NSString的面試題,引出了Tagged Pointer,為了探索小對(duì)象的引用計(jì)數(shù)處理,所以我們需要進(jìn)入objc源碼中查看retain、release源碼 中對(duì) Tagged Pointer小對(duì)象的處理

小對(duì)象的引用計(jì)數(shù)處理分析

  • 查看setProperty -> reallySetProperty源碼,其中是對(duì)新值retain,舊值release

    image

  • 進(jìn)入objc_retainobjc_release源碼,在這里都判斷是否是小對(duì)象,如果是小對(duì)象,則不會(huì)進(jìn)行retain或者release,會(huì)直接返回。因此可以得出一個(gè)結(jié)論:如果對(duì)象是小對(duì)象,不會(huì)進(jìn)行retain 和 release

//****************objc_retain****************
__attribute__((aligned(16), flatten, noinline))
id 
objc_retain(id obj)
{
    if (!obj) return obj;
    //判斷是否是小對(duì)象,如果是,則直接返回對(duì)象
    if (obj->isTaggedPointer()) return obj;
    //如果不是小對(duì)象,則retain
    return obj->retain();
}

//****************objc_release****************
__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    //如果是小對(duì)象,則直接返回
    if (obj->isTaggedPointer()) return;
    //如果不是小對(duì)象,則release
    return obj->release();
}

小對(duì)象的地址分析

繼續(xù)以NSString為例,對(duì)于NSString來(lái)說(shuō)

  • 一般的NSString對(duì)象指針,都是string值 + 指針地址,兩者是分開(kāi)的

  • 對(duì)于Tagged Pointer指針,其指針+值,都能在小對(duì)象中體現(xiàn)。所以Tagged Pointer 既包含指針,也包含值

在之前的文章講類的加載時(shí),其中的_read_images源碼有一個(gè)方法對(duì)小對(duì)象進(jìn)行了處理,即initializeTaggedPointerObfuscator方法

  • 進(jìn)入_read_images -> initializeTaggedPointerObfuscator源碼實(shí)現(xiàn)
static void
initializeTaggedPointerObfuscator(void)
{
    
    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        DisableTaggedPointerObfuscation) {
        objc_debug_taggedpointer_obfuscator = 0;
    }
    //在iOS14之后,對(duì)小對(duì)象進(jìn)行了混淆,通過(guò)與操作+_OBJC_TAG_MASK混淆
    else {
        // Pull random data into the variable, then shift away all non-payload bits.
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    }
}

在實(shí)現(xiàn)中,我們可以看出,在iOS14之后,Tagged Pointer采用了混淆處理,如下所示

image

  • 我們可以在源碼中通過(guò)objc_debug_taggedpointer_obfuscator查找taggedPointer的編碼解碼,來(lái)查看底層是如何混淆處理的
//編碼
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
//編碼
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;

通過(guò)實(shí)現(xiàn),我們可以得知,在編碼和解碼部分,經(jīng)過(guò)了兩層異或,其目的是得到小對(duì)象自己,例如以 1010 0001為例,假設(shè)mask0101 1000

    1010 0001 
   ^0101 1000 mask(編碼)
    1111 1001
   ^0101 1000 mask(解碼)
    1010 0001
  • 所以在外界,為了獲取小對(duì)象的真實(shí)地址,我們可以將解碼的源碼拷貝到外面,將NSString混淆部分進(jìn)行解碼,如下所示
    image

    觀察解碼后的小對(duì)象地址,其中的62表示bASCII碼,再以NSNumber為例,同樣可以看出,1就是我們實(shí)際的值
    image

到這里,我們驗(yàn)證了小對(duì)象指針地址中確實(shí)存儲(chǔ)了值,那么小對(duì)象地址高位其中的0xa、0xb又是什么含義呢?

//NSString
0xa000000000000621

//NSNumber
0xb000000000000012
0xb000000000000025
  • 需要去源碼中查看_objc_isTaggedPointer源碼,主要是通過(guò)保留最高位的值(即64位的值),判斷是否等于_OBJC_TAG_MASK(即2^63),來(lái)判斷是否是小對(duì)象
static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    //等價(jià)于 ptr & 1左移63,即2^63,相當(dāng)于除了64位,其他位都為0,即只是保留了最高位的值
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

所以0xa、0xb主要是用于判斷是否是小對(duì)象taggedpointer,即判斷條件,判斷第64位上是否為1(taggedpointer指針地址即表示指針地址,也表示值)

  • 0xa 轉(zhuǎn)換成二進(jìn)制為 1 010(64為為1,63~61后三位表示 tagType類型 - 2),表示NSString類型

  • 0xb 轉(zhuǎn)換為二進(jìn)制為 1 011(64為為1,63~61后三位表示 tagType類型 - 3),表示NSNumber類型,這里需要注意一點(diǎn),如果NSNumber的值是-1,其地址中的值是用補(bǔ)碼表示的

這里可以通過(guò)_objc_makeTaggedPointer方法的參數(shù)tag類型objc_tag_index_t進(jìn)入其枚舉,其中 2表示NSString,3表示NSNumber

image

  • 同理,我們可以定義一個(gè)NSDate對(duì)象,來(lái)驗(yàn)證其tagType是否為6。通過(guò)打印結(jié)果,其地址高位是0xe,轉(zhuǎn)換為二進(jìn)制為1 110,排除64位的1,剩余的3位正好轉(zhuǎn)換為十進(jìn)制是6,符合上面的枚舉值
    image

Tagged Pointer 總結(jié)

  • Tagged Pointer小對(duì)象類型(用于存儲(chǔ)NSNumber、NSDate、小NSString),小對(duì)象指針不再是簡(jiǎn)單的地址,而是地址 + 值,即真正的值,所以,實(shí)際上它不再是一個(gè)對(duì)象了,它只是一個(gè)披著對(duì)象皮的普通變量而以。所以可以直接進(jìn)行讀取。優(yōu)點(diǎn)是占用空間小 節(jié)省內(nèi)存

  • Tagged Pointer小對(duì)象 不會(huì)進(jìn)入retain 和 release,而是直接返回了,意味著不需要ARC進(jìn)行管理,所以可以直接被系統(tǒng)自主的釋放和回收

  • Tagged Pointer內(nèi)存并不存儲(chǔ)在堆中,而是在常量區(qū)中,也不需要malloc和free,所以可以直接讀取,相比存儲(chǔ)在堆區(qū)的數(shù)據(jù)讀取,效率上快了3倍左右。創(chuàng)建的效率相比堆區(qū)快了近100倍左右

  • 所以,綜合來(lái)說(shuō),taggedPointer的內(nèi)存管理方案,比常規(guī)的內(nèi)存管理,要快很多

  • Tagged Pointer的64位地址中,前4位代表類型,后4位主要適用于系統(tǒng)做一些處理,中間56位用于存儲(chǔ)值

  • 優(yōu)化內(nèi)存建議:對(duì)于NSString來(lái)說(shuō),當(dāng)字符串較小時(shí),建議直接通過(guò)@""初始化,因?yàn)榇鎯?chǔ)在常量區(qū),可以直接進(jìn)行讀取。會(huì)比WithFormat初始化方式更加快速

SideTables 散列表

當(dāng)引用計(jì)數(shù)存儲(chǔ)到一定值是,并不會(huì)再存儲(chǔ)到Nonpointer_isa的位域的extra_rc中,而是會(huì)存儲(chǔ)到SideTables 散列表中

下面我們就來(lái)繼續(xù)探索引用計(jì)數(shù)retain的底層實(shí)現(xiàn)

retain 源碼分析

  • 進(jìn)入objc_retain -> retain -> rootRetain源碼實(shí)現(xiàn),主要有以下幾部分邏輯:
    • 【第一步】判斷是否為Nonpointer_isa

    • 【第二步】操作引用計(jì)數(shù)

      • 1、如果不是Nonpointer_isa,則直接操作SideTables散列表,此時(shí)的散列表并不是只有一張,而是有很多張(后續(xù)會(huì)分析,為什么需要多張)

      • 2、判斷是否正在釋放,如果正在釋放,則執(zhí)行dealloc流程

      • 3、執(zhí)行extra_rc+1,即引用計(jì)數(shù)+1操作,并給一個(gè)引用計(jì)數(shù)的狀態(tài)標(biāo)識(shí)carry,用于表示extra_rc是否滿了

      • 4、如果carray的狀態(tài)表示extra_rc的引用計(jì)數(shù)滿了,此時(shí)需要操作散列表,即 將滿狀態(tài)的一半拿出來(lái)存到extra_rc,另一半存在 散列表的rc_half。這么做的原因是因?yàn)槿绻即鎯?chǔ)在散列表,每次對(duì)散列表操作都需要開(kāi)解鎖,操作耗時(shí),消耗性能大,這么對(duì)半分操作的目的在于提高性能

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;
    //為什么有isa?因?yàn)樾枰獙?duì)引用計(jì)數(shù)+1,即retain+1,而引用計(jì)數(shù)存儲(chǔ)在isa的bits中,需要進(jìn)行新舊isa的替換
    isa_t oldisa;
    isa_t newisa;
    //重點(diǎn)
    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        //判斷是否為nonpointer isa
        if (slowpath(!newisa.nonpointer)) {
            //如果不是 nonpointer isa,直接操作散列表sidetable
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return (id)this;
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        //dealloc源碼
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        
        
        uintptr_t carry;
        //執(zhí)行引用計(jì)數(shù)+1操作,即對(duì)bits中的 1ULL<<45(arm64) 即extra_rc,用于該對(duì)象存儲(chǔ)引用計(jì)數(shù)值
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        //判斷extra_rc是否滿了,carry是標(biāo)識(shí)符
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            //如果extra_rc滿了,則直接將滿狀態(tài)的一半拿出來(lái)存到extra_rc
            newisa.extra_rc = RC_HALF;
            //給一個(gè)標(biāo)識(shí)符為YES,表示需要存儲(chǔ)到散列表
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        //將另一半存在散列表的rc_half中,即滿狀態(tài)下是8位,一半就是1左移7位,即除以2
        //這么操作的目的在于提高性能,因?yàn)槿绻即嬖谏⒘斜碇?,?dāng)需要release-1時(shí),需要去訪問(wèn)散列表,每次都需要開(kāi)解鎖,比較消耗性能。extra_rc存儲(chǔ)一半的話,可以直接操作extra_rc即可,不需要操作散列表。性能會(huì)提高很多
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

問(wèn)題1:散列表為什么在內(nèi)存有多張?最多能夠多少?gòu)垼?/strong>

  • 如果散列表只有一張表,意味著全局所有的對(duì)象都會(huì)存儲(chǔ)在一張表中,都會(huì)進(jìn)行開(kāi)鎖解鎖(鎖是鎖整個(gè)表的讀寫(xiě))。當(dāng)開(kāi)鎖時(shí),由于所有數(shù)據(jù)都在一張表,則意味著數(shù)據(jù)不安全

  • 如果每個(gè)對(duì)象都開(kāi)一個(gè)表,會(huì)耗費(fèi)性能,所以也不能有無(wú)數(shù)個(gè)表

  • 散列表的類型是SideTable,有如下定義

struct SideTable {
    spinlock_t slock;//開(kāi)/解鎖
    RefcountMap refcnts;//引用計(jì)數(shù)表
    weak_table_t weak_table;//弱引用表
    
    ....
}
  • 通過(guò)查看sidetable_unlock方法定位SideTables,其內(nèi)部是通過(guò)SideTablesMap的get方法獲取。而SideTablesMap是通過(guò)StripedMap<SideTable>定義的
void 
objc_object::sidetable_unlock()
{
    //SideTables散列表并不只是一張,而是很多張,與關(guān)聯(lián)對(duì)象表類似
    SideTable& table = SideTables()[this];
    table.unlock();
}
??
static StripedMap<SideTable>& SideTables() {
    return SideTablesMap.get();
}
??
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

從而進(jìn)入StripedMap的定義,從這里可以看出,同一時(shí)間,真機(jī)中散列表最多只能有8張

image

問(wèn)題2:為什么在用散列表,而不用數(shù)組、鏈表?

  • 數(shù)組:特點(diǎn)在于查詢方便(即通過(guò)下標(biāo)訪問(wèn)),增刪比較麻煩(類似于之前講過(guò)的methodList,通過(guò)memcopy、memmove增刪,非常麻煩),所以數(shù)據(jù)的特性是讀取快,存儲(chǔ)不方便

  • 鏈表:特點(diǎn)在于增刪方便,查詢慢(需要從頭節(jié)點(diǎn)開(kāi)始遍歷查詢),所以鏈表的特性是存儲(chǔ)快,讀取慢

  • 散列表本質(zhì)就是一張哈希表,哈希表集合了數(shù)組和鏈表的長(zhǎng)處,增刪改查都比較方便,例如拉鏈哈希表(在之前鎖的文章中,講過(guò)的tls的存儲(chǔ)結(jié)構(gòu)就是拉鏈形式的),是最常用的,如下所示

    image

    可以從SideTables -> StripedMap -> indexForPointer中驗(yàn)證是通過(guò)哈希函數(shù)計(jì)算哈希下標(biāo) 以及sideTables為什么可以使用[]的原因
    image

所以,綜上所述,retain的底層流程如下所示

image

總結(jié):retain 完整回答

  • retain在底層首先會(huì)判斷是否是 Nonpointer isa,如果不是,則直接操作散列表 進(jìn)行+1操作

  • 如果是Nonpointer isa,還需要判斷是否正在釋放,如果正在釋放,則執(zhí)行dealloc流程,釋放弱引用表和引用技術(shù)表,最后free釋放對(duì)象內(nèi)存

  • 如果不是正在釋放,則對(duì)Nonpointer isa進(jìn)行常規(guī)的引用計(jì)數(shù)+1.這里需要注意一點(diǎn)的是,extra_rc在真機(jī)上只有8位用于存儲(chǔ)引用計(jì)數(shù)的值,當(dāng)存儲(chǔ)滿了時(shí),需要借助散列表用于存儲(chǔ)。需要將滿了的extra_rc對(duì)半分,一半(即2^7)存儲(chǔ)在散列表中。另一半還是存儲(chǔ)在extra_rc中,用于常規(guī)的引用計(jì)數(shù)的+1或者-1操作,然后再返回

release 源碼分析

分析了retain的底層實(shí)現(xiàn),下面來(lái)分析release的底層實(shí)現(xiàn)

  • 通過(guò)setProperty -> reallySetProperty -> objc_release -> release -> rootRelease -> rootRelease順序,進(jìn)入rootRelease源碼,其操作與retain 相反
    • 判斷是否是Nonpointer isa,如果不是,則直接對(duì)散列表進(jìn)行-1操作

    • 如果是Nonpointer isa,則對(duì)extra_rc中的引用計(jì)數(shù)值進(jìn)行-1操作,并存儲(chǔ)此時(shí)的extra_rc狀態(tài)到carry

    • 如果此時(shí)的狀態(tài)carray為0,則走到underflow流程

    • underflow流程有以下幾步:

      • 判斷散列表是否存儲(chǔ)了一半的引用計(jì)數(shù)

      • 如果是,則從散列表取出存儲(chǔ)的一半引用計(jì)數(shù),進(jìn)行-1操作,然后存儲(chǔ)到extra_rc

      • 如果此時(shí)extra_rc沒(méi)有值,散列表中也是空的,則直接進(jìn)行析構(gòu),即dealloc操作,屬于自動(dòng)觸發(fā)

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        //判斷是否是Nonpointer isa
        if (slowpath(!newisa.nonpointer)) {
            //如果不是,則直接操作散列表-1
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return false;
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        //進(jìn)行引用計(jì)數(shù)-1操作,即extra_rc-1
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        //如果此時(shí)extra_rc的值為0了,則走到underflow
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;
    //判斷散列表中是否存儲(chǔ)了一半的引用計(jì)數(shù)
    if (slowpath(newisa.has_sidetable_rc)) {
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.

        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            goto retry;
        }

        // Try to remove some retain counts from the side table.
        //從散列表中取出存儲(chǔ)的一半引用計(jì)數(shù)
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set 
        // even if the side table count is now zero.

        if (borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            //進(jìn)行-1操作,然后存儲(chǔ)到extra_rc中
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }

            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // This decrement cannot be the deallocating decrement - the side 
            // table lock and has_sidetable_rc bit ensure that if everyone 
            // else tried to -release while we worked, the last one would block.
            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }
    //此時(shí)extra_rc中值為0,散列表中也是空的,則直接進(jìn)行析構(gòu),即自動(dòng)觸發(fā)dealloc流程
    // Really deallocate.
    //觸發(fā)dealloc的時(shí)機(jī)
    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        //發(fā)送一個(gè)dealloc消息
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

所以,綜上所述,release的底層流程如下圖所示

29-release底層流程圖示.png

dealloc 源碼分析

retainrelease的底層實(shí)現(xiàn)中,都提及了dealloc析構(gòu)函數(shù),下面來(lái)分析dealloc的底層的實(shí)現(xiàn)

  • 進(jìn)入dealloc -> _objc_rootDealloc -> rootDealloc源碼實(shí)現(xiàn),主要有兩件事:
    • 根據(jù)條件判斷是否有isa、cxx、關(guān)聯(lián)對(duì)象、弱引用表、引用計(jì)數(shù)表,如果沒(méi)有,則直接free釋放內(nèi)存
    • 如果有,則進(jìn)入object_dispose方法
inline void
objc_object::rootDealloc()
{
    //對(duì)象要釋放,需要做哪些事情?
    //1、isa - cxx - 關(guān)聯(lián)對(duì)象 - 弱引用表 - 引用計(jì)數(shù)表
    //2、free
    if (isTaggedPointer()) return;  // fixme necessary?

    //如果沒(méi)有這些,則直接free
    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        //如果有
        object_dispose((id)this);
    }
}
  • 進(jìn)入object_dispose源碼,其目的有以下幾個(gè)
    • 銷(xiāo)毀實(shí)例,主要有以下操作
      • 調(diào)用c++析構(gòu)函數(shù)

      • 刪除關(guān)聯(lián)引用

      • 釋放散列表

      • 清空弱引用表

    • free釋放內(nèi)存
id 
object_dispose(id obj)
{
    if (!obj) return nil;
    //銷(xiāo)毀實(shí)例而不會(huì)釋放內(nèi)存
    objc_destructInstance(obj);
    //釋放內(nèi)存
    free(obj);

    return nil;
}
??
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        //調(diào)用C ++析構(gòu)函數(shù)
        if (cxx) object_cxxDestruct(obj);
        //刪除關(guān)聯(lián)引用
        if (assoc) _object_remove_assocations(obj);
        //釋放
        obj->clearDeallocating();
    }

    return obj;
}
??
inline void 
objc_object::clearDeallocating()
{
    //判斷是否為nonpointer isa
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        //如果不是,則直接釋放散列表
        sidetable_clearDeallocating();
    }
    //如果是,清空弱引用表 + 散列表
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}
??
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        //清空弱引用表
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        //清空引用計(jì)數(shù)
        table.refcnts.erase(this);
    }
    table.unlock();
}

所以,綜上所述,dealloc底層的流程圖如圖所示

image

所以,到目前為止,從最開(kāi)始的alloc底層分析(見(jiàn)iOS-底層原理 02:alloc & init & new 源碼分析)-> retain -> release -> dealloc就全部串聯(lián)起來(lái)了

retainCount 源碼分析

引用計(jì)數(shù)的分析通過(guò)一個(gè)面試題來(lái)說(shuō)明

面試題:alloc創(chuàng)建的對(duì)象的引用計(jì)數(shù)為多少?

  • 定義如下代碼,打印其引用計(jì)數(shù)
NSObject *objc = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));

打印結(jié)果如下


image
  • 進(jìn)入retainCount -> _objc_rootRetainCount -> rootRetainCount源碼,其實(shí)現(xiàn)如下
- (NSUInteger)retainCount {
    return _objc_rootRetainCount(self);
}
??
uintptr_t
_objc_rootRetainCount(id obj)
{
    ASSERT(obj);

    return obj->rootRetainCount();
}
??
inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    //如果是nonpointer isa,才有引用計(jì)數(shù)的下層處理
    if (bits.nonpointer) {
        //alloc創(chuàng)建的對(duì)象引用計(jì)數(shù)為0,包括sideTable,所以對(duì)于alloc來(lái)說(shuō),是 0+1=1,這也是為什么通過(guò)retaincount獲取的引用計(jì)數(shù)為1的原因
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }
    //如果不是,則正常返回
    sidetable_unlock();
    return sidetable_retainCount();
}

在這里我們可以通過(guò)源碼斷點(diǎn)調(diào)試,來(lái)查看此時(shí)的extra_rc的值,結(jié)果如下

image

答案:綜上所述,alloc創(chuàng)建的對(duì)象實(shí)際的引用計(jì)數(shù)為0,其引用計(jì)數(shù)打印結(jié)果為1,是因?yàn)樵诘讓?code>rootRetainCount方法中,引用計(jì)數(shù)默認(rèn)+1了,但是這里只有對(duì)引用計(jì)數(shù)的讀取操作,是沒(méi)有寫(xiě)入操作的,簡(jiǎn)單來(lái)說(shuō)就是:為了防止alloc創(chuàng)建的對(duì)象被釋放(引用計(jì)數(shù)為0會(huì)被釋放),所以在編譯階段,程序底層默認(rèn)進(jìn)行了+1操作。實(shí)際上在extra_rc中的引用計(jì)數(shù)仍然為0

總結(jié)

  • alloc創(chuàng)建的對(duì)象沒(méi)有retain和release

  • alloc創(chuàng)建對(duì)象的引用計(jì)數(shù)為0,會(huì)在編譯時(shí)期,程序默認(rèn)加1,所以讀取引用計(jì)數(shù)時(shí)為1

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

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

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