iOS 底層 - 內(nèi)存管理之Tagged Pointer

本文源自本人的學(xué)習(xí)記錄整理與理解跨嘉,其中參考閱讀了部分優(yōu)秀的博客和書籍兆览,盡量以通俗簡(jiǎn)單的語(yǔ)句轉(zhuǎn)述勃黍。引用到的地方如有遺漏或未能一一列舉原文出處還望見(jiàn)諒與指出踱蛀,另文章內(nèi)容如有不妥之處還望指教,萬(wàn)分感謝 馋辈!

關(guān)于TaggedPointer

  • 從64bit開(kāi)始抚芦,IOS引入了Tagged Pointer技術(shù);用于優(yōu)化NSNumberNSDate叉抡、NSString 等小對(duì)象(OC對(duì)象)的存儲(chǔ)尔崔;
    Tagged Pointer指針的值不再是地址了,而是真正的值褥民。所以季春,實(shí)際上它不再是一個(gè)對(duì)象了,它只是一個(gè)披著對(duì)象皮的普通變量而已消返。所以载弄,它的內(nèi)存并不存儲(chǔ)在堆中,也不需要malloc和free
    在內(nèi)存讀取上有著3倍的效率撵颊,創(chuàng)建時(shí)比以前快106倍

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ō)

比如:NSString 就會(huì)被優(yōu)化成NSTaggedPointerString類型

  • 注意:Tagged Pointer技術(shù)不會(huì)運(yùn)用在可變類型對(duì)象词身,比如:NSMutableString

  • 在沒(méi)有使用Tagged Pointer 之前厅目,NSNumber等對(duì)象需要?jiǎng)討B(tài)分配內(nèi)存、維護(hù)引用計(jì)數(shù)等法严,NSNumber指針存儲(chǔ)的是堆空間NSNumber對(duì)象的地址值

  • 使用Tagged Pointer 技術(shù)之后损敷,NSNumber指針里面存儲(chǔ)的數(shù)據(jù)變成了:Tag+Data (Tag 是一個(gè)標(biāo)記信息:數(shù)據(jù)類型),也就是NSNumber指針存儲(chǔ)的是NSNumber對(duì)象的數(shù)據(jù)

  • 當(dāng)對(duì)象指針地址的最高有效位是1,則該指針為Tagged Pointer
    比如:十六進(jìn)制0x7 轉(zhuǎn)換成二進(jìn)制0b1110深啤;十六進(jìn)制0x6 轉(zhuǎn)換成二進(jìn)制0b1100拗馒;最左邊數(shù)字是最高有效位 ;總體來(lái)說(shuō)NSNumber溯街、NSDate诱桂、NSString內(nèi)存地址十六進(jìn)制最后一位是只要不是0,就是Tagged Pointer指針

  • 當(dāng)指針不夠存儲(chǔ)這些數(shù)據(jù)是呈昔,才會(huì)使用動(dòng)態(tài)分配內(nèi)存的方式來(lái)存儲(chǔ)數(shù)據(jù)挥等;即將之前的直接動(dòng)態(tài)分配內(nèi)存,改變?yōu)橄劝褦?shù)據(jù)加上tag值存儲(chǔ)在指針中堤尾,如果不夠再動(dòng)態(tài)分配

  • Tagged Pointer指針的賦值過(guò)程是不會(huì)走setter方法和getter方法的

  • Tagged Pointer是由編譯器來(lái)實(shí)現(xiàn)肝劲,開(kāi)發(fā)者全程無(wú)感

示例代碼:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        NSNumber   *number = @10;
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

*圖解:

number-taggedPointer.png
  • 十進(jìn)制的10 轉(zhuǎn)化成十六進(jìn)制就是 0xa

優(yōu)點(diǎn):

  • Tagged Pointer技術(shù)節(jié)省了內(nèi)存空間開(kāi)銷,本來(lái)正常的局部變量的NSNumber指針占8字節(jié),堆空間動(dòng)態(tài)分配內(nèi)存至少16字節(jié)辞槐;這樣看來(lái)需要在兩塊內(nèi)存區(qū)域分配共24字節(jié)掷漱;但是Tagged Pointer技術(shù)可以實(shí)現(xiàn)只在棧空間分配8字節(jié)就可以榄檬。
  • 在方法調(diào)用方面也變的更簡(jiǎn)單切威,objc_msgSend能夠識(shí)別Tagged Pointer,比如NSNumber的intValue方法丙号,直接從指針提取數(shù)據(jù)先朦,節(jié)省了以前的調(diào)用開(kāi)銷

示例代碼二:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSNumber *number1 = @10;
    NSNumber *number2 = @11;
    NSNumber *number3 = @12;

    NSLog(@"\n         %p \n         %p \n         %p", number1, number2, number3);
}
圖解.png

