iOS 內(nèi)存管理--內(nèi)存五大區(qū)、TiggedPointer、引用計數(shù)

前言

作為一名iOS開發(fā)者,內(nèi)存的的只是儲備是必不可少的,這篇文章會帶領(lǐng)我們探索iOS的內(nèi)存管理,繼續(xù)往下走吧。

準(zhǔn)備工作

1. 內(nèi)存布局

內(nèi)存五大分區(qū): 棧區(qū)、堆區(qū)、全局區(qū)、常量區(qū)、代碼區(qū)

內(nèi)存五大區(qū)

1.1 內(nèi)存五大區(qū)

  • 棧區(qū)--stack
    • 特點(diǎn)
      • 棧是系統(tǒng)數(shù)據(jù)結(jié)構(gòu),其對應(yīng)的進(jìn)程或者線程是唯一
      • 棧是向低地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)
      • 棧是一塊連續(xù)的內(nèi)存區(qū)域,遵循先進(jìn)后出FILO)原則
      • 棧的地址空間在iOS中是以0X7開頭
      • 棧區(qū)一般在運(yùn)行時分配
    • 存儲內(nèi)容
      • 棧區(qū)是由編譯器自動分配并釋放的,主要用來存儲局部變量
      • 函數(shù)的參數(shù),例如函數(shù)的隱藏參數(shù)(id self,SEL _cmd
    • 優(yōu)缺點(diǎn)
      • 優(yōu)點(diǎn):因為棧是由編譯器自動分配并釋放的,不會產(chǎn)生內(nèi)存碎片,所以快速高效
      • 缺點(diǎn):棧的內(nèi)存大小有限制數(shù)據(jù)不靈活
      • iOS主線程棧大小是1MB,其他主線程是512KBMAC只有8M

注意:傳入函數(shù)的參數(shù)值、函數(shù)體內(nèi)聲明的局部變量等,由編譯器自動分配釋放,通常在函數(shù)執(zhí)行結(jié)束后就釋放了(不包括static修飾的變量,static意味該變量存放在全局/靜態(tài)區(qū))。

Threading Programming Guide中有相關(guān)內(nèi)存大小的說明,如下:

內(nèi)存大小相關(guān)說明

  • 堆區(qū)--heep
    • 特點(diǎn)
      • 堆是向高地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)
      • 堆是不連續(xù)的內(nèi)存區(qū)域,類似于鏈表結(jié)構(gòu)便于增刪,不便于查詢),
      • 遵循先進(jìn)先出FIFO)原則
      • 堆的地址空間在iOS中是以0x6開頭,其空間的分配總是動態(tài)
      • 堆區(qū)的分配一般是在運(yùn)行時分配
    • 存儲內(nèi)容
      • 堆區(qū)是由程序員動態(tài)分配釋放的,如果程序員不釋放,程序結(jié)束后,可能由操作系統(tǒng)回收
      • OC中使用alloc或者使用new開辟空間創(chuàng)建對象
      • C語言中使用malloccalloc、realloc分配的空間,需要free釋放
    • 優(yōu)缺點(diǎn)
      • 優(yōu)點(diǎn):靈活方便,數(shù)據(jù)適應(yīng)面廣泛
      • 缺點(diǎn):需手動管理,速度慢、容易產(chǎn)生內(nèi)存碎片

