本文主要是分析內(nèi)存管理中的內(nèi)存管理方案,以及retain
重贺、retainCount
骑祟、release
回懦、dealloc
的底層源碼分析
1. ARC & MRC
iOS中的內(nèi)存管理方案,大致可以分為兩類:MRC
(手動(dòng)內(nèi)存管理)和ARC
(自動(dòng)內(nèi)存管理)
1.1 MRC
在MRC時(shí)代次企,系統(tǒng)是通過(guò)對(duì)象的引用計(jì)數(shù)來(lái)判斷一個(gè)是否銷毀怯晕,有以下規(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ì)銷毀
這個(gè)對(duì)象
所以,在MRC模式下崇堵,必須遵守:誰(shuí)創(chuàng)建型诚,誰(shuí)釋放,誰(shuí)引用鸳劳,誰(shuí)管理
1.2 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
2. 內(nèi)存管理方案
內(nèi)存管理方案除了上面提及的MRC
和ARC
既忆,還有以下三種
Tagged Pointer
:專門用來(lái)處理小對(duì)象驱负,例如NSNumber、NSDate患雇、小NSString等Nonpointer_isa
:非指針類型的isa跃脊,主要是用來(lái)優(yōu)化64位地址,這個(gè)在iOS-底層原理7:isa與類關(guān)聯(lián)的原理一文中苛吱,已經(jīng)介紹了SideTables
:散列表
酪术,在散列表中主要有兩個(gè)表,分別是引用計(jì)數(shù)表
翠储、弱引用表
這里主要著重介紹Tagged Pointer
和SideTables
绘雁,我們通過(guò)一個(gè)面試題來(lái)引入Tagged Pointer
2.1 面試題
//*********代碼1*********
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("com.lbh.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"iOS"]; // alloc 堆 iOS優(yōu)化 - taggedpointer
NSLog(@"%@",self.nameStr);
});
}
}
運(yùn)行以上代碼,一切正常彰亥。
將上面額代碼稍微改動(dòng)下
//*********代碼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:@"iOS_努力學(xué)習(xí)底層咧七,不做代碼搬運(yùn)工"];
NSLog(@"%@",self.nameStr);
});
}
}
運(yùn)行程序,點(diǎn)擊屏幕任斋,程序會(huì)崩潰
問(wèn)題:
為什么會(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ǔ)在堆上
2.2 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)簽指針,是蘋果在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ū)
3. Tagged Pointer 小對(duì)象
由一個(gè)NSString
的面試題抡谐,引出了Tagged Pointer
,為了探索小對(duì)象的引用計(jì)數(shù)
處理桐猬,所以我們需要進(jìn)入objc源碼
中查看retain
麦撵、release
源碼 中對(duì) Tagged Pointer
小對(duì)象的處理
3.1 小對(duì)象的引用計(jì)數(shù)處理分析
step1:
查看setProperty
-> reallySetProperty
源碼,其中是對(duì)新值retain
溃肪,舊值release
step2:
進(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();
}
3.2 小對(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
方法
step1:
進(jìn)入_read_images
--> initializeTaggedPointerObfuscator
源碼實(shí)現(xiàn)
static void
initializeTaggedPointerObfuscator(void)
{
// sdkIsOlderThan(mac, ios, tv, watch, bridge)
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;
}
//在iOS12之后造烁,對(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)中午笛,我們可以看出惭蟋,在iOS12
之后,Tagged Pointer采用了混淆處理药磺,如下所示
step2:
我們可以在源碼中通過(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)行解碼
,如下所示
觀察解碼后的小對(duì)象地址掸冤,其中的62
表示b
的ASCII碼
厘托,再以NSNumber為例,同樣可以看出稿湿,1
就是我們實(shí)際的值
到這里铅匹,我們驗(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,符合上面的枚舉值
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初始化方式更加快速
4. SideTables 散列表
當(dāng)引用計(jì)數(shù)存儲(chǔ)到一定值
時(shí)键闺,并不會(huì)再存儲(chǔ)到Nonpointer_isa
的位域的extra_rc
中砾脑,而是會(huì)存儲(chǔ)到SideTables
散列表中
注意:
這里是針對(duì)源碼objc781
所講
下面我們就來(lái)繼續(xù)探索引用計(jì)數(shù)retain的底層實(shí)現(xiàn)