前言
在 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ì)翻倍。如下圖所示:
我們?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)存圖變成了以下這樣:
對(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è)試,如下圖所示:
運(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)的介紹:
-
Tagged Pointer
專門用來存儲(chǔ)小的對(duì)象,例如NSNumber
和NSDate
-
Tagged Pointer
指針的值不再是地址了,而是真正的值。所以,實(shí)際上它不再是一個(gè)對(duì)象了,它只是一個(gè)披著對(duì)象皮的普通變量而已。所以,它的內(nèi)存并不存儲(chǔ)在堆中,也不需要 malloc 和 free。 - 在內(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 Pointer
的isa
成員的話,在編譯時(shí)將會(huì)有如下警告:
對(duì)于上面的寫法,應(yīng)該換成相應(yīng)的方法調(diào)用,如 isKindOfClass
和 object_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 變量。