注意:當(dāng)需要訪問堆中內(nèi)存時,一般需要先通過對象讀取到棧區(qū)的指針地址,然后通過指針地址訪問堆區(qū)。因為現(xiàn)在iOS基本都使用ARC來管理對象,所以也不需要手動釋放

  • 全局區(qū)(靜態(tài)區(qū))(BSS段)

    • BSS段bss segment)通常是指用來存放程序中未初始化的或者初始值為0全局變量的一塊內(nèi)存區(qū)域。BSS是英文Block Started by Symbol的簡稱。BSS段屬于靜態(tài)內(nèi)存分配。
    • 數(shù)據(jù)段:數(shù)據(jù)段(data segment)通常是指用來存放程序中已初始化的全局變量的一塊內(nèi)存區(qū)域,數(shù)據(jù)段屬于靜態(tài)內(nèi)存分配。
    • 全局區(qū)是編譯時分配的內(nèi)存空間,在iOS中一般以0x1開頭,在程序運(yùn)行過程中,此內(nèi)存中的數(shù)據(jù)一直存在,程序結(jié)束后由系統(tǒng)釋放,主要存放
      • 未初始化的全局變量和靜態(tài)變量,即BSS區(qū).bss
      • 已初始化的全局變量和靜態(tài)變量,即數(shù)據(jù)區(qū).data
    • static修飾的變量會成為靜態(tài)變量,該變量的內(nèi)存由全局/靜態(tài)區(qū)編譯階段完成分配,且僅分配一次。
    • static可以修飾局部變量也可以修飾全局變量
  • 常量區(qū)(數(shù)據(jù)段)

    • 常量區(qū)是編譯時分配的內(nèi)存空間,在iOS中一般以0x1開頭,在程序結(jié)束后由系統(tǒng)釋放
    • 通常是指用來存放程序中已經(jīng)初始化的全局變量和靜態(tài)變量的一塊內(nèi)存區(qū)域。數(shù)據(jù)段屬于靜態(tài)內(nèi)存分配,可以分為只讀數(shù)據(jù)段讀寫數(shù)據(jù)段。字符串常量等,是放在只讀數(shù)據(jù)段中,結(jié)束程序時才會被收回。
  • 代碼區(qū)(代碼段)

    • 代碼區(qū)是編譯時分配主要用于存放程序運(yùn)行時的代碼,代碼會被編譯成二進(jìn)制存進(jìn)內(nèi)存
    • 代碼區(qū)需要防止在運(yùn)行時被非法修改,所以只準(zhǔn)許讀取操作,而不允許寫入(修改)操作——它是不可寫的。

補(bǔ)充:除了以上內(nèi)存區(qū)域外,系統(tǒng)還會保留一些內(nèi)存區(qū)域。

1.2 內(nèi)存分區(qū)驗證

1.2.1 棧區(qū)

驗證代碼如下:

- (void)testStack{
    NSLog(@"************棧區(qū)************");

    // 棧區(qū)
    int a = 10;
    int b = 20;
    NSObject *object = [NSObject new];

    NSLog(@"a == \t%p",&a);
    NSLog(@"b == \t%p",&b);
    NSLog(@"object == \t%p",&object);

    NSLog(@"%lu",sizeof(&object));
    NSLog(@"%lu",sizeof(a));
}

上面代碼中,a、b、object都是局部變量,這些變量都存儲在棧區(qū)。運(yùn)行結(jié)果如下:

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

1.2.2 堆區(qū)

驗證代碼如下:

- (void)testHeap{
    NSLog(@"************堆區(qū)************");
    // 堆區(qū)
    NSObject *object1 = [NSObject new];
    NSObject *object2 = [NSObject new];
    NSObject *object3 = [NSObject new];
    NSLog(@"object1 = %@",object1);
    NSLog(@"object2 = %@",object2);
    NSLog(@"object3 = %@",object3);
    // 訪問---通過對象->堆區(qū)地址->存在棧區(qū)的指針
}

代碼創(chuàng)建了三個變量,這三個變量都存儲在棧區(qū),這些變量存儲的指針都指向堆區(qū)的對象。運(yùn)行結(jié)果如下:

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

1.2.3 全局區(qū)、常量區(qū)

驗證代碼如下:

int clA;
int clB = 10;

static int bssA;
static NSString *bssStr1;
static int bssB = 10;
static NSString *bssStr2 = @"hello";
static NSString *name = @"name";

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"************棧區(qū)************");
    int sa = 10;
    NSLog(@"bssA == \t%p",&sa);
    NSLog(@"************全局區(qū)************");
    NSLog(@"clA == \t%p",&clA);
    NSLog(@"bssA == \t%p",&bssA);
    NSLog(@"bssStr1 == \t%p",&bssStr1);
    NSLog(@"clB == \t%p",&clB);
    NSLog(@"bssB == \t%p",&bssB);
    NSLog(@"bssStr2 == \t%p",&bssStr2);
    NSLog(@"bssStr2 == \t%p",&name);
}

