前言
前段時間,看到在知識小集的交流群里正在討論 copy 和 mutableCopy 的相關特性。所以自己寫了一個 Demo 驗證一下群里提供的表是否正確。后來發(fā)現(xiàn)了 NSString 出現(xiàn)了中間類的情況。所以,寫了這篇文章,記錄一下。
NSString 解析
在 iOS 開發(fā)中字符串的使用通常用的比較多的是 NSString 而不是 char。而對于這個 NSString 類,實際上在編譯和運行的時候會轉化為不同的類型。所以接下來,就需要了解一下這些相關類:NSString、NSMutableString、__NSCFConstantString、__NSCFString、NSTaggedPointerString。
NSString 相關類說明表格
| 類名 | 存儲區(qū)域 | 初始化的引用計數(shù)(retainCount) | 作用描述 |
|---|---|---|---|
| NSString | 堆區(qū) | 1 | 開發(fā)者常用的不可變字符串類,編譯期間會轉換到其他類型 |
| NSMutableString | 堆區(qū) | 1 | 開發(fā)者常用的可變字符串類,編譯期間會轉換到其他類型 |
| __NSCFString | 堆區(qū) | 1 | 可變字符串 NSMutableString 類,編譯期間會轉換到該類型 |
| __NSCFConstantString | 堆區(qū) | 2^64-1 | 不可變字符串 NSString 類,編譯期間會轉換到該類型 |
| NSTaggedPointerString | 棧區(qū) | 2^64-1 | Tagged Pointer對象,并不是真的對象 |
測試代碼
測試代碼主要分為兩部分:NSString 和 NSMutableString。當然,會通過這兩部分代碼說明問題。
NSString 測試代碼
首先,執(zhí)行 NSString 的測試代碼,如下:
NSString *str = @"abc"; // __NSCFConstantString
NSString *str1 = @"abc"; //__NSCFConstantString
NSString *str2 = [NSString stringWithFormat:@"%@", str]; // NSTaggedPointerString
NSString *str3 = [str copy]; // __NSCFConstantString
NSString *str4 = [str mutableCopy]; // __NSCFString
NSLog(@"str(%@<%p>: %p): %@", [str class], &str, str, str);
NSLog(@"str1(%@<%p>: %p): %@", [str1 class], &str1, str1, str1);
NSLog(@"str2(%@<%p>: %p): %@", [str2 class], &str2, str2, str2);
NSLog(@"str3(%@<%p>: %p): %@", [str3 class], &str3, str3, str3);
NSLog(@"str4(%@<%p>: %p): %@", [str4 class], &str4, str4, str4);
變量內存分布截圖:

打印的結果如下:
2018-08-10 19:35:59.172724+0800 TestCocoaPods[3527:192649] str(__NSCFConstantString<0x7ffeecbe5ba8>: 0x10301c090): abc
2018-08-10 19:35:59.173112+0800 TestCocoaPods[3527:192649] str1(__NSCFConstantString<0x7ffeecbe5ba0>: 0x10301c090): abc
2018-08-10 19:35:59.173445+0800 TestCocoaPods[3527:192649] str2(NSTaggedPointerString<0x7ffeecbe5b98>: 0xa000000006362613): abc
2018-08-10 19:35:59.173616+0800 TestCocoaPods[3527:192649] str3(__NSCFConstantString<0x7ffeecbe5b90>: 0x10301c090): abc
2018-08-10 19:35:59.173845+0800 TestCocoaPods[3527:192649] str4(__NSCFString<0x7ffeecbe5b88>: 0x600000259050): abc
NSMutableString 測試代碼
接下來,執(zhí)行 NSMutableString 的測試代碼,如下:
NSMutableString *str = [NSMutableString stringWithString:@"abc"];
NSMutableString *str1 = [NSMutableString stringWithString:@"abc"];
NSMutableString *str2 = [NSMutableString stringWithFormat:@"%@", str];
NSMutableString *str3 = [str copy];
NSMutableString *str4 = [str mutableCopy];
NSLog(@"str(%@<%p>: %p): %@", [str class], &str, str, str);
NSLog(@"str1(%@<%p>: %p): %@", [str1 class], &str1, str1, str1);
NSLog(@"str2(%@<%p>: %p): %@", [str2 class], &str2, str2, str2);
NSLog(@"str3(%@<%p>: %p): %@", [str3 class], &str3, str3, str3);
NSLog(@"str4(%@<%p>: %p): %@", [str4 class], &str4, str4, str4);
變量內存分布截圖:

