Objective-C對(duì)象的TaggedPointer特性

前言

前段時(shí)間,看到在知識(shí)小集的交流群里正在討論 copymutableCopy 的相關(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)類:NSStringNSMutableString__NSCFConstantString__NSCFStringNSTaggedPointerString

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è)試代碼主要分為兩部分:NSStringNSMutableString。當(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)存分布截圖:

NSString變量狀態(tà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)存分布截圖:

NSMutableString變量狀態(tà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è)空間,并且 retainCount1,也就是說(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__NSCFNumberNSValue

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)存分布截圖:

NSNumber變量狀態(tà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è)空間,并且 retainCount1。該類的定義在 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ì) NSStringNSNumberNSDate等對(duì)象進(jìn)行優(yōu)化。

未引入 Tagged Pointer 內(nèi)存分布

一般的 iOS 程序,從32位遷移到64位CPU,雖然邏輯上是不會(huì)有任何變化,但是所占有的內(nèi)存空間就會(huì)翻倍。以 NSInteger 封裝成 NSNumber 為例,內(nèi)存分布圖如下:

未引入TaggedPointer內(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)存分布圖如下:

引入TaggedPointer內(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):

  1. Tagged Pointer 專門用于存儲(chǔ)的對(duì)象,例如:NSStringNSNumberNSDate
  2. Tagged Pointer指針的值不再是堆區(qū)地址,而是真正的值。所以,實(shí)際上它不再是一個(gè)對(duì)象了,它只是一個(gè)披著對(duì)象皮的普通變量而已。所以,它的內(nèi)存并不存儲(chǔ)在堆中,也不需要 mallocfree
  3. 在內(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 Pointerisa 成員的話,編譯時(shí)期將會(huì)有警告。可以通過(guò)調(diào)用 isKindOfClassobject_getClass,避免直接訪問(wèn)對(duì)象的 isa 變量。

結(jié)論

在iOS的日常開(kāi)發(fā)中,同樣內(nèi)容的字符串常量 __NSCFConstantString 全局只有一份,放在堆區(qū),并且不會(huì)被釋放(retainCount值最大)。并且由于有 Tagged Pointer 的存在,盡量避免直接訪問(wèn)對(duì)象的 isa 變量。

參考文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容