通過打印全局區(qū)的變量的地址與棧區(qū)變量進(jìn)行對比,運(yùn)行結(jié)果如下:


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

2. TiggedPointer小對象

2.1 什么是小對象

iOS開發(fā)中,一個對象至少要8個字節(jié),但是對于一些數(shù)據(jù)來說是有些浪費(fèi)的,比如NSNumber、NSDate、NSString(小字符串)。所以64位環(huán)境下,引入了Tagged Pointer技術(shù),用一個小對象來存儲這些數(shù)據(jù)。以字符串為例,見下圖:

字符串小對象

通過以上案例發(fā)現(xiàn),str1str4的區(qū)別,str1的類型是NSTaggedPointerString,而str4__NSCFString類型。同時通過控制臺輸出地址發(fā)現(xiàn),其余堆區(qū)的地址也有很大的區(qū)別,如下:
運(yùn)行輸出

2.2 案例分析

通過以下的案例,我們繼續(xù)分析他們之間的區(qū)別。

  • 案例1
    案例1
  • 案例2
    案例2
  • 運(yùn)行結(jié)果
    分別運(yùn)行上面兩個案例,得出的結(jié)果分別是
    • 案例1出現(xiàn)報錯
    • 案例2能夠正常運(yùn)行

打開匯編,查看案例1報錯的信息,如下:

查看案例1報錯信息

分析其報錯的原因是壞內(nèi)存訪問,這是什么回事呢?

  • 原因分析
    set方法實際就是新值的retain舊值的release。由于nameStr修飾為nonatomic所以是線程不安全的。當(dāng)多條線程同時訪問,造成多次release,所以會出現(xiàn)壞內(nèi)存訪問

  • 解決方案
    修飾改為atomic或者加鎖。

  • 為什么案例2可以正常運(yùn)行呢?
    案例1中,設(shè)置斷點(diǎn),發(fā)現(xiàn)此時nameStr數(shù)據(jù)類型為__NSCFString,如下:

    查看案例1字符串類型

    而在案例2中,nameStr數(shù)據(jù)類型為TiggedPointer,如下:
    查看案例1字符串類型

  • 正常對象都是指針指向堆內(nèi)存中的地址,所以案例1會因為多線程訪問而造成壞內(nèi)存訪問,而TaggedPointer存儲在常量區(qū),不會創(chuàng)建內(nèi)存

  • 在進(jìn)行對象釋放時,針對TiggedPointer類型進(jìn)行了過濾處理,也就說TiggedPointer類型不會對引用計數(shù)進(jìn)行處理。

  • release方法源碼

    release源碼

2.3 TiggedPointer原理分析

在之前類加載原理分析中的_read_images方法中已經(jīng)探索到了TiggedPointer方面的內(nèi)容,如下:

_read_images

通過initializeTaggedPointerObfuscator方法,實現(xiàn)TaggedPointer指針混淆器的初始化,實現(xiàn)源碼如下:
initializeTaggedPointerObfuscator

也就是說,上面案例中,我們通過%p打印TaggedPointer對象地址時得到的內(nèi)容,是指針經(jīng)過混淆器換算后得到的結(jié)果。

接著根據(jù)objc_debug_taggedpointer_obfuscator方法,針對TaggedPointer對象的指針編碼解碼算法:,如下:


通過上面的算法可以發(fā)現(xiàn),編碼過程為

uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);

解碼過程為

value ^ objc_debug_taggedpointer_obfuscator;

找到了編碼解碼算法,我們可以將小對象輸出的地址進(jìn)行解碼,得到他原來的指針內(nèi)容。見下面處理流程:

小對象解碼

0xa000000000000621就是解碼得到的結(jié)果,這又有什么意義呢?請繼續(xù)往下探索。

2.3.1 TaggedPointer指針類型分析

TaggedPointer相關(guān)的源碼中,找到了下面這個代碼:

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

#if OBJC_SPLIT_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)