打印的結果如下:
2018-08-10 21:37:49.709725+0800 TestCocoaPods[4309:248326] str(__NSCFString<0x7ffeed8e6ba8>: 0x60000044f6c0): abc
2018-08-10 21:37:49.709956+0800 TestCocoaPods[4309:248326] str1(__NSCFString<0x7ffeed8e6ba0>: 0x600000450290): abc
2018-08-10 21:37:49.710309+0800 TestCocoaPods[4309:248326] str2(__NSCFString<0x7ffeed8e6b98>: 0x600000450740): abc
2018-08-10 21:37:49.710652+0800 TestCocoaPods[4309:248326] str3(NSTaggedPointerString<0x7ffeed8e6b90>: 0xa000000006362613): abc
2018-08-10 21:37:49.711494+0800 TestCocoaPods[4309:248326] str4(__NSCFString<0x7ffeed8e6b88>: 0x6000004506e0): abc
相關類的繼承鏈條
以上所說的字符串的相關類,它們有什么關系呢?或者說有什么關聯(lián)呢?這一節(jié)主要圍繞這兩個問題展開。由以上的測試代碼和測試結果可以推斷出字符串類的繼承鏈條如下:
__NSCFConstantString -> __NSCFString -> NSMutableString -> NSString -> NSObject
其中,編譯后的 NSString 一般實際使用的是 __NSCFConstantString,編譯后的 NSMutableString 一般實際是使用 __NSCFString。所以,開發(fā)者只要了解其對應關系就可以了。從測試代碼中打印的結果看還有一種類:NSTaggedPointerString。這是干嘛的呢?其實嚴格地說,這并不是一個類,它是適用于 64位處理器 的一個內存優(yōu)化機制,也就是 Tagged Pointer。
接下來,將從 CoreFoundation 露出來的頭文件進行分析。
__NSCFConstantString 字符串常量
在編譯期間,就已經(jīng)決定 NSString -> __NSCFConstantString。所以同一個字符串常量在堆區(qū)只分配一個空間,并且 retainCount 為最大。也就是說不會被釋放掉。該類的定義在 CoreFoundation 中的 __NSCFConstantString.h 文件中。
定義代碼如下:
@interface __NSCFConstantString : __NSCFString
- (id)autorelease;
- (id)copyWithZone:(struct _NSZone { }*)arg1;
- (bool)isNSCFConstantString__;
- (oneway void)release;
- (id)retain;
- (unsigned long long)retainCount;
@end
如上代碼可知,__NSCFConstantString 是繼承于 __NSCFString。也就是說,重復的聲明同樣內容的字符串常量,實際上指向的是同一個堆區(qū)地址,如NSString測試代碼的以下幾行:
NSString *str = @"abc"; // __NSCFConstantString
NSString *str1 = @"abc"; //__NSCFConstantString
打印出的結果對應如下:
2018-08-10 19:35:59.172724+0800 TestCocoaPods[3527:192649] str(__NSCFConstantString<0x7ffeecbe5ba8>: 0x10301c090): abc
2018-08-10 19:35:59.173112+0800 TestCocoaPods[3527:192649] str1(__NSCFConstantString<0x7ffeecbe5ba0>: 0x10301c090): abc
可以看出,打印出來的堆區(qū)地址都是 0x10301c090。
__NSCFString 可變字符串
在編譯期間,就已經(jīng)決定 NSMutableString -> __NSCFString。所以一個可變字符串常量在堆區(qū)會分配一個空間,并且 retainCount 為 1,也就是說按正常對象的生命周期被釋放。該類的定義在 CoreFoundation 中的 __NSCFString.h。
定義代碼如下:
@interface __NSCFString : NSMutableString
...
@end
如上代碼可知,__NSCFString 是繼承于 NSMutableString。
NSTaggedPointerString
在編譯期間,已經(jīng)會決定 NSString -> NSTaggedPointerString。值將存儲在指針空間,也就是棧(Stack)區(qū),并且 retainCount 為最大。不過要觸發(fā)這樣的類型轉換,需要滿足以下兩個條件:
- 64位處理器
- 內容很少,棧區(qū)能夠裝得下
具體的內存分布請看 Tagged Pointer。
NSNumber 解析
在 iOS 開發(fā)中,數(shù)字通常會使用 NSNumber 類進行封裝承載。而對于這個 NSNumber 類,實際上在編譯和運行的時候會轉化為不同的類型。所以接下來,就需要了解一下這些相關類:NSNumber、__NSCFNumber、NSValue。
NSNumber 相關類說明表格
| 類名 | 存儲區(qū)域 | 初始化的引用計數(shù)(retainCount | 作用描述 |
|---|---|---|---|
| NSValue | 堆區(qū) | 1 | 主要用于封裝結構體 |
| NSNumber | 堆區(qū) | 1 | 開發(fā)者常用的數(shù)字類,編譯期間會轉換到其他類型 |
| __NSCFNumber | 堆區(qū)、棧區(qū) | 1、2^64-1 | 數(shù)字類 NSNumber 類,編譯期間會轉換到該類型,若是 Tagged Pointer 則在棧區(qū),引用計數(shù)為 2^64-1 |
測試代碼
執(zhí)行NSNumber的測試代碼:
NSNumber *num1 = @1;
NSNumber *num2 = @2;
NSNumber *num3 = @3;
NSNumber *num4 = @(3.1415927);
NSNumber *num5 = [num1 copy];
NSNumber *num6 = [num4 copy];
NSLog(@"num1(%@<%p>: %p): %@", [num1 class], &num1, num1, num1);
NSLog(@"num2(%@<%p>: %p): %@", [num2 class], &num2, num2, num2);
NSLog(@"num3(%@<%p>: %p): %@", [num3 class], &num3, num3, num3);
NSLog(@"num4(%@<%p>: %p): %@", [num4 class], &num4, num4, num4);
NSLog(@"num5(%@<%p>: %p): %@", [num5 class], &num5, num5, num5);
NSLog(@"num6(%@<%p>: %p): %@", [num6 class], &num6, num6, num6);
變量內存分布截圖:

打印的結果如下:
2018-08-10 23:55:08.025987+0800 TestCocoaPods[5422:331863] num1(__NSCFNumber<0x7ffee5c32b70>: 0xb000000000000012): 1
2018-08-10 23:55:08.026190+0800 TestCocoaPods[5422:331863] num2(__NSCFNumber<0x7ffee5c32b68>: 0xb000000000000022): 2
2018-08-10 23:55:08.026329+0800 TestCocoaPods[5422:331863] num3(__NSCFNumber<0x7ffee5c32b60>: 0xb000000000000032): 3
2018-08-10 23:55:08.026422+0800 TestCocoaPods[5422:331863] num4(__NSCFNumber<0x7ffee5c32b58>: 0x604000425be0): 3.1415927
2018-08-10 23:55:08.026516+0800 TestCocoaPods[5422:331863] num5(__NSCFNumber<0x7ffee5c32b50>: 0xb000000000000012): 1
2018-08-10 23:55:09.688991+0800 TestCocoaPods[5422:331863] num6(__NSCFNumber<0x7ffee5c32b48>: 0x604000425be0): 3.1415927
相關類的繼承鏈條
以上所說的數(shù)字的相關類,它們有什么關系呢?或者說有什么關聯(lián)呢?這一節(jié)主要圍繞這兩個問題展開。由以上的測試代碼和測試結果可以推斷出數(shù)字類的繼承鏈條如下:
__NSCFNumber -> NSNumber -> NSValue -> NSObject
其中,編譯后的 NSNumber 一般實際使用的是 __NSCFNumber。所以,開發(fā)者只要了解其對應關系就可以了。在 Tagged Pointer 機制中,和字符串不同的地方是沒有對應的Tagged Pointer對象類型。
接下來,將從 CoreFoundation 露出來的頭文件進行分析。
__NSCFNumber 數(shù)字類
在編譯期間,就已經(jīng)決定 NSNumber -> __NSCFNumber。所以同一個字符串常量在堆區(qū)會分配一個空間,并且 retainCount 為 1。該類的定義在 CoreFoundation 中的 __NSCFNumber.h 文件中。
定義代碼如下:
@interface __NSCFNumber : NSNumber
+ (bool)automaticallyNotifiesObserversForKey:(id)arg1;
- (long long)_cfNumberType;
- (unsigned long long)_cfTypeID;
- (unsigned char)_getValue:(void*)arg1 forType:(long long)arg2;
- (bool)_isDeallocating;
- (long long)_reverseCompare:(id)arg1;
- (bool)_tryRetain;
- (bool)boolValue;
- (BOOL)charValue;
- (long long)compare:(id)arg1;
- (id)copyWithZone:(struct _NSZone { }*)arg1;
- (id)description;
- (id)descriptionWithLocale:(id)arg1;
- (double)doubleValue;
- (float)floatValue;
- (void)getValue:(void*)arg1;
- (unsigned long long)hash;
- (int)intValue;
- (long long)integerValue;
- (bool)isEqual:(id)arg1;
- (bool)isEqualToNumber:(id)arg1;
- (bool)isNSNumber__;
- (long long)longLongValue;
- (long long)longValue;
- (const char *)objCType;
- (oneway void)release;
- (id)retain;
- (unsigned long long)retainCount;
- (short)shortValue;
- (id)stringValue;
- (unsigned char)unsignedCharValue;
- (unsigned int)unsignedIntValue;
- (unsigned long long)unsignedIntegerValue;
- (unsigned long long)unsignedLongLongValue;
- (unsigned long long)unsignedLongValue;
- (unsigned short)unsignedShortValue;
@end
__NSCFNumber 的 Tagged Pointer 特性
在編譯期間,就已經(jīng)決定 NSNumber -> __NSCFNumber。不過,需要啟動 Tagged Pointer 的條件和字符串的 NSTaggedPointerString條件一樣如下:
- 64位處理器
- 數(shù)字較小,棧區(qū)能夠裝得下
Tagged Pointer 特性分析
為了改進從 32位CPU 遷移到 64位CPU 的內存浪費和效率問題,在 64位CPU 環(huán)境下,引入了 Tagged Pointer 對象。有了這樣的機制,系統(tǒng)會對 NSString、NSNumber 和 NSDate等對象進行優(yōu)化。
未引入 Tagged Pointer 內存分布
一般的 iOS 程序,從32位遷移到64位CPU,雖然邏輯上是不會有任何變化,但是所占有的內存空間就會翻倍。以 NSInteger 封裝成 NSNumber 為例,內存分布圖如下:

由分布圖所示,占用內存從32位CPU的12個字節(jié)到24個字節(jié)整整翻了一倍。
引入 Tagged Pointer 內存分布
引用了 Tagged Pointer 的對象,節(jié)省了分配在堆區(qū)的空間,將值存在指針區(qū)域的棧區(qū)。從而節(jié)省了內存空間以及大大提升了訪問速度。以 NSInteger 封裝成 NSNumber 為例,內存分布圖如下:

由分布圖所示,占用內存從32位CPU的12個字節(jié)到8個字節(jié),還節(jié)省了3個字節(jié)的內存空間。而且引用計數(shù) retainCount 為最大值。
驗證過程
根據(jù)以上NSNumber的測試代碼:
NSNumber *num1 = @1;
NSNumber *num2 = @2;
NSNumber *num3 = @3;
NSNumber *num4 = @(3.1415927);
NSNumber *num5 = [num1 copy];
NSNumber *num6 = [num4 copy];
打印的結果如下:
2018-08-10 23:55:08.025987+0800 TestCocoaPods[5422:331863] num1(__NSCFNumber<0x7ffee5c32b70>: 0xb000000000000012): 1
2018-08-10 23:55:08.026190+0800 TestCocoaPods[5422:331863] num2(__NSCFNumber<0x7ffee5c32b68>: 0xb000000000000022): 2
2018-08-10 23:55:08.026329+0800 TestCocoaPods[5422:331863] num3(__NSCFNumber<0x7ffee5c32b60>: 0xb000000000000032): 3
2018-08-10 23:55:08.026422+0800 TestCocoaPods[5422:331863] num4(__NSCFNumber<0x7ffee5c32b58>: 0x604000425be0): 3.1415927
2018-08-10 23:55:08.026516+0800 TestCocoaPods[5422:331863] num5(__NSCFNumber<0x7ffee5c32b50>: 0xb000000000000012): 1
2018-08-10 23:55:09.688991+0800 TestCocoaPods[5422:331863] num6(__NSCFNumber<0x7ffee5c32b48>: 0x604000425be0): 3.1415927
說明使用 Tagged Pointer 的對象的值都會存儲在指針的值里。以上打印結果,可看出 0xb 開頭的地址都是 Tagged Pointer,只要把前面的 0xb 和 尾部的 2去掉,剩下的就是真正的值。具體的存儲細節(jié),可參考 tagged-pointers 文檔。
而打印結果中的 num4 變量存儲的是雙精度浮點數(shù),棧區(qū)存不了,所以會在堆區(qū)開辟空間存儲。
特點總結
Tagged Pointer 的引用主要解決內存浪費和訪問效率的問題。所以其有以下特點:
-
Tagged Pointer專門用于存儲小的對象,例如:NSString、NSNumber和NSDate。 -
Tagged Pointer指針的值不再是堆區(qū)地址,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已。所以,它的內存并不存儲在堆中,也不需要malloc和free。 - 在內存讀取上有著 3 倍的效率,創(chuàng)建時比以前快 106 倍。
如此可見,Apple 引入了 Tagged Pointer 不僅僅節(jié)省了64位處理器的占用內存空間,還提高了運行效率。
使用注意點
Tagged Pointer 并不是真正的指針,由測試代碼的變量內存分布截圖,都可表明其對應的 isa 指針已經(jīng)指向 0x0 地址。所以如果你直接訪問 Tagged Pointer 的 isa 成員的話,編譯時期將會有警告??梢酝ㄟ^調用 isKindOfClass 和 object_getClass,避免直接訪問對象的 isa 變量。
結論
在iOS的日常開發(fā)中,同樣內容的字符串常量 __NSCFConstantString 全局只有一份,放在堆區(qū),并且不會被釋放(retainCount值最大)。并且由于有 Tagged Pointer 的存在,盡量避免直接訪問對象的 isa 變量。