前言
前段時(shí)間,看到在知識(shí)小集的交流群里正在討論 copy
和 mutableCopy
的相關(guān)特性。所以自己寫了一個(gè) Demo 驗(yàn)證一下群里提供的表是否正確。后來(lái)發(fā)現(xiàn)了 NSString
出現(xiàn)了中間類的情況。所以,寫了這篇文章,記錄一下。
NSString 解析
在 iOS 開(kāi)發(fā)中字符串的使用通常用的比較多的是 NSString
而不是 char
。而對(duì)于這個(gè) NSString
類,實(shí)際上在編譯和運(yùn)行的時(shí)候會(huì)轉(zhuǎn)化為不同的類型。所以接下來(lái),就需要了解一下這些相關(guān)類:NSString
、NSMutableString
、__NSCFConstantString
、__NSCFString
、NSTaggedPointerString
。
NSString 相關(guān)類說(shuō)明表格
類名 | 存儲(chǔ)區(qū)域 | 初始化的引用計(jì)數(shù)(retainCount) | 作用描述 |
---|---|---|---|
NSString | 堆區(qū) | 1 | 開(kāi)發(fā)者常用的不可變字符串類,編譯期間會(huì)轉(zhuǎn)換到其他類型 |
NSMutableString | 堆區(qū) | 1 | 開(kāi)發(fā)者常用的可變字符串類,編譯期間會(huì)轉(zhuǎn)換到其他類型 |
__NSCFString | 堆區(qū) | 1 | 可變字符串 NSMutableString 類,編譯期間會(huì)轉(zhuǎn)換到該類型 |
__NSCFConstantString | 堆區(qū) | 2^64-1 | 不可變字符串 NSString 類,編譯期間會(huì)轉(zhuǎn)換到該類型 |
NSTaggedPointerString | 棧區(qū) | 2^64-1 | Tagged Pointer對(duì)象,并不是真的對(duì)象 |
測(cè)試代碼
測(cè)試代碼主要分為兩部分:NSString
和 NSMutableString
。當(dāng)然,會(huì)通過(guò)這兩部分代碼說(shuō)明問(wèn)題。
NSString 測(cè)試代碼
首先,執(zhí)行 NSString 的測(cè)試代碼,如下:
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);
變量?jī)?nèi)存分布截圖:
打印的結(jié)果如下:
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 測(cè)試代碼
接下來(lái),執(zhí)行 NSMutableString 的測(cè)試代碼,如下:
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);
變量?jī)?nèi)存分布截圖:
打印的結(jié)果如下:
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
相關(guān)類的繼承鏈條
以上所說(shuō)的字符串的相關(guān)類,它們有什么關(guān)系呢?或者說(shuō)有什么關(guān)聯(lián)呢?這一節(jié)主要圍繞這兩個(gè)問(wèn)題展開(kāi)。由以上的測(cè)試代碼和測(cè)試結(jié)果可以推斷出字符串類的繼承鏈條如下:
__NSCFConstantString
-> __NSCFString
-> NSMutableString
-> NSString
-> NSObject
其中,編譯后的 NSString
一般實(shí)際使用的是 __NSCFConstantString
,編譯后的 NSMutableString
一般實(shí)際是使用 __NSCFString
。所以,開(kāi)發(fā)者只要了解其對(duì)應(yīng)關(guān)系就可以了。從測(cè)試代碼中打印的結(jié)果看還有一種類:NSTaggedPointerString
。這是干嘛的呢?其實(shí)嚴(yán)格地說(shuō),這并不是一個(gè)類,它是適用于 64位處理器
的一個(gè)內(nèi)存優(yōu)化機(jī)制,也就是 Tagged Pointer
。
接下來(lái),將從 CoreFoundation 露出來(lái)的頭文件進(jìn)行分析。
__NSCFConstantString 字符串常量
在編譯期間,就已經(jīng)決定 NSString
-> __NSCFConstantString
。所以同一個(gè)字符串常量在堆區(qū)只分配一個(gè)空間,并且 retainCount
為最大。也就是說(shuō)不會(huì)被釋放掉。該類的定義在 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
。也就是說(shuō),重復(fù)的聲明同樣內(nèi)容的字符串常量,實(shí)際上指向的是同一個(gè)堆區(qū)地址,如NSString測(cè)試代碼的以下幾行:
NSString *str = @"abc"; // __NSCFConstantString
NSString *str1 = @"abc"; //__NSCFConstantString
打印出的結(jié)果對(duì)應(yīng)如下:
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
可以看出,打印出來(lái)的堆區(qū)地址都是 0x10301c090
。
__NSCFString 可變字符串
在編譯期間,就已經(jīng)決定 NSMutableString
-> __NSCFString
。所以一個(gè)可變字符串常量在堆區(qū)會(huì)分配一個(gè)空間,并且 retainCount
為 1,也就是說(shuō)按正常對(duì)象的生命周期被釋放。該類的定義在 CoreFoundation 中的 __NSCFString.h。
定義代碼如下:
@interface __NSCFString : NSMutableString
...
@end
如上代碼可知,__NSCFString
是繼承于 NSMutableString
。
NSTaggedPointerString
在編譯期間,已經(jīng)會(huì)決定 NSString
-> NSTaggedPointerString
。值將存儲(chǔ)在指針空間,也就是棧(Stack)區(qū),并且 retainCount
為最大。不過(guò)要觸發(fā)這樣的類型轉(zhuǎn)換,需要滿足以下兩個(gè)條件:
- 64位處理器
- 內(nèi)容很少,棧區(qū)能夠裝得下
具體的內(nèi)存分布請(qǐng)看 Tagged Pointer
。
NSNumber 解析
在 iOS 開(kāi)發(fā)中,數(shù)字通常會(huì)使用 NSNumber
類進(jìn)行封裝承載。而對(duì)于這個(gè) NSNumber
類,實(shí)際上在編譯和運(yùn)行的時(shí)候會(huì)轉(zhuǎn)化為不同的類型。所以接下來(lái),就需要了解一下這些相關(guān)類:NSNumber
、__NSCFNumber
、NSValue
。
NSNumber 相關(guān)類說(shuō)明表格
類名 | 存儲(chǔ)區(qū)域 | 初始化的引用計(jì)數(shù)(retainCount | 作用描述 |
---|---|---|---|
NSValue | 堆區(qū) | 1 | 主要用于封裝結(jié)構(gòu)體 |
NSNumber | 堆區(qū) | 1 | 開(kāi)發(fā)者常用的數(shù)字類,編譯期間會(huì)轉(zhuǎn)換到其他類型 |
__NSCFNumber | 堆區(qū)、棧區(qū) | 1、2^64-1 | 數(shù)字類 NSNumber 類,編譯期間會(huì)轉(zhuǎn)換到該類型,若是 Tagged Pointer 則在棧區(qū),引用計(jì)數(shù)為 2^64-1 |
測(cè)試代碼
執(zhí)行NSNumber的測(cè)試代碼:
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);
變量?jī)?nèi)存分布截圖:
打印的結(jié)果如下:
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
相關(guān)類的繼承鏈條
以上所說(shuō)的數(shù)字的相關(guān)類,它們有什么關(guān)系呢?或者說(shuō)有什么關(guān)聯(lián)呢?這一節(jié)主要圍繞這兩個(gè)問(wèn)題展開(kāi)。由以上的測(cè)試代碼和測(cè)試結(jié)果可以推斷出數(shù)字類的繼承鏈條如下:
__NSCFNumber
-> NSNumber
-> NSValue
-> NSObject
其中,編譯后的 NSNumber
一般實(shí)際使用的是 __NSCFNumber
。所以,開(kāi)發(fā)者只要了解其對(duì)應(yīng)關(guān)系就可以了。在 Tagged Pointer
機(jī)制中,和字符串不同的地方是沒(méi)有對(duì)應(yīng)的Tagged Pointer
對(duì)象類型。
接下來(lái),將從 CoreFoundation 露出來(lái)的頭文件進(jìn)行分析。
__NSCFNumber 數(shù)字類
在編譯期間,就已經(jīng)決定 NSNumber
-> __NSCFNumber
。所以同一個(gè)字符串常量在堆區(qū)會(huì)分配一個(gè)空間,并且 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
。不過(guò),需要啟動(dòng) Tagged Pointer
的條件和字符串的 NSTaggedPointerString
條件一樣如下:
- 64位處理器
- 數(shù)字較小,棧區(qū)能夠裝得下
Tagged Pointer 特性分析
為了改進(jìn)從 32位CPU 遷移到 64位CPU 的內(nèi)存浪費(fèi)和效率問(wèn)題,在 64位CPU 環(huán)境下,引入了 Tagged Pointer
對(duì)象。有了這樣的機(jī)制,系統(tǒng)會(huì)對(duì) NSString
、NSNumber
和 NSDate
等對(duì)象進(jìn)行優(yōu)化。
未引入 Tagged Pointer 內(nèi)存分布
一般的 iOS 程序,從32位遷移到64位CPU,雖然邏輯上是不會(huì)有任何變化,但是所占有的內(nèi)存空間就會(huì)翻倍。以 NSInteger
封裝成 NSNumber
為例,內(nèi)存分布圖如下:
由分布圖所示,占用內(nèi)存從32位CPU的12個(gè)字節(jié)到24個(gè)字節(jié)整整翻了一倍。
引入 Tagged Pointer 內(nèi)存分布
引用了 Tagged Pointer
的對(duì)象,節(jié)省了分配在堆區(qū)的空間,將值存在指針區(qū)域的棧區(qū)。從而節(jié)省了內(nèi)存空間以及大大提升了訪問(wèn)速度。以 NSInteger
封裝成 NSNumber
為例,內(nèi)存分布圖如下:
由分布圖所示,占用內(nèi)存從32位CPU的12個(gè)字節(jié)到8個(gè)字節(jié),還節(jié)省了3個(gè)字節(jié)的內(nèi)存空間。而且引用計(jì)數(shù) retainCount
為最大值。
驗(yàn)證過(guò)程
根據(jù)以上NSNumber的測(cè)試代碼:
NSNumber *num1 = @1;
NSNumber *num2 = @2;
NSNumber *num3 = @3;
NSNumber *num4 = @(3.1415927);
NSNumber *num5 = [num1 copy];
NSNumber *num6 = [num4 copy];
打印的結(jié)果如下:
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
說(shuō)明使用 Tagged Pointer
的對(duì)象的值都會(huì)存儲(chǔ)在指針的值里。以上打印結(jié)果,可看出 0xb
開(kāi)頭的地址都是 Tagged Pointer
,只要把前面的 0xb
和 尾部的 2
去掉,剩下的就是真正的值。具體的存儲(chǔ)細(xì)節(jié),可參考 tagged-pointers 文檔。
而打印結(jié)果中的 num4
變量存儲(chǔ)的是雙精度浮點(diǎn)數(shù),棧區(qū)存不了,所以會(huì)在堆區(qū)開(kāi)辟空間存儲(chǔ)。
特點(diǎn)總結(jié)
Tagged Pointer
的引用主要解決內(nèi)存浪費(fèi)和訪問(wèn)效率的問(wèn)題。所以其有以下特點(diǎn):
-
Tagged Pointer
專門用于存儲(chǔ)小的對(duì)象,例如:NSString
、NSNumber
和NSDate
。 -
Tagged Pointer
指針的值不再是堆區(qū)地址,而是真正的值。所以,實(shí)際上它不再是一個(gè)對(duì)象了,它只是一個(gè)披著對(duì)象皮的普通變量而已。所以,它的內(nèi)存并不存儲(chǔ)在堆中,也不需要malloc
和free
。 - 在內(nèi)存讀取上有著 3 倍的效率,創(chuàng)建時(shí)比以前快 106 倍。
如此可見(jiàn),Apple 引入了 Tagged Pointer
不僅僅節(jié)省了64位處理器的占用內(nèi)存空間,還提高了運(yùn)行效率。
使用注意點(diǎn)
Tagged Pointer
并不是真正的指針,由測(cè)試代碼的變量?jī)?nèi)存分布截圖,都可表明其對(duì)應(yīng)的 isa
指針已經(jīng)指向 0x0
地址。所以如果你直接訪問(wèn) Tagged Pointer
的 isa
成員的話,編譯時(shí)期將會(huì)有警告。可以通過(guò)調(diào)用 isKindOfClass
和 object_getClass
,避免直接訪問(wèn)對(duì)象的 isa
變量。
結(jié)論
在iOS的日常開(kāi)發(fā)中,同樣內(nèi)容的字符串常量 __NSCFConstantString
全局只有一份,放在堆區(qū),并且不會(huì)被釋放(retainCount值最大)。并且由于有 Tagged Pointer
的存在,盡量避免直接訪問(wèn)對(duì)象的 isa
變量。