內(nèi)存分配


我們可以看到string1和string2的內(nèi)存地址是相同的。事實上,@"11"存在于常量存儲區(qū),無論你創(chuàng)建、釋放多少次,都不會被釋放掉。如果你有興趣打印下它的類型和retainCount,可以發(fā)現(xiàn)分別是__NSCFConstantString和1152921504606846975。
事實上,所有的__NSCFConstantString類型的實例都是有無限的retainCount的。這就意味著所有的__NSCFConstantString都不會被釋放。


- self.testStr只是對test2的一個淺拷貝,自然地址和2一樣;
- 3,5,6的類型都是NSTaggedPointerString,4的類型是__NSCFString。3,5,6的字面量雖然和1、2一樣的,但是類型其實是不同的。
- 上面打印的結(jié)果中可以看到3,5,6的地址位置非常高,那它們分配在哪個區(qū)呢?
- 另外需要注意的是:如果換成較長的字符串,3,5,6的類型也不是NSTaggedPointerString而是__NSCFString
要研究明白為什么使用的是不同的類型,首先要清楚什么是NSTaggedPointerString,以及為什么直接用字面量賦值給NSString的時候,蘋果不采用NSTaggedPointerString類型。
Tagged Pointer(引用唐巧博客)
在 2013 年 9 月,蘋果推出了 iPhone5s ,與此同時,iPhone5s 配備了首個采用 64 位架構(gòu)的 A7 雙核處理器,為了節(jié)省內(nèi)存和提高執(zhí)行效率,蘋果提出了Tagged Pointer的概念。對于 64 位程序,引入 Tagged Pointer 后,相關(guān)邏輯能減少一半的內(nèi)存占用,以及 3 倍的訪問速度提升,100 倍的創(chuàng)建、銷毀速度提升。
我們先看看原有的對象為什么會浪費內(nèi)存。假設(shè)我們要存儲一個 NSNumber 對象,其值是一個整數(shù)。正常情況下,如果這個整數(shù)只是一個 NSInteger 的普通變量,那么它所占用的內(nèi)存是與 CPU 的位數(shù)有關(guān),在 32 位 CPU 下占 4 個字節(jié),在 64 位 CPU 下是占 8 個字節(jié)的。而指針類型的大小通常也是與 CPU 位數(shù)相關(guān),一個指針?biāo)加玫膬?nèi)存在 32 位 CPU 下為 4 個字節(jié),在 64 位 CPU 下也是 8 個字節(jié)。
所以一個普通的 iOS 程序,如果沒有Tagged Pointer對象,從 32 位機器遷移到 64 位機器中后,雖然邏輯沒有任何變化,但這種 NSNumber、NSDate 一類的對象所占用的內(nèi)存會翻倍。如下圖所示:

我們再來看看效率上的問題,為了存儲和訪問一個 NSNumber 對象,我們需要在堆上為其分配內(nèi)存,另外還要維護它的引用計數(shù),管理它的生命期。這些都給程序增加了額外的邏輯,造成運行效率上的損失。
為了改進上面提到的內(nèi)存占用和效率問題,蘋果提出了Tagged Pointer對象。由于 NSNumber、NSDate 一類的變量本身的值需要占用的內(nèi)存大小常常不需要 8 個字節(jié),拿整數(shù)來說,4 個字節(jié)所能表示的有符號整數(shù)就可以達到 20 多億(注:2^31=2147483648,另外 1 位作為符號位),對于絕大多數(shù)情況都是可以處理的。
所以我們可以將一個對象的指針拆成兩部分,一部分直接保存數(shù)據(jù),另一部分作為特殊標(biāo)記,表示這是一個特別的指針,不指向任何一個地址。所以,引入了Tagged Pointer對象之后,64 位 CPU 下 NSNumber 的內(nèi)存圖變成了以下這樣:

對此,我們也可以用 Xcode 做實驗來驗證。我們的實驗代碼如下:

