本文主要是分析內(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í)管理
ARCARC模式是在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)核區(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ū)blockARC模式下,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)存管理方案除了前文提及的MRC和ARC,還有以下三種
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 Pointer 和SideTables,我們通過(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)試看出

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é)果

所以,從上面可以總結(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_retain、objc_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采用了混淆處理,如下所示

- 我們可以在源碼中通過(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è)mask為 0101 1000
1010 0001
^0101 1000 mask(編碼)
1111 1001
^0101 1000 mask(解碼)
1010 0001
- 所以在外界,為了
獲取小對(duì)象的真實(shí)地址,我們可以將解碼的源碼拷貝到外面,將NSString混淆部分進(jìn)行解碼,如下所示
image
觀察解碼后的小對(duì)象地址,其中的62表示b的ASCII碼,再以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

- 同理,我們可以定義一個(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張

問(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的底層流程如下所示

總結(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的底層流程如下圖所示

dealloc 源碼分析
在retain和release的底層實(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方法
- 根據(jù)條件
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)存
- 銷(xiāo)毀實(shí)例,主要有以下操作
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底層的流程圖如圖所示

所以,到目前為止,從最開(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é)果如下

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

答案:綜上所述,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和releasealloc創(chuàng)建對(duì)象的引用計(jì)數(shù)為0,會(huì)在編譯時(shí)期,程序默認(rèn)加1,所以讀取引用計(jì)數(shù)時(shí)為1





