前言
在 2013 年 9 月,蘋果推出了 iPhone5s,與此同時,iPhone5s 配備了首個采用 64 位架構的 A7 雙核處理器编矾,為了節(jié)省內存和提高執(zhí)行效率,蘋果提出了Tagged Pointer
的概念馁害。對于 64 位程序窄俏,引入 Tagged Pointer 后,相關邏輯能減少一半的內存占用碘菜,以及 3 倍的訪問速度提升凹蜈,100 倍的創(chuàng)建、銷毀速度提升忍啸。本文從Tagged Pointer
試圖解決的問題入手仰坦,帶領讀者理解Tagged Pointer
的實現細節(jié)和優(yōu)勢,最后指出了使用時的注意事項吊骤。
問題
我們先看看原有的對象為什么會浪費內存。假設我們要存儲一個 NSNumber 對象静尼,其值是一個整數白粉。正常情況下,如果這個整數只是一個 NSInteger 的普通變量鼠渺,那么它所占用的內存是與 CPU 的位數有關鸭巴,在 32 位 CPU 下占 4 個字節(jié),在 64 位 CPU 下是占 8 個字節(jié)的拦盹。而指針類型的大小通常也是與 CPU 位數相關鹃祖,一個指針所占用的內存在 32 位 CPU 下為 4 個字節(jié),在 64 位 CPU 下也是 8 個字節(jié)普舆。
所以一個普通的 iOS 程序恬口,如果沒有Tagged Pointer
對象校读,從 32 位機器遷移到 64 位機器中后,雖然邏輯沒有任何變化祖能,但這種 NSNumber歉秫、NSDate 一類的對象所占用的內存會翻倍。如下圖所示:
我們再來看看效率上的問題养铸,為了存儲和訪問一個 NSNumber 對象雁芙,我們需要在堆上為其分配內存,另外還要維護它的引用計數钞螟,管理它的生命期兔甘。這些都給程序增加了額外的邏輯,造成運行效率上的損失鳞滨。
Tagged Pointer
為了改進上面提到的內存占用和效率問題洞焙,蘋果提出了Tagged Pointer
對象。由于 NSNumber太援、NSDate 一類的變量本身的值需要占用的內存大小常常不需要 8 個字節(jié)闽晦,拿整數來說,4 個字節(jié)所能表示的有符號整數就可以達到 20 多億(注:2^31=2147483648提岔,另外 1 位作為符號位)仙蛉,對于絕大多數情況都是可以處理的。
所以我們可以將一個對象的指針拆成兩部分碱蒙,一部分直接保存數據荠瘪,另一部分作為特殊標記,表示這是一個特別的指針赛惩,不指向任何一個地址哀墓。所以,引入了Tagged Pointer
對象之后喷兼,64 位 CPU 下 NSNumber 的內存圖變成了以下這樣:
對此篮绰,我們也可以用 Xcode 做實驗來驗證。我們的實驗代碼如下:
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]));
}
}
在該代碼中季惯,我們將幾個 Number 類型的指針的值直接輸出吠各。需要注意的是,我們需要將模擬器切換成 64 位的 CPU 來測試勉抓,如下圖所示:
運行之后贾漏,我們得到的結果如下,可以看到藕筋,除去最后的數字最末尾的 2 以及最開頭的 0xb纵散,其它數字剛好表示了相應 NSNumber 的值。
number1 pointer is 0xb000000000000012
number2 pointer is 0xb000000000000022
number3 pointer is 0xb000000000000032
numberFFFF pointer is 0xb0000000000ffff2
可見,蘋果確實是將值直接存儲到了指針本身里面伍掀。我們還可以猜測掰茶,數字最末尾的 2 以及最開頭的 0xb 是否就是蘋果對于Tagged Pointer
的特殊標記呢?我們嘗試放一個 8 字節(jié)的長的整數到NSNumber
實例中硕盹,對于這樣的實例符匾,由于Tagged Pointer
無法將其按上面的壓縮方式來保存,那么應該就會以普通對象的方式來保存瘩例,我們的實驗代碼如下:
NSNumber *bigNumber = @(0xEFFFFFFFFFFFFFFF);
NSLog(@"bigNumber pointer is %p", bigNumber);
運行之后啊胶,結果如下,驗證了我們的猜測垛贤,bigNumber
的地址更像是一個普通的指針地址焰坪,和它本身的值看不出任何關系:
bigNumber pointer is 0x10921ecc0
可見,當 8 字節(jié)可以承載用于表示的數值時聘惦,系統就會以Tagged Pointer
的方式生成指針某饰,如果 8 字節(jié)承載不了時,則又用以前的方式來生成普通的指針善绎。關于以上關于Tag Pointer
的存儲細節(jié)黔漂,我們也可以在 這里 找到相應的討論,但是其中關于Tagged Pointer
的實現細節(jié)與我們的實驗并不相符禀酱,筆者認為可能是蘋果更改了具體的實現細節(jié)炬守,并且這并不影響Tagged Pointer
我們討論Tagged Pointer
本身的優(yōu)點。
特點
我們也可以在 WWDC2013 的《Session 404 Advanced in Objective-C》視頻中剂跟,看到蘋果對于Tagged Pointer
特點的介紹:
-
Tagged Pointer
專門用來存儲小的對象减途,例如NSNumber
和NSDate
-
Tagged Pointer
指針的值不再是地址了,而是真正的值曹洽。所以鳍置,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已送淆。所以税产,它的內存并不存儲在堆中,也不需要 malloc 和 free偷崩。 - 在內存讀取上有著 3 倍的效率辟拷,創(chuàng)建時比以前快 106 倍。
由此可見环凿,蘋果引入Tagged Pointer
梧兼,不但減少了 64 位機器下程序的內存占用放吩,還提高了運行效率智听。完美地解決了小內存對象在存儲和訪問效率上的問題。
isa 指針
Tagged Pointer
的引入也帶來了問題,即Tagged Pointer
因為并不是真正的對象到推,而是一個偽對象考赛,所以你如果完全把它當成對象來使,可能會讓它露馬腳莉测。比如我在 《Objective-C 對象模型及應用》 一文中就寫道颜骤,所有對象都有 isa
指針,而Tagged Pointer
其實是沒有的捣卤,因為它不是真正的對象忍抽。
因為不是真正的對象,所以如果你直接訪問Tagged Pointer
的isa
成員的話董朝,在編譯時將會有如下警告:
對于上面的寫法鸠项,應該換成相應的方法調用,如 isKindOfClass
和 object_getClass
子姜。只要避免在代碼中直接訪問對象的 isa 變量祟绊,即可避免這個問題。
總結
蘋果將Tagged Pointer
引入哥捕,給 64 位系統帶來了內存的節(jié)省和運行效率的提高牧抽。Tagged Pointer
通過在其最后一個 bit 位設置一個特殊標記,用于將數據直接保存在指針本身中遥赚。因為Tagged Pointer
并不是真正的對象扬舒,我們在使用時需要注意不要直接訪問其 isa 變量。