疑問(wèn):如何判斷一個(gè)指針是否是Tagged Pointer ?

  • iOS 平臺(tái),最高有效位是1 (第64bit)
  • Mac平臺(tái)犬缨,最低有效位是1
  • 這里的有效位是二進(jìn)制數(shù)
底層源碼判斷.png
底層源碼解讀.png
  • 通過(guò)retainCount判斷喳魏,如果retainCount是一個(gè)非常大的值那么就是Tagged Pointer類型,本質(zhì)上這個(gè)值就是該指針地址轉(zhuǎn)成的十進(jìn)制數(shù) (注意:這里十進(jìn)制數(shù)值是通過(guò) uintptr_t retain_count = (uintptr_t)this; 得到的怀薛,不是直接調(diào)用retainCount方法)

還有一個(gè)笨辦法 刺彩!

讓指針對(duì)象調(diào)用一個(gè)在編譯階段不報(bào)錯(cuò),運(yùn)行時(shí)就報(bào)錯(cuò)的方法枝恋;看崩潰日志......

比如:

崩潰日志.png

如何區(qū)分內(nèi)存地址是堆控件還是棿淳螅空間 ?

十六進(jìn)制看最后一位是否是0焚碌,如果是0那就是堆空間地址畦攘;否則就是棧空間地址十电;

log.png

NSTaggedPointerString

Tagged Pointer是一個(gè)很棒的技術(shù)知押,能把它運(yùn)用在字符串上很不尋常。顯然Apple花了很多心思在這上面鹃骂,他們必須要看到一個(gè)顯著的好處台盯。看他們?nèi)绾伟堰@些技術(shù)融合在一起畏线,看他們?nèi)绾卧诜浅S邢薜目臻g里面盡可能的存儲(chǔ)信息静盅,實(shí)在有趣。

做了什么 寝殴?

利用有限的空間蒿叠,把小數(shù)據(jù)存儲(chǔ)在指針內(nèi)部

有什么用 ?

  • 提高訪問(wèn)速度杯矩,對(duì)性能提升很有幫助
  • 節(jié)省了內(nèi)存空間
  • 簡(jiǎn)單來(lái)說(shuō):體量更小栈虚,??跑的更快 !

用什么編碼方式實(shí)現(xiàn) 史隆?

NSTaggedPointerString的存儲(chǔ)有三種編碼方式:ASCII碼魂务,六位編碼五位編碼
ASCII碼:除去第一位和最后一位粘姜,用8位的ascll碼的話最多可以存儲(chǔ)7個(gè)字符鬓照。字符串?dāng)?shù)目0~7之間
六位編碼: 六位二進(jìn)制編碼, (144)/6=9.333… ;最多存儲(chǔ)9位字符孤紧。字符數(shù)目在8~9使用
五位編碼:五位二進(jìn)制編碼豺裆,(14
4)/5 = 11.2;最多存儲(chǔ)11位字符号显。字符數(shù)目在10~11使用

神秘的表:

完整的六位編碼表是:

`eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX`

一個(gè)顯然的問(wèn)題是:為什么這個(gè)指令這么奇怪?

因?yàn)檫@個(gè)表同時(shí)用于六位編碼和五位編碼臭猜,它不完全按字母順序排列是有意義的。最常使用的字符應(yīng)該是上半部分押蚤,而較少使用的字符應(yīng)該在下半部分蔑歌。這可以確保盡可能多的長(zhǎng)字符串可以使用五位編碼。

表中前幾個(gè)字母與字母出現(xiàn)在英語(yǔ)里的頻率相似揽碘。最常見(jiàn)的英文字母是E次屠,然后是T,A雳刺,O劫灶,I,N掖桦,S本昏。E作為表的開(kāi)頭是正確的,其他的則靠近開(kāi)頭滞详。表似乎是按使用頻率排序凛俱。與英語(yǔ)的差異可能是因?yàn)镃ocoa APP中的短字符串并不是一個(gè)從英語(yǔ)散文中隨機(jī)選擇的單詞,而是更專業(yè)的語(yǔ)言料饥。

但這并不是一成不變的,apple會(huì)持續(xù)更新并統(tǒng)計(jì)字母使用頻率朱监,系統(tǒng)每次升級(jí)都可能不一樣岸啡,當(dāng)前第一位是字母e,之后是i,l赫编,o巡蘸,t…;這兩種編碼是從左向右的擂送;
根據(jù)編碼位數(shù)我們顯然也能推測(cè)出并不是所有字符都可以進(jìn)行ASCII或者六位五位編碼的悦荒,當(dāng)出現(xiàn)這樣不能編碼(搜狗emoji顏文字嘹吨、符號(hào)表情)的時(shí)候搬味,系統(tǒng)也就不會(huì)使用NSTaggedPointerString類。

