1鲸郊、原有系統(tǒng)的問題
假設我們要存儲一個NSNumber對象丰榴,其值是一個整數(shù),正常情況下秆撮,如果這個整數(shù)只是一個NSInteger的普通變量四濒,那么它所占用的內(nèi)存與CPU的位數(shù)有關,在32位CPU下占4個字節(jié)职辨,在64位CPU下是占8個字節(jié)的盗蟆,而指針類型的大小通常通常也與CPU的位數(shù)有關,在32位CPU下占4個字節(jié)舒裤,在64位CPU下是占8個字節(jié)喳资。
所以,一個普通的iOS程序腾供,如果沒有Tagged Pointer(標記指針)對象仆邓,從32位機器遷移到64位機器中后,雖然邏輯上沒有任何變化台腥,但這種NSNumber宏赘、NSDate一類的對象所占用的內(nèi)存會翻倍。
2扳埂、Tagged Pointer介紹
由于NSNumber业簿、NSDate一類的變量本身的值需要占用的內(nèi)存大小常常不需要8個字節(jié),拿整數(shù)來說阳懂,4個字節(jié)所能表示的有符號整數(shù)就可以達到20多億(注:2^31 = 2147483648梅尤,另外一位作為符號位),對于絕大多數(shù)情況都是可以處理的岩调!
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);
我們將NSNumber類型的指針在64位CPU下直接輸出,除去末尾的2和開頭的0xb其他的數(shù)字剛好表示響應NSNumber的值赞厕。猜測:末尾的2和最開頭0xb就是Tagged Pointer的特殊標記Q藓?
我們繼續(xù)驗證坑傅,嘗試方一個8字節(jié)長的整數(shù)到NSNumber實例中僵驰,這樣的實例,Tagged Pointer無法將其按上面的壓縮方式來保存:
NSNumber *bigNumber = @(0xFFFFFFFFFFFFFFFF);
NSLog(@"bigNumber pointer is %p", bigNumber);
打印結果:bigNumber pointer is 0x600000029520
Tagged Pointer特點:
- Tagged Pointer專門用來存儲小的對象唁毒,例如NSNumber和NSDate蒜茴。
- Tagged Pointer指針的值不再是地址,而是真正的值浆西。所以粉私,實際上它不再是一個對象了,它只是一個披著對象"皮"的普通變量而已近零。所以诺核,它的內(nèi)存并不存儲在堆中,也不需要malloc和free久信!
- 在內(nèi)存讀取上有著以前3倍的效率窖杀,創(chuàng)建時比以前快106倍!
結論:
- 當8個字節(jié)可以承載用于表示的數(shù)值時裙士,系統(tǒng)會以Tagged Pointer的方式生成指針入客,如果8個字節(jié)承載不了時,則又用以前的方式生產(chǎn)普通的指針!桌硫!
- 引入Tagged Pointer夭咬,不但減少了64位機器下程序的內(nèi)存占用,還提高了運行效率铆隘,完美地解決了小內(nèi)存對象在存儲和訪問效率上的問題W慷妗!
3膀钠、注意事項和實現(xiàn)細節(jié)
3.1 isa指針
Tagged Pointer的引入也帶來了問題掏湾,即Tagged Pointer并不是真正的對象,而是一個偽對象托修,所以你如果完全把它當做對象來使用忘巧,可能會出問題,比如:所有對象都有isa指針睦刃,而Tagged Pointer其實是沒有的,因為它不是真正的對象十酣,以為不是真正的對象涩拙,所以你如果直接訪問Tagged Pointer的isa成員的話,在編譯時會有警告:
obj->isa
我們應該盡量避免上述寫法耸采,應該換成相應的方法調(diào)用兴泥,如isKindOfClass和object_getClass。只要避免在代碼中直接訪問對象的isa就可以了虾宇!
3.2 引用計數(shù)
對于64位設備搓彻,蘋果除了引入Tagged Pointer來優(yōu)化小的對象外,對于普通的對象嘱朽,其isa指針也進行了優(yōu)化和調(diào)整旭贬!
在32位環(huán)境下,對象的引用計數(shù)都保存在一個外部的表中搪泳,每一個對象的Retain操作稀轨,實際上包括如下5個步驟:
- 獲得全局的記錄引用計數(shù)的hash表
- 為了線程安全,給該hash表枷鎖
- 查找到目標對象的引用計數(shù)值
- 將該引用計數(shù)值加1岸军,寫回hash表
- 給該hash表解鎖
從上面步驟來看奋刽,為了保證線程安全,對引用計數(shù)的增減操作都要先鎖定這個表艰赞,這從性能上看是非常差的佣谐!
而在64位環(huán)境下,isa指針也是64位的方妖,實際作為指針部分只用到了33位狭魂,剩余31位蘋果使用了類似Tagged Pointer的概念,其中19位將保存對象的引用計數(shù),這樣對引用計數(shù)的操作只需要修改這個指針即可趁蕊。只有當引用計數(shù)超出19位坞生,才會將引用計數(shù)保存到外部表,而這種情況是很少掷伙,所以這樣引用計數(shù)的更改銷量會更高是己!
在64位環(huán)境下,新的retain操作包括如下5個步驟:
- 檢查isa指針上面的標記位任柜,看引用計數(shù)是否保存在isa變量中卒废,如果不是,則使用以前的步驟宙地,否則執(zhí)行第2步
- 檢查當前對象是否正在釋放摔认,如果是,則不做任何事情
- 增加該對象的引用計數(shù)宅粥,但是并不是馬上寫回到isa變量中
- 檢查增加后的引用計數(shù)的值是否能夠被19位表示参袱,如果不是,則切換成以前的辦法秽梅,否則執(zhí)行第5步
- 進行一個原子的寫操作抹蚀,將isa的值寫回
由于沒有了全局的加鎖操作,所以引用計數(shù)的更改更快了企垦!
3.3 isa的bit位
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_NONPOINTER_ISA
# if __arm64__
# define ISA_MASK 0x00000001fffffff8ULL
# define ISA_MAGIC_MASK 0x000003fe00000001ULL
# define ISA_MAGIC_VALUE 0x000001a400000001ULL
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000
uintptr_t magic : 9;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 14;
# define RC_ONE (1ULL<<50)
# define RC_HALF (1ULL<<13)
};
# else
// Available bits in isa field are architecture-specific.
# error unknown architecture
# endif
// SUPPORT_NONPOINTER_ISA
#endif
};
SUPPORT_NONPOINTER_ISA 用于標記是否支持優(yōu)化的 isa 指針环壤,其字面含義意思是 isa 的內(nèi)容不再是類的指針了,而是包含了更多信息钞诡,比如引用計數(shù)郑现,析構狀態(tài),被其他 weak 變量引用情況荧降。判斷方法也是根據(jù)設備類型:
#if !__LP64__ || TARGET_OS_WIN32 || TARGET_IPHONE_SIMULATOR || __x86_64__
# define SUPPORT_NONPOINTER_ISA 0
#else
# define SUPPORT_NONPOINTER_ISA 1
#endif
我們可以看到接箫,模擬器也是不支持Tagged Pointer的!
參考:isa 指針 和 IMP 指針