深入理解Tagged Pointer

前言

在 2013 年 9 月,蘋果推出了 iPhone5s,與此同時(shí),iPhone5s 配備了首個(gè)采用 64 位架構(gòu)的 A7 雙核處理器,為了節(jié)省內(nèi)存和提高執(zhí)行效率,蘋果提出了Tagged Pointer的概念。對(duì)于 64 位程序,引入 Tagged Pointer 后,相關(guān)邏輯能減少一半的內(nèi)存占用,以及 3 倍的訪問速度提升,100 倍的創(chuàng)建、銷毀速度提升。本文從Tagged Pointer試圖解決的問題入手,帶領(lǐng)讀者理解Tagged Pointer的實(shí)現(xiàn)細(xì)節(jié)和優(yōu)勢(shì),最后指出了使用時(shí)的注意事項(xiàng)。

問題

我們先看看原有的對(duì)象為什么會(huì)浪費(fèi)內(nèi)存。假設(shè)我們要存儲(chǔ)一個(gè) NSNumber 對(duì)象,其值是一個(gè)整數(shù)。正常情況下,如果這個(gè)整數(shù)只是一個(gè) NSInteger 的普通變量,那么它所占用的內(nèi)存是與 CPU 的位數(shù)有關(guān),在 32 位 CPU 下占 4 個(gè)字節(jié),在 64 位 CPU 下是占 8 個(gè)字節(jié)的。而指針類型的大小通常也是與 CPU 位數(shù)相關(guān),一個(gè)指針?biāo)加玫膬?nèi)存在 32 位 CPU 下為 4 個(gè)字節(jié),在 64 位 CPU 下也是 8 個(gè)字節(jié)。

所以一個(gè)普通的 iOS 程序,如果沒有Tagged Pointer對(duì)象,從 32 位機(jī)器遷移到 64 位機(jī)器中后,雖然邏輯沒有任何變化,但這種 NSNumber、NSDate 一類的對(duì)象所占用的內(nèi)存會(huì)翻倍。如下圖所示:

image

我們?cè)賮砜纯葱噬系膯栴},為了存儲(chǔ)和訪問一個(gè) NSNumber 對(duì)象,我們需要在堆上為其分配內(nèi)存,另外還要維護(hù)它的引用計(jì)數(shù),管理它的生命期。這些都給程序增加了額外的邏輯,造成運(yùn)行效率上的損失。

Tagged Pointer

為了改進(jìn)上面提到的內(nèi)存占用和效率問題,蘋果提出了Tagged Pointer對(duì)象。由于 NSNumber、NSDate 一類的變量本身的值需要占用的內(nèi)存大小常常不需要 8 個(gè)字節(jié),拿整數(shù)來說,4 個(gè)字節(jié)所能表示的有符號(hào)整數(shù)就可以達(dá)到 20 多億(注:2^31=2147483648,另外 1 位作為符號(hào)位),對(duì)于絕大多數(shù)情況都是可以處理的。

所以我們可以將一個(gè)對(duì)象的指針拆成兩部分,一部分直接保存數(shù)據(jù),另一部分作為特殊標(biāo)記,表示這是一個(gè)特別的指針,不指向任何一個(gè)地址。所以,引入了Tagged Pointer對(duì)象之后,64 位 CPU 下 NSNumber 的內(nèi)存圖變成了以下這樣:

image

對(duì)此,我們也可以用 Xcode 做實(shí)驗(yàn)來驗(yàn)證。我們的實(shí)驗(yàn)代碼如下:


int main(int argc, char * argv[])

{

    @autoreleasepool {

        NSNumber *number1 = @1;

        NSNumber *number2 = @2;

        NSNumber *number3 = @3;

        NSNumber *numberFFFF = @(0xFFFF);

        NSLog(@"number1 pointer is %p", number1);

        NSLog(@"number2 pointer is %p", number2);

        NSLog(@"number3 pointer is %p", number3);

        NSLog(@"numberffff pointer is %p", numberFFFF);

        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));

    }

}

在該代碼中,我們將幾個(gè) Number 類型的指針的值直接輸出。需要注意的是,我們需要將模擬器切換成 64 位的 CPU 來測(cè)試,如下圖所示:

image

運(yùn)行之后,我們得到的結(jié)果如下,可以看到,除去最后的數(shù)字最末尾的 2 以及最開頭的 0xb,其它數(shù)字剛好表示了相應(yīng) NSNumber 的值。


number1 pointer is 0xb000000000000012

number2 pointer is 0xb000000000000022

number3 pointer is 0xb000000000000032

numberFFFF pointer is 0xb0000000000ffff2

可見,蘋果確實(shí)是將值直接存儲(chǔ)到了指針本身里面。我們還可以猜測(cè),數(shù)字最末尾的 2 以及最開頭的 0xb 是否就是蘋果對(duì)于Tagged Pointer的特殊標(biāo)記呢?我們嘗試放一個(gè) 8 字節(jié)的長(zhǎng)的整數(shù)到NSNumber實(shí)例中,對(duì)于這樣的實(shí)例,由于Tagged Pointer無法將其按上面的壓縮方式來保存,那么應(yīng)該就會(huì)以普通對(duì)象的方式來保存,我們的實(shí)驗(yàn)代碼如下:


