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

本文主要是分析內(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)存管理方案除了上面提及的MRCARC既忆,還有以下三種

  • 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 PointerSideTables绘雁,我們通過(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è)mask0101 1000

    1010 0001 
   ^0101 1000 mask(編碼)
    1111 1001
   ^0101 1000 mask(解碼)
    1010 0001

所以在外界,為了獲取小對(duì)象的真實(shí)地址姚建,我們可以將解碼的源碼拷貝到外面矫俺,將NSString混淆部分進(jìn)行解碼,如下所示

觀察解碼后的小對(duì)象地址掸冤,其中的62表示bASCII碼厘托,再以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)

4.1 retain 源碼分析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市艾杏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌盅藻,老刑警劉巖购桑,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異氏淑,居然都是意外死亡勃蜘,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門假残,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)缭贡,“玉大人,你說(shuō)我怎么就攤上這事辉懒⊙羧牵” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵眶俩,是天一觀的道長(zhǎng)莹汤。 經(jīng)常有香客問(wèn)我,道長(zhǎng)颠印,這世上最難降的妖魔是什么纲岭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任抹竹,我火速辦了婚禮,結(jié)果婚禮上止潮,老公的妹妹穿的比我還像新娘窃判。我一直安慰自己,他們只是感情好喇闸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布袄琳。 她就那樣靜靜地躺著,像睡著了一般仅偎。 火紅的嫁衣襯著肌膚如雪跨蟹。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,115評(píng)論 1 296
  • 那天橘沥,我揣著相機(jī)與錄音窗轩,去河邊找鬼。 笑死座咆,一個(gè)胖子當(dāng)著我的面吹牛痢艺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播介陶,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼堤舒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了哺呜?” 一聲冷哼從身側(cè)響起舌缤,我...
    開(kāi)封第一講書(shū)人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎某残,沒(méi)想到半個(gè)月后国撵,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡玻墅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年介牙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片澳厢。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡环础,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出剩拢,到底是詐尸還是另有隱情线得,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布徐伐,位于F島的核電站框都,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜魏保,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一熬尺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谓罗,春花似錦粱哼、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至刻蚯,卻和暖如春绊含,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背炊汹。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工躬充, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人讨便。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓充甚,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親霸褒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子伴找,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容