舉例證明:

#   define _OBJC_TAG_MASK (1UL<<63)
static inline bool objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

- (void)test10 {
    
    NSString *string1 = [NSString stringWithFormat:@"????"];
    NSString *string2 = [NSString stringWithFormat:@"* ̄? ̄*)"];
    NSString *string3 = [NSString stringWithFormat:@"222"];
    
    BOOL isTagP1 = objc_isTaggedPointer((__bridge const void * _Nullable)(string1));
    BOOL isTagP2 = objc_isTaggedPointer((__bridge const void * _Nullable)(string2));
    BOOL isTagP3 = objc_isTaggedPointer((__bridge const void * _Nullable)(string3));

    if (isTagP1) {
        NSLog(@"??? string1是TaggedPointer指針");
    }else {
        NSLog(@"??? string1不是TaggedPointer指針");
    }
    
    if (isTagP2) {
        NSLog(@"??? string2是TaggedPointer指針");
    }else {
        NSLog(@"??? string2不是TaggedPointer指針");
    }

    if (isTagP3) {
        NSLog(@"??? string3是TaggedPointer指針");
    }else {
        NSLog(@"??? string3不是TaggedPointer指針");
    }

}

??? string1不是TaggedPointer指針
??? string2不是TaggedPointer指針
??? string3是TaggedPointer指針

  ### retainCount 的底層實(shí)現(xiàn) 
   inline uintptr_t  objc_object::rootRetainCount() {

    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED);
    if (bits.nonpointer) {
        uintptr_t rc = bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

開(kāi)發(fā)中那些字符串符合TaggedPointer呢 ?

  • 對(duì)于以前的@""符號(hào)創(chuàng)建的字符串還是常量字符串碰纬,這個(gè)沒(méi)有改變萍聊,但是采用 stringWithFormat:stringWithUTF8String:悦析、stringWithString: 等NSString方法的創(chuàng)建字符串對(duì)象則有了區(qū)別寿桨。
  • 用這些方法創(chuàng)建的字符串字符數(shù)目在11位之內(nèi)的會(huì)被存儲(chǔ)在TaggedPointer指針內(nèi)部

參考優(yōu)秀文章:

【譯】采用Tagged Pointer的字符串

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市强戴,隨后出現(xiàn)的幾起案子亭螟,更是在濱河造成了極大的恐慌,老刑警劉巖骑歹,帶你破解...
    沈念sama閱讀 222,946評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件预烙,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡陵刹,警方通過(guò)查閱死者的電腦和手機(jī)默伍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)衰琐,“玉大人也糊,你說(shuō)我怎么就攤上這事∠壑妫” “怎么了狸剃?”我有些...
    開(kāi)封第一講書人閱讀 169,716評(píng)論 0 364
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)狗热。 經(jīng)常有香客問(wèn)我钞馁,道長(zhǎng),這世上最難降的妖魔是什么匿刮? 我笑而不...
    開(kāi)封第一講書人閱讀 60,222評(píng)論 1 300
  • 正文 為了忘掉前任僧凰,我火速辦了婚禮,結(jié)果婚禮上熟丸,老公的妹妹穿的比我還像新娘训措。我一直安慰自己,他們只是感情好光羞,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,223評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布绩鸣。 她就那樣靜靜地躺著,像睡著了一般纱兑。 火紅的嫁衣襯著肌膚如雪呀闻。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 52,807評(píng)論 1 314
  • 那天潜慎,我揣著相機(jī)與錄音捡多,去河邊找鬼蓖康。 笑死,一個(gè)胖子當(dāng)著我的面吹牛局服,可吹牛的內(nèi)容都是我干的钓瞭。 我是一名探鬼主播,決...
    沈念sama閱讀 41,235評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼淫奔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼山涡!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起唆迁,我...
    開(kāi)封第一講書人閱讀 40,189評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鸭丛,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后唐责,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鳞溉,經(jīng)...
    沈念sama閱讀 46,712評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,775評(píng)論 3 343
  • 正文 我和宋清朗相戀三年鼠哥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了熟菲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,926評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡朴恳,死狀恐怖抄罕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情于颖,我是刑警寧澤呆贿,帶...
    沈念sama閱讀 36,580評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站森渐,受9級(jí)特大地震影響做入,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜同衣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,259評(píng)論 3 336
  • 文/蒙蒙 一竟块、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧耐齐,春花似錦彩郊、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,750評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)恕出。三九已至询枚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浙巫,已是汗流浹背金蜀。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,867評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工刷后, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人渊抄。 一個(gè)月前我還...
    沈念sama閱讀 49,368評(píng)論 3 379
  • 正文 我出身青樓尝胆,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親护桦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子含衔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,930評(píng)論 2 361

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