判斷一個對象是否為TaggedPointer類型,通過對象指針&_OBJC_TAG_MASK之后并等于_OBJC_TAG_MASK自己;而這個mask高位為1,其余都為064位數(shù)值。也就是說如果一個對象的高位地址是1,則視為小對象。

引入案例進(jìn)行分析:

案例分析

通過上面的案例的輸出結(jié)構(gòu),基本可以確定,高位的0xa代表NSString,0xb代表NSNumber,0xe代表NSDate。我們來還原一下:

  • 0xa -> 1010
  • 0xb -> 1011
  • 0xe -> 1110
    可以發(fā)現(xiàn)高位都是1,所以這些都是TaggedPointer類型,也就是小對象。那么如果移除高位的1,剩下的位就應(yīng)該是代表tag,即:
  • 0xa -> 1010 -> 010 表示NSString
  • 0xb -> 1011 -> 011 表示NSNumber
  • 0xe -> 1110 -> 110 表示NSDate

那么類型是不是這樣子標(biāo)記的呢?那么我們根據(jù)源碼繼續(xù)探索,如下:

_objc_makeTaggedPointer

在小對象類型進(jìn)行標(biāo)記時,傳入了objc_tag_index_t類型的tag,查看objc_tag_index_t的定義,如下:

enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
    // 60-bit payloads
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 
    ......
}
  • NSString = 2 -> 010
  • NSNumber = 3 -> 011
  • NSDate = 6 -> 110

結(jié)論:好明顯就是跟我們猜想的完全一致?。。?!小對象類型地址包含了類型!

2.3.2 TaggedPointer值分析

在上面分析中只是找出了小對象的標(biāo)記為以及小對象類型的tag,那么值存儲在哪里呢?請繼續(xù)往下走哦!
同樣我們引入一個案例:

案例分析

在上面的案例中,我們可以發(fā)現(xiàn)指針的末尾位表示小對象的長度。那么數(shù)值存儲在哪呢?WWDC的相關(guān)說明中提到,如需要獲取其內(nèi)部的數(shù)值,需要查看二進(jìn)制,按位獲取對應(yīng)的數(shù)值。分析過程如下:
獲取對應(yīng)的二進(jìn)制位

通過上面可以看出,小對象的指針包含了對象類型,對象的值對象的長度信息。

2.4 總結(jié)

通過解讀源碼和案例的分析,我們發(fā)現(xiàn)小對象在進(jìn)行釋放操作時會被過濾,不會執(zhí)行相關(guān)的釋放流程,其是存儲在常量區(qū),并不會進(jìn)行內(nèi)存的申請和釋放效率高更高了。

3. 引用計數(shù)

我們知道內(nèi)存管理方案分為MRCARC,但是不管是哪種方案,都是對引用計數(shù)的處理,這些方法涉及:alloc、dealloc、realeaseretain、retainCountautorealease等。
MRC環(huán)境下,需要我們手動調(diào)用這些方法,ARC環(huán)境,系統(tǒng)會自動幫我們調(diào)用。那么這些方法的實現(xiàn)原理是怎樣的呢?請繼續(xù)往下走。

