Objective-C對象的TaggedPointer特性

前言

前段時間,看到在知識小集的交流群里正在討論 copymutableCopy 的相關特性。所以自己寫了一個 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對象,并不是真的對象

測試代碼

測試代碼主要分為兩部分:NSStringNSMutableString。當然,會通過這兩部分代碼說明問題。

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);

變量內存分布截圖:

NSString變量狀態(tài)

打印的結果如下:

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);

變量內存分布截圖:

NSMutableString變量狀態(tài)

打印的結果如下:

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ū)會分配一個空間,并且 retainCount1,也就是說按正常對象的生命周期被釋放。該類的定義在 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);

變量內存分布截圖:

NSNumber變量狀態(tài)

打印的結果如下:

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ū)會分配一個空間,并且 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。不過,需要啟動 Tagged Pointer 的條件和字符串的 NSTaggedPointerString條件一樣如下:

  • 64位處理器
  • 數(shù)字較小,棧區(qū)能夠裝得下

Tagged Pointer 特性分析

為了改進從 32位CPU 遷移到 64位CPU內存浪費和效率問題,在 64位CPU 環(huán)境下,引入了 Tagged Pointer 對象。有了這樣的機制,系統(tǒng)會對 NSString、NSNumberNSDate等對象進行優(yōu)化。

未引入 Tagged Pointer 內存分布

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

未引入TaggedPointer內存分布圖

由分布圖所示,占用內存從32位CPU的12個字節(jié)24個字節(jié)整整翻了一倍。

引入 Tagged Pointer 內存分布

引用了 Tagged Pointer 的對象,節(jié)省了分配在堆區(qū)的空間,將值存在指針區(qū)域的棧區(qū)。從而節(jié)省了內存空間以及大大提升了訪問速度。以 NSInteger 封裝成 NSNumber 為例,內存分布圖如下:

引入TaggedPointer內存分布圖

由分布圖所示,占用內存從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 的引用主要解決內存浪費訪問效率的問題。所以其有以下特點:

  1. Tagged Pointer 專門用于存儲的對象,例如:NSString、NSNumberNSDate。
  2. Tagged Pointer指針的值不再是堆區(qū)地址,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已。所以,它的內存并不存儲在堆中,也不需要 mallocfree
  3. 在內存讀取上有著 3 倍的效率,創(chuàng)建時比以前快 106 倍。

如此可見,Apple 引入了 Tagged Pointer 不僅僅節(jié)省了64位處理器的占用內存空間,還提高了運行效率。

使用注意點

Tagged Pointer 并不是真正的指針,由測試代碼的變量內存分布截圖,都可表明其對應的 isa 指針已經(jīng)指向 0x0 地址。所以如果你直接訪問 Tagged Pointerisa 成員的話,編譯時期將會有警告??梢酝ㄟ^調用 isKindOfClassobject_getClass,避免直接訪問對象的 isa 變量。

結論

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

參考文檔

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容