寫這篇文章源于組內(nèi)同事的一個分享,在分享過程中,我們對 Tagged Pointer
有一些疑問,但是網(wǎng)上又沒有找到很好的相關(guān)資料來進行解釋。分享完之后,我讀了 Tagged Pointer
在 objc
源碼中相關(guān)的內(nèi)容,對這一點有了比較深入和細致的了解,就此記錄一下。
Tagged Pointer 介紹
Tagged Pointer
是蘋果在 2013
年在 Objective-C
中增加的一個技術(shù),主要是為了解決當時推出 64
位系統(tǒng)架構(gòu),指針變量由 32
位增加到 64
位。大家都知道 Objective-C
對象都是分配在堆上面的,在棧上面有一個指針地址指向堆的內(nèi)存地址,但是對于一些變量比如:@1
@"abc"
,如果同時在棧上面和在堆上面都去分配內(nèi)存空間的話,那么就太浪費存儲空間了;所以蘋果采取了一個方式將一些比較小的數(shù)據(jù)直接存儲在指針變量中,這些指針變量就稱為 Tagged Pointer
。
那具體是如何實現(xiàn)的呢?以及都有哪些類型的數(shù)據(jù)可以使用這些技術(shù)呢?
Tagged Pointer 實現(xiàn)
在講述實現(xiàn)之前大家先看幾個 Tagged Pointer
的指針地址,我們在 macOS
平臺下和在 iOS
平臺下分別運行如下代碼:
NSNumber *number1 = @1;
NSLog(@"number1 %p %@", number1, [number1 class]);
NSNumber *number2 = @2;
NSLog(@"number2 %p %@", number2, [number2 class]);
NSString *a = [[@"a" mutableCopy] copy];
NSLog(@"a %p %@", a, [a class]);
NSString *ab = [[@"ab" mutableCopy] copy];
NSLog(@"ab %p %@", ab, [ab class]);
macOS print:
number1 0x127 __NSCFNumber
number2 0x227 __NSCFNumber
a 0x6115 NSTaggedPointerString
ab 0x626125 NSTaggedPointerString
iOS print:
number1 0xb000000000000012 __NSCFNumber
number2 0xb000000000000022 __NSCFNumber
a 0xa000000000000611 NSTaggedPointerString
ab 0xa000000000062612 NSTaggedPointerString
通過觀察上面的打印地址不妨作一些簡單的推測。其中地址 0x127
和 0x227
不同的就是 1
2
那我們可以這么認為,1
2
代表的就是對應的數(shù)字 1
2
,而后面的 27
中的某些信息則標識了這個指針為 Tagged Pointer
。地址 0x6115
和 0x626125
前面不同的是 61
6261
,而這兩個數(shù)字正好對應字符 a
ab
的 ASCII
編碼值,而后面不同的 15
和 25
中的某些信息則標識了這個指針為 Tagged Pointer
。后面對應打印的 class
類型也可以印證。但 NSNumber
蘋果并沒有設(shè)計一個單獨的 class
來表示 Tagged Pointer
。在看 iOS
的打印也有類似的規(guī)律。
那怎么判斷一個指針是不是 Tagged Pointer
呢?可以通過 objc
源碼看到對應的判斷方法如下:
static inline bool
_objc_isTaggedPointer(const void *ptr)
{
return ((intptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1ULL<<63)
#else
# define _OBJC_TAG_MASK 1
#endif
#if TARGET_OS_OSX && __x86_64__
// 64-bit Mac - tag bit is LSB
# define OBJC_MSB_TAGGED_POINTERS 0
#else
// Everything else - tag bit is MSB
# define OBJC_MSB_TAGGED_POINTERS 1
#endif
通過上面的代碼可以看到判斷是否是 Tagged Pointer
僅僅是將地址和 _OBJC_TAG_MASK
這個常量做了一個簡單的與運算,而 _OBJC_TAG_MASK
常量在不同平臺下值值也不同,在 macOS
和 __x86_64__
下值為1(使用低位優(yōu)先規(guī)則 LSB
),其他的平臺值為 1ULL<<63
(使用高位優(yōu)先規(guī)則 MSB
)。那么這樣的話只要最高位或者最低位為 1
,那么這個指針就是 Tagged Pointer
。
為什么可以通過設(shè)定最高位或者最低位是否為 1
來標識呢?
這是因為在分配內(nèi)存的時候,都是按 2
的整數(shù)倍來分配的,這樣分配出來的正常內(nèi)存地址末位不可能為 1
,這樣通過將最低標識為 1
,就可以和其他正常指針做出區(qū)分。那么為什么最高位為 1
,也可以標識呢?這是因為64
位操作系統(tǒng),設(shè)備一般沒有那么大的內(nèi)存,所以內(nèi)存地址一般只有 48
個左右有效位,也就是說高位的 16
位左右都為 0
,所以可以通過最高位標識為 1
來表示 Tagged Pointer
。那么既然一位就可以標識 Tagged Pointer
了其他的信息是干嘛的呢?我們可以想象,要有一些 bit
位來表示這個指針對應的類型,不然拿到一個Tagged Pointer
的時候我們不知道類型,就無法解析成對應的值。
那么具體是怎么來表示類型的呢?繼續(xù)翻看源碼可以找到如下定義:
enum
{
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
OBJC_TAG_RESERVED_7 = 7,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
當我們看到這些定義的時候,就恍然大悟了。0 ~ 7
分別表示類型。而另外 OBJC_TAG_First60BitPayload
等定義分別表示前 60
位或者 52
位為負載內(nèi)容,還是后 60
位或者后 52
位為負載內(nèi)容。
那么這時候我們試著去解讀 0x127
這個指針:首先轉(zhuǎn)換為對應的二進制 則為 .... 0001 0010 0111
最后一位 1
表示是 Tagged Pointer
,由于我們推測倒數(shù)第九位的 1
對應真實值 1
,那么這個就不可能是前 52
位為負載,只能是前 60
位為負載,那么還剩下 011
十進制是 3
正好是 OBJC_TAG_NSNumber
的值。這時候我們只剩下 0010
這個不能解釋。
不急我們試著去執(zhí)行一下其他的一些代碼來看看是否能發(fā)現(xiàn)一些端倪,執(zhí)行以下代碼:
int int1 = 1;
long long long1 = 1;
float float1 = 1.0;
double double1 = 1.0;
NSNumber *intNumber1 = @(int1);
NSNumber *longNumber1 = @(long1);
NSNumber *floatNumber1 = @(float1);
NSNumber *doubleNumber1 = @(double1);
NSLog(@"intNumber1 %p %@", intNumber1, [intNumber1 class]);
NSLog(@"longNumber1 %p %@", longNumber1, [longNumber1 class]);
NSLog(@"floatNumber1 %p %@", floatNumber1, [floatNumber1 class]);
NSLog(@"doubleNumber1 %p %@", doubleNumber1, [doubleNumber1 class]);
macOS print:
intNumber1 0x127 __NSCFNumber
longNumber1 0x137 __NSCFNumber
floatNumber1 0x147 __NSCFNumber
doubleNumber1 0x157 __NSCFNumber
哈哈,這時候中間的值代表了什么就一目了然了。2
對應的是 int
類型,3
對應的是 long
類型, 4
對應的是 float
類型,5
對應的是 double
類型。這樣就可以完全解釋 0x127
這個指針了。而且也很容易猜出字符串地址不同的 15
和 25
中的 1
2
,分別對應字符串的長度。以此類推就不難解釋其他的地址,甚至一些OBJC_TAG_NSIndexPath
OBJC_TAG_NSDate
等類型的地址。
Tagged Pointer 其他的一些點
現(xiàn)在我們已經(jīng)完全明白了 Tagged Pointer
是如何表示的。除此之外,我們還可以考慮一些其他的,比如表示NSNumber
的時候,由于剩下 56
位是負載那么表示的最大值只能為 2^56-1
,大家也可以去驗證。而字符串如果按 ASCII
來編碼的話,56
位只能存儲下 7
個字符,事實是不是這樣呢? 你可以自己去驗證,但是這里可以告訴你,答案是否定的,具體相關(guān)的知識,大家可以參考這篇文章Tagged Pointer Strings。另外由于 Tagged Pointer
是一個偽對象,所以內(nèi)部并沒有 isa
指針。Tagged Pointer
為我們帶來了 3
倍存儲空間的節(jié)省,以及 106
倍的訪問速度,通過這點也可以看出來蘋果背后為性能做出的一些優(yōu)化。