(isa詳解)[http://www.itdecent.cn/p/6a295edebf69]文章中詳細(xì)說明了nonpointer isa,使用了結(jié)構(gòu)體位域,針對arm64架構(gòu)和x86架構(gòu)提供了不同的位域設(shè)置規(guī)則。其中包括了兩個重要的字段:has_sidetable_rc引用計數(shù)表和extra_rc對象引用計數(shù)。

那么它們之間的關(guān)系是怎么樣子的呢?alloc與retain方法肯定是繞不開的探索方向了,我們在前面的章節(jié)中,已經(jīng)分析了alloc的處理流程,完成isa的創(chuàng)建,并初始化引用計數(shù)為1。見下圖:

alloc流程

retain也會對對象的引用計數(shù)進(jìn)行操作,下面從retain方法開始分析。

3.1 retain方法

找到retain方法的實現(xiàn)源碼,如下:

retain

其調(diào)用了rootRetain方法,查找rootRetain的實現(xiàn)源碼,如下:
rootRetain

處理好rootRetain方法內(nèi)部的判斷邏輯關(guān)系,發(fā)現(xiàn)do while是這個方法的核心部分,我們就重點(diǎn)分析這一部分的內(nèi)容。

在方法的一開始就進(jìn)行了判斷,當(dāng)前對象是否為TaggedPointer類型,也就是小對象,如果是小對象直接返回不處理,所以小對象不進(jìn)行引用計數(shù)方面的處理,也不需要進(jìn)行內(nèi)存的開辟和釋放,由系統(tǒng)自動完成

do...while循環(huán)中進(jìn)行isa中引用計數(shù)相關(guān)的操作,在while判斷語句中,調(diào)用StoreExclusive方法完成新老isa的比對替換操作,成功后跳出循環(huán)。

在循環(huán)中,首先判斷如果不是nonpointer isa,則處理對象對應(yīng)的散列表SideTable的引用計數(shù)。見下面代碼:

    if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain(sideTableLocked);
    }
    
    id
    objc_object::sidetable_retain(bool locked)
    {
    #if SUPPORT_NONPOINTER_ISA
        ASSERT(!isa.nonpointer);
    #endif
        SideTable& table = SideTables()[this];
        if (!locked) table.lock();
        size_t& refcntStorage = table.refcnts[this];
        if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
            refcntStorage += SIDE_TABLE_RC_ONE;
        }
        table.unlock();
        
        return (id)this;
    }

流程是:從系統(tǒng)維護(hù)的SideTables中找到自己所在的散列表SideTable,再找到自己引用計數(shù)表的存儲空間,對自己的引用計數(shù)進(jìn)行加操作。

散列表的相關(guān)內(nèi)容,在分析弱引用的時候已經(jīng)說明:(iOS weak實現(xiàn)原理和銷毀過程)[http://www.itdecent.cn/p/1b566137b3fe]

如果以上內(nèi)容都不滿足,則會進(jìn)行isaextra_rc屬性的操作,也就是對引用計數(shù)加1,不同框架下extra_rc所在isa的位置不同,所以RC_ONE位域值也不同。見下面代碼:

    uintptr_t carry;
    newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  //extra_rc++

carry用于判斷extra_rc字段是否已經(jīng)存儲滿了,如果已滿,則執(zhí)行下面的這段代碼:

 if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (variant != RRVariant::Full) {
                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;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }

如果extra_rc已滿,會將extra_rc所能存儲的容量的一半放到,對象對應(yīng)的散列表中。見下面這段代碼:

  if (variant == RRVariant::Full) {
        if (slowpath(transcribeToSideTable)) {
            // Copy the other half of the retain counts to the side table.
            sidetable_addExtraRC_nolock(RC_HALF);
        }
....
}

sidetable_addExtraRC_nolock方法的源碼實現(xiàn)如下:

bool 
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    ASSERT(isa.nonpointer);
    SideTable& table = SideTables()[this];

    size_t& refcntStorage = table.refcnts[this];
    size_t oldRefcnt = refcntStorage;
    // isa-side bits should not be set here
    ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;

    uintptr_t carry;
    size_t newRefcnt = 
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
    if (carry) {
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {
        refcntStorage = newRefcnt;
        return false;
    }
}

在進(jìn)行散列表操作時進(jìn)行了的操作,這樣會影響性能,所以在extra_rc滿狀態(tài)下,會將其滿狀態(tài)的一半放到散列表中,避免頻繁操作散列表。同時extra_rc滿狀態(tài)也不是頻繁的出現(xiàn)slowpath(carry),所以滿狀態(tài)的一半已經(jīng)有相當(dāng)大的存儲空間了!

3.2 release方法

release的處理流程也就很容易理解了,對引用計數(shù)的反向操作。找到release的實現(xiàn)源碼,如下:

release

釋放時也會判斷當(dāng)前的對象是否為小對象TaggedPointer,如果是小對象就不需要對引用計數(shù)進(jìn)行處理。如果不是小對象則調(diào)用release方法。繼續(xù)跟蹤代碼,最終會調(diào)用到rootRelease方法,如下:
rootRelease

同樣其依然會進(jìn)行判斷是否為nopointerisa、是否正在釋放,如果不是,則進(jìn)行extra_rc減1操作,見下面代碼:

 uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }

同時也會設(shè)置一個標(biāo)記位carry,用于判斷extra_rc是否已經(jīng)被清空。如果此時extra_rc的引用計數(shù)值為0,則會走到underflow流程中。在underflow中,首先判斷該對象是否存在散列表,如果存在,則從散列表中移除一些引用計數(shù)到extra_rc中,見下面代碼:

underflow

其中的核心操作代碼如下:

    // Try to remove some retain counts from the side table.        
    auto borrow = sidetable_subExtraRC_nolock(RC_HALF);
    
    bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there
    
    // 設(shè)置extra_rc,并對散列表進(jìn)行設(shè)置,是否清空散列表
    newisa.extra_rc = borrow.borrowed - 1;  // redo the original decrement too
    newisa.has_sidetable_rc = !emptySideTable;

在此過程中,會將散列表中的一部分引用計數(shù)賦值到extra_rc中,同時,根據(jù)剩余引用數(shù),來設(shè)置散列表是否需要清空。如果此時散列表被設(shè)置為emptySideTable,空,則會調(diào)用sidetable_clearExtraRC_nolock方法將該SideTable從SideTables中抹除:

 if (emptySideTable)
                sidetable_clearExtraRC_nolock();

void
objc_object::sidetable_clearExtraRC_nolock()
{
    ASSERT(isa.nonpointer);
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    table.refcnts.erase(it);
}

當(dāng)extra_rc數(shù)值為空,散列表也被清除,則此時處于isDeallocating狀態(tài),會進(jìn)入deallocate流程中,發(fā)送dealloc消息,完成對象的釋放。

if (slowpath(newisa.isDeallocating()))
        goto deallocate;

deallocate:
    // Really deallocate.

    ASSERT(newisa.isDeallocating());
    ASSERT(isa.isDeallocating());

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
   // 發(fā)送dealloc消息,完成對象的釋放。
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

3.3 dealloc

dealloc最終會調(diào)用rootDealloc方法,代碼如下:

rootDealloc

首先判斷其是否為小對象,小對象不需要處理,因為系統(tǒng)會自動幫我們釋放掉。同時通過對象的isa判斷是否為nonpointer isa,如果是繼續(xù)判斷其是否能有弱引用、是否存在關(guān)聯(lián)對象、是否存在析構(gòu)、是否存在散列表。如果不存在上面的內(nèi)容則會調(diào)用free方法,將對象釋放。如果存在,則調(diào)用object_dispose方法。見下面代碼:
object_dispose

object_dispose會調(diào)用objc_destructInstance方法,其實現(xiàn)如下:
objc_destructInstance

如果存在關(guān)聯(lián)對象,通過_object_remove_assocations方法對關(guān)聯(lián)對象進(jìn)行釋放。此部分內(nèi)容在分類的(iOS 類擴(kuò)展&關(guān)聯(lián)對象)[http://www.itdecent.cn/p/4315c2440c56]中已經(jīng)做了分析,通過調(diào)用clearDeallocating方法,完成散列表弱引用表的釋放,此部分內(nèi)容在(iOS weak實現(xiàn)原理和銷毀過程)[http://www.itdecent.cn/p/1b566137b3fe]中也做了說明,如下所示:
clearDeallocating

3.4 retainCount

獲取引用計數(shù)最終調(diào)用的是rootretainCount方法,源碼實現(xiàn)見下圖:

rootretainCount

首先判斷是否為否為小對象,小對象不做引用計數(shù)處理。如果是nonpointer isa,首先從isa指針中獲取extra_rc數(shù)值,同時判斷是否存在散列表,如果存在,則再加上散列表中的數(shù)值。如果不是nonpointer isa,直接獲取對象對應(yīng)SideTable中的引用計數(shù)。

總結(jié)

這篇文章針對iOS內(nèi)存管理先探索到這里了,內(nèi)存管理還有很多的內(nèi)容,我們繼續(xù)往后探索。繼續(xù)努力!

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

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

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