可見,蘋果確實是將值直接存儲到了指針本身里面。我們還可以猜測,數(shù)字最末尾的 2 以及最開頭的 0xb 是否就是蘋果對于
Tagged Pointer的特殊標(biāo)記呢?我們嘗試放一個 8 字節(jié)的長的整數(shù)到NSNumber實例中,對于這樣的實例,由于Tagged Pointer無法將其按上面的壓縮方式來保存,那么應(yīng)該就會以普通對象的方式來保存,我們的實驗代碼如下:
可見,當(dāng) 8 字節(jié)可以承載用于表示的數(shù)值時,系統(tǒng)就會以Tagged Pointer的方式生成指針,如果 8 字節(jié)承載不了時,則又用以前的方式來生成普通的指針。關(guān)于以上關(guān)于Tag Pointer的存儲細節(jié),我們也可以在這里找到相應(yīng)的討論,但是其中關(guān)于Tagged Pointer的實現(xiàn)細節(jié)與我們的實驗并不相符,筆者認(rèn)為可能是蘋果更改了具體的實現(xiàn)細節(jié),并且這并不影響Tagged Pointer我們討論Tagged Pointer本身的優(yōu)點。
NSTaggedPointerString


由上可知,NSTaggedPointerString是以0xa開頭,中間用特殊方式記錄值,字符串長度作為末尾數(shù)字的一串地址。如果字符串為純數(shù)字,就直接用3X的方式表示,比如test2的值為8,那么值就表示為38再加上后面的字符串長度1就是0xa000000000000381。如果字符串為字符,就以ASCII碼作為值存儲,比如test1。
蘋果對于Tagged Pointer特點的介紹:
-
Tagged Pointer專門用來存儲小的對象,例如NSNumber和NSDate -
Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已。所以,它的內(nèi)存并不存儲在堆中,也不需要 malloc 和 free。 - 在內(nèi)存讀取上有著 3 倍的效率,創(chuàng)建時比以前快 106 倍。
由此可見,蘋果引入Tagged Pointer,不但減少了 64 位機器下程序的內(nèi)存占用,還提高了運行效率。完美地解決了小內(nèi)存對象在存儲和訪問效率上的問題。
蘋果將Tagged Pointer引入,給 64 位系統(tǒng)帶來了內(nèi)存的節(jié)省和運行效率的提高。Tagged Pointer通過在其最后一個 bit 位設(shè)置一個特殊標(biāo)記,用于將數(shù)據(jù)直接保存在指針本身中。因為Tagged Pointer并不是真正的對象,我們在使用時需要注意不要直接訪問其 isa 變量。
NSString用copy還是strong修飾?
@property (nonatomic, strong) NSString *strongStr;
@property (nonatomic, copy) NSString *copyedStr;
//不可變字符串賦值
- (void)testString {
NSString *string = [NSString stringWithFormat:@"lalala"];
self.strongStr = string;
self.copyedStr = string;
NSLog(@"origin string: %p, %p", string, &string);
NSLog(@"strong string: %p, %p", _strongStr, &_strongStr);
NSLog(@"copyed string: %p, %p", _copyedStr, &_copyedStr);
}

這種情況下,不管是strong還是copy屬性的對象,其指向的地址都是同一個,即為string指向的地址。
//可變字符串賦值
- (void)testMutbleString {
NSMutableString *mutbleString = [NSMutableString stringWithFormat:@"hahaha"];
self.strongStr = mutbleString;
self.copyedStr = mutbleString;
// [mutbleString appendString:@" wawawa"];
NSLog(@"mut origin string: %p, %p", mutbleString, &mutbleString);
NSLog(@"mut strong string: %p, %p", _strongStr, &_strongStr);
NSLog(@"mut copyed string: %p, %p", _copyedStr, &_copyedStr);
}

此時copy屬性字符串已不再指向string字符串對象,而是深拷貝了string字符串,并讓_copyedStr對象指向這個字符串