前言
作為一名iOS開發(fā)者,內(nèi)存的的只是儲備是必不可少的,這篇文章會帶領(lǐng)我們探索iOS的內(nèi)存管理,繼續(xù)往下走吧。
準(zhǔn)備工作
1. 內(nèi)存布局
內(nèi)存五大分區(qū): 棧區(qū)、堆區(qū)、全局區(qū)、常量區(qū)、代碼區(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)
- 棧區(qū)是由
- 優(yōu)缺點(diǎn)
-
優(yōu)點(diǎn):因為棧是由編譯器自動分配并釋放的,不會產(chǎn)生內(nèi)存碎片,所以快速高效 -
缺點(diǎn):棧的內(nèi)存大小有限制,數(shù)據(jù)不靈活 -
iOS主線程棧大小是1MB,其他主線程是512KB,MAC只有8M
-
- 特點(diǎn)
注意:傳入函數(shù)的參數(shù)值、函數(shù)體內(nèi)聲明的局部變量等,由編譯器自動分配釋放,通常在函數(shù)執(zhí)行結(jié)束后就釋放了(不包括static修飾的變量,static意味該變量存放在全局/靜態(tài)區(qū))。
在Threading Programming Guide中有相關(guān)內(nèi)存大小的說明,如下:

-
堆區(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語言中使用malloc、calloc、realloc分配的空間,需要free釋放
- 堆區(qū)是由程序員
- 優(yōu)缺點(diǎn)
-
優(yōu)點(diǎn):靈活方便,數(shù)據(jù)適應(yīng)面廣泛 -
缺點(diǎn):需手動管理,速度慢、容易產(chǎn)生內(nèi)存碎片
-
- 特點(diǎn)
注意:當(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ū)是編譯時分配的內(nèi)存空間,在
-
代碼區(qū)(代碼段)- 代碼區(qū)是編譯時分配主要用于存放程序
運(yùn)行時的代碼,代碼會被編譯成二進(jìn)制存進(jìn)內(nèi)存 - 代碼區(qū)需要
防止在運(yùn)行時被非法修改,所以只準(zhǔn)許讀取操作,而不允許寫入(修改)操作——它是不可寫的。
- 代碼區(qū)是編譯時分配主要用于存放程序
補(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é)果如下:

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é)果如下:

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é)果如下:

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),
str1和str4的區(qū)別,str1的類型是NSTaggedPointerString,而str4是__NSCFString類型。同時通過控制臺輸出地址發(fā)現(xiàn),其余堆區(qū)的地址也有很大的區(qū)別,如下:
2.2 案例分析
通過以下的案例,我們繼續(xù)分析他們之間的區(qū)別。
-
案例1
案例1 -
案例2
案例2 -
運(yùn)行結(jié)果
分別運(yùn)行上面兩個案例,得出的結(jié)果分別是-
案例1出現(xiàn)報錯 -
案例2能夠正常運(yùn)行
-
打開匯編,查看案例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)容,如下:

通過
initializeTaggedPointerObfuscator方法,實現(xiàn)TaggedPointer指針混淆器的初始化,實現(xiàn)源碼如下:
也就是說,上面案例中,我們通過
%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,其余都為0的64位數(shù)值。也就是說如果一個對象的高位地址是1,則視為小對象。
引入案例進(jìn)行分析:

通過上面的案例的輸出結(jié)構(gòu),基本可以確定,高位的
0xa代表NSString,0xb代表NSNumber,0xe代表NSDate。我們來還原一下:
0xa -> 10100xb -> 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ù)探索,如下:

在小對象類型進(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 -> 010NSNumber = 3 -> 011NSDate = 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ù)值。分析過程如下:
通過上面可以看出,小對象的指針包含了
對象類型,對象的值,對象的長度信息。
2.4 總結(jié)
通過解讀源碼和案例的分析,我們發(fā)現(xiàn)小對象在進(jìn)行釋放操作時會被過濾,不會執(zhí)行相關(guān)的釋放流程,其是存儲在常量區(qū),并不會進(jìn)行內(nèi)存的申請和釋放,效率高更高了。
3. 引用計數(shù)
我們知道內(nèi)存管理方案分為MRC和ARC,但是不管是哪種方案,都是對引用計數(shù)的處理,這些方法涉及:alloc、dealloc、realease、retain、retainCount、autorealease等。
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。見下圖:

retain也會對對象的引用計數(shù)進(jìn)行操作,下面從retain方法開始分析。
3.1 retain方法
找到retain方法的實現(xiàn)源碼,如下:

其調(diào)用了
rootRetain方法,查找rootRetain的實現(xiàn)源碼,如下:
處理好
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)行isa中extra_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)源碼,如下:

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

其中的核心操作代碼如下:
// 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方法,代碼如下:

首先判斷其是否為小對象,小對象不需要處理,因為系統(tǒng)會自動幫我們釋放掉。同時通過對象的
isa判斷是否為nonpointer isa,如果是繼續(xù)判斷其是否能有弱引用、是否存在關(guān)聯(lián)對象、是否存在析構(gòu)、是否存在散列表。如果不存在上面的內(nèi)容則會調(diào)用free方法,將對象釋放。如果存在,則調(diào)用object_dispose方法。見下面代碼:
object_dispose會調(diào)用objc_destructInstance方法,其實現(xiàn)如下:
如果
存在關(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]中也做了說明,如下所示:
3.4 retainCount
獲取引用計數(shù)最終調(diào)用的是rootretainCount方法,源碼實現(xiàn)見下圖:

首先判斷是否為否為小對象,小對象不做引用計數(shù)處理。如果是
nonpointer isa,首先從isa指針中獲取extra_rc數(shù)值,同時判斷是否存在散列表,如果存在,則再加上散列表中的數(shù)值。如果不是nonpointer isa,直接獲取對象對應(yīng)SideTable中的引用計數(shù)。
總結(jié)
這篇文章針對iOS內(nèi)存管理先探索到這里了,內(nèi)存管理還有很多的內(nèi)容,我們繼續(xù)往后探索。繼續(xù)努力!




