今天小伙伴問了一個(gè)問題,這兩個(gè)變量地址是否相同?
NSString *a = @"a";
NSString *b = @"a";
輸出如下:
(__NSCFConstantString *) $1 = 0x0000000109fdf9f8 @"a"
(__NSCFConstantString *) $2 = 0x0000000109fdf9f8 @"a"
可以看到這兩個(gè)對(duì)象是常量,所以是存儲(chǔ)在常量區(qū),并且地址是一樣的绍移。接下來嘗試其他創(chuàng)建方式:
NSString *c = [NSString stringWithString:a];
NSString *d = [NSString stringWithFormat:a];
NSString *e = [[NSString alloc] initWithFormat:a];
NSString *f = [NSString stringWithFormat:@"asdfhaksfhjkashkhfakskhf"];
NSString *g = [NSString stringWithFormat:@"asdfhaksfhjkashkhfakskhf"];
輸出如下:
(__NSCFConstantString *) $0 = 0x000000010581b9f8 @"a"
(NSTaggedPointerString *) $1 = 0xa000000000000611 @"a"
(NSTaggedPointerString *) $2 = 0xa000000000000611 @"a"
(__NSCFString *) $3 = 0x0000604000249ab0 @"asdfhaksfhjkashkhfakskhf"
(__NSCFString *) $4 = 0x0000604000249ab0 @"asdfhaksfhjkashkhfakskhf"
可以看到創(chuàng)建方式不同地址也是不同的,initWithString和@“”創(chuàng)建的字符串是一樣的都是常量讥电,而initWithFormat和stringWthFormat創(chuàng)建的字符串為NSTaggedPointerString形式蹂窖,也就是所謂的Tagged Pointer對(duì)象,在字符串特別長(zhǎng)時(shí)生成的是__NSCFString對(duì)象恩敌,
查閱資料之后得知NSNumber的存儲(chǔ)也有Tagged Pointer瞬测。
蘋果創(chuàng)建Tagged Pointer的背景如下:
在2013年9月,蘋果推出了iPhone5s纠炮,與此同時(shí)月趟,iPhone5s配備了首 個(gè)采用64位架構(gòu)的A7雙核處理器,為了節(jié)省內(nèi)存和提高執(zhí)行效率抗碰,蘋果提出了Tagged Pointer的概念狮斗。先看看原有的對(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ì)翻倍特咆。
為了存儲(chǔ)和訪問一個(gè)NSNumber對(duì)象季惩,我們需要在堆上為其分配內(nèi)存,另外還要維護(hù)它的引用計(jì)數(shù)腻格,管理它的生命期画拾。這些都給程序增加了額外的邏輯,造成運(yùn)行效率上的損失菜职。
為了改進(jìn)上面提到的內(nèi)存占用和效率問題青抛,蘋果提出了Tagged Pointer對(duì)象。由于NSNumber酬核、NSDate一類的變量本身的值需要占用的內(nèi)存大小常常不需要8個(gè)字節(jié)脂凶,拿整數(shù)來說,4個(gè)字節(jié)所能表示的有符號(hào)整數(shù)就可以達(dá)到20多億愁茁。所以我們可以將一個(gè)對(duì)象的指針拆成兩部分蚕钦,一部分直接保存數(shù)據(jù),另一部分作為特殊標(biāo)記鹅很,表示這是一個(gè)特別的指針嘶居,不指向任何一個(gè)地址。
于是促煮,簡(jiǎn)單來講可以理解為把指針指向的內(nèi)容直接放在了指針變量的內(nèi)存地址中邮屁,因?yàn)樵?64 位環(huán)境下指針變量的大小達(dá)到了 8 位足以容納一些長(zhǎng)度較小的內(nèi)容。于是使用了標(biāo)簽指針這種方式來優(yōu)化數(shù)據(jù)的存儲(chǔ)方式菠齿。從引用計(jì)數(shù)可以看出佑吝,這個(gè)是一個(gè)釋放不掉的單例常量對(duì)象。在運(yùn)行時(shí)根據(jù)實(shí)際情況創(chuàng)建绳匀。
Tagged Pointer 示例
首先先看NSNumber數(shù)值對(duì)象
muStr2 = [NSMutableString stringWithString:@"1"];
for(int i=0; i<20; i+=1){
NSNumber *number = @([muStr2 longLongValue]);
NSLog(@"%@, %p", [number class], number);
[muStr2 appendString:@"1"];
}
// 輸出結(jié)果
__NSCFNumber, 0xb000000000000013
__NSCFNumber, 0xb0000000000000b3
__NSCFNumber, 0xb0000000000006f3
__NSCFNumber, 0xb000000000004573
__NSCFNumber, 0xb00000000002b673
__NSCFNumber, 0xb0000000001b2073
__NSCFNumber, 0xb0000000010f4473
__NSCFNumber, 0xb00000000a98ac73
__NSCFNumber, 0xb000000069f6bc73
__NSCFNumber, 0xb000000423a35c73
__NSCFNumber, 0xb000002964619c73
__NSCFNumber, 0xb000019debd01c73
__NSCFNumber, 0xb000102b36211c73
__NSCFNumber, 0xb000a1b01d4b1c73
__NSCFNumber, 0xb00650e124ef1c73
__NSCFNumber, 0xb03f28cb71571c73
__NSCFNumber, 0xb27797f26d671c73
__NSCFNumber, 0x60000003d540
__NSCFNumber, 0x61000003cb40
__NSCFNumber, 0x61800003c760
數(shù)值是1芋忿、11、111疾棵、1111…..這樣遞增戈钢,可以從輸出指針的地址看出最低4位一直為3,這個(gè)用于標(biāo)記是long(float則為4是尔,Int為2殉了,double為5),而最高4位的“b”表示是NSNumber類型拟枚;其余56位則用來存儲(chǔ)數(shù)值本身內(nèi)容薪铜。當(dāng)存儲(chǔ)用的數(shù)值超過56位存儲(chǔ)上限的時(shí)候众弓,那么NSNumber才會(huì)用真正的64位內(nèi)存地址存儲(chǔ)數(shù)值,然后用指針指向該內(nèi)存地址隔箍。(如果數(shù)值長(zhǎng)度超過64位田轧,那么就crash)。
以上的NSString類型也是和NSNumber一個(gè)道理鞍恢,最低位表示字符串的長(zhǎng)度傻粘,而其余的56位也是用來存儲(chǔ)數(shù)組,這里需要注意的是帮掉,當(dāng)字符串內(nèi)存長(zhǎng)度超過了56位的時(shí)候弦悉,Tagged Pointer并沒有立即用指針轉(zhuǎn)向,而是用了一種算法編碼蟆炊,把字符串長(zhǎng)度進(jìn)行壓縮存儲(chǔ)稽莉,當(dāng)這個(gè)算法壓縮的數(shù)據(jù)長(zhǎng)度超過56位了才使用指針指向。
特點(diǎn)
我們也可以在WWDC2013的《Session 404 Advanced in Objective-C》視頻中涩搓,看到蘋果對(duì)于Tagged Pointer特點(diǎn)的介紹:
1.Tagged Pointer專門用來存儲(chǔ)小的對(duì)象污秆,例如NSNumber和NSDate,
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ǔ)和訪問效率上的問題脖含。
因?yàn)門agged Pointed不是一個(gè)真正的對(duì)象,所以其沒有isa仅仆。不過只要避免在代碼中直接訪問對(duì)象的isa變量器赞,就沒問題垢袱。具體如Tagged Pointer 怎么訪問類方法列表墓拜,之后再詳細(xì)看下,也許是根據(jù)最夠?yàn)榈念愋蜆?biāo)記请契,然后調(diào)用對(duì)應(yīng)的class方法列表咳榜。
總結(jié)
@“” 和 initWithString:方法生成的字符串分配在常量區(qū)夏醉,系統(tǒng)自動(dòng)管理內(nèi)存;
initWithFormat:和 stringWithFormat: 方法生成的字符串分配在堆區(qū)涌韩;
當(dāng)數(shù)據(jù)內(nèi)容超出Tagged Pointed能存儲(chǔ)的內(nèi)容時(shí)畔柔,就會(huì)像正常的創(chuàng)建對(duì)象一樣創(chuàng)建指針和數(shù)據(jù)內(nèi)容。