NSNumber *bigNumber = @(0xEFFFFFFFFFFFFFFF);

NSLog(@"bigNumber pointer is %p", bigNumber);

運(yùn)行之后,結(jié)果如下,驗(yàn)證了我們的猜測(cè),bigNumber的地址更像是一個(gè)普通的指針地址,和它本身的值看不出任何關(guān)系:


bigNumber pointer is 0x10921ecc0

可見,當(dāng) 8 字節(jié)可以承載用于表示的數(shù)值時(shí),系統(tǒng)就會(huì)以Tagged Pointer的方式生成指針,如果 8 字節(jié)承載不了時(shí),則又用以前的方式來生成普通的指針。關(guān)于以上關(guān)于Tag Pointer的存儲(chǔ)細(xì)節(jié),我們也可以在 這里 找到相應(yīng)的討論,但是其中關(guān)于Tagged Pointer的實(shí)現(xiàn)細(xì)節(jié)與我們的實(shí)驗(yàn)并不相符,筆者認(rèn)為可能是蘋果更改了具體的實(shí)現(xiàn)細(xì)節(jié),并且這并不影響Tagged Pointer我們討論Tagged Pointer本身的優(yōu)點(diǎn)。

特點(diǎn)

我們也可以在 WWDC2013 的《Session 404 Advanced in Objective-C》視頻中,看到蘋果對(duì)于Tagged Pointer特點(diǎn)的介紹:

  1. Tagged Pointer專門用來存儲(chǔ)小的對(duì)象,例如NSNumberNSDate
  2. Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實(shí)際上它不再是一個(gè)對(duì)象了,它只是一個(gè)披著對(duì)象皮的普通變量而已。所以,它的內(nèi)存并不存儲(chǔ)在堆中,也不需要 malloc 和 free。
  3. 在內(nèi)存讀取上有著 3 倍的效率,創(chuàng)建時(shí)比以前快 106 倍。

由此可見,蘋果引入Tagged Pointer,不但減少了 64 位機(jī)器下程序的內(nèi)存占用,還提高了運(yùn)行效率。完美地解決了小內(nèi)存對(duì)象在存儲(chǔ)和訪問效率上的問題。

isa 指針

Tagged Pointer的引入也帶來了問題,即Tagged Pointer因?yàn)椴⒉皇钦嬲膶?duì)象,而是一個(gè)偽對(duì)象,所以你如果完全把它當(dāng)成對(duì)象來使,可能會(huì)讓它露馬腳。比如我在 《Objective-C 對(duì)象模型及應(yīng)用》 一文中就寫道,所有對(duì)象都有 isa 指針,而Tagged Pointer其實(shí)是沒有的,因?yàn)樗皇钦嬲膶?duì)象。
因?yàn)椴皇钦嬲膶?duì)象,所以如果你直接訪問Tagged Pointerisa成員的話,在編譯時(shí)將會(huì)有如下警告:

image

對(duì)于上面的寫法,應(yīng)該換成相應(yīng)的方法調(diào)用,如 isKindOfClassobject_getClass。只要避免在代碼中直接訪問對(duì)象的 isa 變量,即可避免這個(gè)問題。

總結(jié)

蘋果將Tagged Pointer引入,給 64 位系統(tǒng)帶來了內(nèi)存的節(jié)省和運(yùn)行效率的提高。Tagged Pointer通過在其最后一個(gè) bit 位設(shè)置一個(gè)特殊標(biāo)記,用于將數(shù)據(jù)直接保存在指針本身中。因?yàn)?code>Tagged Pointer并不是真正的對(duì)象,我們?cè)谑褂脮r(shí)需要注意不要直接訪問其 isa 變量。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 在調(diào)試程序或者反編譯App時(shí),經(jīng)常可以看到"NSTaggedPointerString"這個(gè)東西例如: 打印: 這...
    Mr_Baymax閱讀 10,467評(píng)論 15 48
  • 前言 在2013年9月,蘋果推出了iPhone5s,與此同時(shí),iPhone5s配備了首個(gè)采用64位架構(gòu)的A7雙核處...
    woshishui1243閱讀 170評(píng)論 0 0
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,757評(píng)論 0 9
  • 本文基于objc4-709源碼進(jìn)行分析。關(guān)于源碼編譯:objc - 編譯Runtime源碼objc4-706 ob...
    WeiHing閱讀 852評(píng)論 1 3
  • 文/豬豬貓張 上帝造人的時(shí)候讓我們擁有兩只耳朵、兩只眼睛和一張嘴,就是讓我們多聽、多看、少說。我們對(duì)還不會(huì)說話的嬰...
    向上有陽(yáng)光閱讀 509評(píng)論 1 10