前言
在之前描述isa和objc_object的結(jié)構(gòu)體的時候,都有涉及到TaggedPointer的概念??紤]到TaggedPointer本身也有其自己的一套內(nèi)存結(jié)構(gòu)和特征,因此,專門拿出來做一個專題。
何為TaggedPointer
TaggedPointer直譯的話,就是“帶有標(biāo)記的指針”。實際上TaggedPointer是一種及其特殊的對象。我們都知道在iOS中,多有的對象都是objc_object的機(jī)構(gòu)體。當(dāng)我們聲明一個指針后,指針的地址就指向它。而TaggedPointer不一樣,它不能稱其為一個指針,但它確實也是64位長。在這64位當(dāng)中,不僅標(biāo)記了TaggedPointer到底是什么類型的值。更關(guān)鍵的是,TaggedPointer的值本身也被存在了這個64位長度當(dāng)中。具體如下圖所示:

上圖中描述的是iOS設(shè)備上的內(nèi)存布局。如果是其他設(shè)備上內(nèi)存布局會有所變化,但這不在我們的討論范圍。
我們可以看到TaggedPointer中主要由4部分組成。
第一部分只占1位,是nonpointer位,這與isa中的內(nèi)存布局是一樣的,且含義也一樣。
第二部分占3個位,其作用是標(biāo)記當(dāng)前TaggedPointer的實際類型的編號。
第三部分占56個位,主要用來存儲TaggedPointer的值。
第四部分占4個位,用來記錄當(dāng)前值的長度。
這里要注意的是,如果直接在設(shè)備上打印地址,即使你看到它是一個TaggedPointer對象,但其地址仍舊不會展現(xiàn)成上圖中的內(nèi)存分布。這是因為系統(tǒng)為TaggedPointer的地址做了混淆。
源碼解析
我們從源碼當(dāng)中就可以看出端倪
//objc-internal.h
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
// PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
// They are reversed here for payload insertion.
// ASSERT(_objc_taggedPointersEnabled());
if (tag <= OBJC_TAG_Last60BitPayload) {
// ASSERT(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
} else {
// ASSERT(tag >= OBJC_TAG_First52BitPayload);
// ASSERT(tag <= OBJC_TAG_Last52BitPayload);
// ASSERT(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_EXT_MASK |
((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
}
}
這個函數(shù)就是用來生成一個TaggedPointer的方法,其入?yún)⑹且粋€Tag類型,和一個64位的值。這里可以先說結(jié)論,入?yún)ag就是TaggedPointer的類型索引下標(biāo)。也就是之前篇幅里提到的從objc_tag_classes數(shù)組中獲取類型。第二個64位的參數(shù)就是TaggedPointer的值,可以認(rèn)定的是,這64位中,后4位是值的長度,接著的56位都是值的存儲空間。
現(xiàn)在回到源碼上,根據(jù)上面的代碼。
- 判斷類型的值是否小于等于OBJC_TAG_Last60BitPayload的值,那么我們先看一下這個值的定義。
{
// 60-bit payloads
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,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_NSColor = 16,
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
從這個代碼中,我們可以看到OBJC_TAG_Last60BitPayload的值位6,也就是說上面的代碼規(guī)定了系統(tǒng)定義的標(biāo)準(zhǔn)的TaggedPointer只有7種,也就是最開頭的0~6的類型。剩下的都被認(rèn)定為擴(kuò)展的TaggedPointer類型。
根據(jù)前面的條件語句判斷,先來看看如果為true的情況:
聲明一個64位的值,然后將tag和value一頓操作,最終獲得一個result的值,接著調(diào)用_objc_encodeTaggedPointer函數(shù)來進(jìn)行編碼。
先來看看那一頓操作都是什么
(1)((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) 將tag的值左移動60位(_OBJC_TAG_INDEX_SHIFT的值為60)。這樣就相當(dāng)于只保留了tag原值的最后4位。根據(jù)前面的定義,tag的系統(tǒng)類型值有7種,因此用4位也足以保存了。
(2)((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) 將value的值先左移4位,再右移4位。這就相當(dāng)于把value值的前4位去掉,再去掉由于左移自動填上的尾部4個0的,最終掐頭去尾的值。
(3)用第一步和第二步的值進(jìn)行或運(yùn)算,相當(dāng)于把第一步的值填在了第二步值的前4位上。此時,TaggedPointer的值已經(jīng)是頭4位為類型,后面是value+長度的值。
(4)第三步的值與_OBJC_TAG_MASK做或運(yùn)算。_OBJC_TAG_MASK的定義是1UL<<63,相當(dāng)于是1后面跟著63個0。此時第三部的值的第一位就變成了1。如果按照isa來看,這就相當(dāng)于第一位nonpointer位設(shè)置為了1。
(5)將最終的值賦值給result變量,并傳入_objc_encodeTaggedPointer函數(shù)進(jìn)行編碼,并返回結(jié)果。如果判斷條件為false的情況:
false的情況就意味著tag的值一定大于6。而從上面的定義看,7為保留字段,因此可以斷定擴(kuò)展的taggedPointer的tag值一定大于等于8。在明確這一點(diǎn)后,仍舊是先聲明一個64為的result變量,然后再對tag和value進(jìn)行操作
(1)((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) 。先將tag減去8(OBJC_TAG_First52BitPayload = 8),然后再左移動52位,也就留下了底12位的值。
(2)(value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT)。將value的值去掉頭12位。
(3)第一步和第二步進(jìn)行合并,將第一步的頭12位寫到底二步的值的里面。
(4)與_OBJC_TAG_EXT_MASK(oxff)做或運(yùn)算,即,將最終值的頭4位全部變成1。
(5)將最終的值賦值給result變量,并傳入_objc_encodeTaggedPointer函數(shù)進(jìn)行編碼,并返回結(jié)果。到這里來看看_objc_encodeTaggedPointer都做了什么事情。
extern uintptr_t objc_debug_taggedpointer_obfuscator;
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
從上面的代碼上就可以看出,所謂的編碼就是用傳進(jìn)來的值,也就是前面說的result與一個objc_debug_taggedpointer_obfuscator的值進(jìn)行異或。這樣做完,你看到的TaggedPointer的值就更像一個指針的地址而不是結(jié)構(gòu)明顯的值了。順便說一下objc_debug_taggedpointer_obfuscator的值也是一個ptr類型,系統(tǒng)每次初始化時會對其進(jìn)行初始化。
當(dāng)然,也由此知道編碼即然是這樣做的,那么解碼必然是再次與objc_debug_taggedpointer_obfuscator的值進(jìn)行異或。有代碼為證
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
其他
下面,看一看系統(tǒng)是如何針對以上協(xié)議來獲取TaggedPointer類型的
//objc-internal.h
static inline objc_tag_index_t
_objc_getTaggedPointerTag(const void * _Nullable ptr)
{
// ASSERT(_objc_isTaggedPointer(ptr));
uintptr_t value = _objc_decodeTaggedPointer(ptr);
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
uintptr_t extTag = (value >> _OBJC_TAG_EXT_INDEX_SHIFT) & _OBJC_TAG_EXT_INDEX_MASK;
if (basicTag == _OBJC_TAG_INDEX_MASK) {
return (objc_tag_index_t)(extTag + OBJC_TAG_First52BitPayload);
} else {
return (objc_tag_index_t)basicTag;
}
}
首先,傳入TaggedPointer的指針(其實就是個值),然后使用解碼函數(shù)進(jìn)行解碼。這個解碼函數(shù)上面已經(jīng)介紹過了,這里就不再贅述。
其次,使用解碼后的value右移60位(_OBJC_TAG_INDEX_SHIFT=60)。這樣就獲得了高4位的值。然后在與0x7進(jìn)行與操作(_OBJC_TAG_INDEX_MASK=0x7)。0x7就是0111,這樣與value與操作后,等于就要頭4位的后3位的值作為basicTag的值。
再次,使用解碼后的value右移52位(_OBJC_TAG_EXT_INDEX_SHIFT=52)。這樣就獲得了高12位的值,然后再與0xff進(jìn)行與操作(_OBJC_TAG_EXT_INDEX_MASK 0xff)。這就相當(dāng)于這就相當(dāng)于只要12位中的后8位。這后8位的值就做為extTag的值。
再次,判斷basicTag是不是等于7,如果是,則認(rèn)定當(dāng)前TaggedPointer的類型為擴(kuò)展型(ext)。再之上面介紹過,ext的類型值是被減去8的值。所以這里要加上8然后返回。
最后,如果不等于7,則認(rèn)為是默認(rèn)類型的TaggedPointer,直接返回basicTag即可。
縷清楚runtime是如何獲取TaggedPointer的類型后,如何獲取值也就呼之欲出了。
//objc-internal.h
static inline uintptr_t
_objc_getTaggedPointerValue(const void * _Nullable ptr)
{
// ASSERT(_objc_isTaggedPointer(ptr));
uintptr_t value = _objc_decodeTaggedPointer(ptr);
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
if (basicTag == _OBJC_TAG_INDEX_MASK) {
return (value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
} else {
return (value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
}
}
static inline intptr_t
_objc_getTaggedPointerSignedValue(const void * _Nullable ptr)
{
// ASSERT(_objc_isTaggedPointer(ptr));
uintptr_t value = _objc_decodeTaggedPointer(ptr);
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
if (basicTag == _OBJC_TAG_INDEX_MASK) {
return ((intptr_t)value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
} else {
return ((intptr_t)value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
}
}
以上兩個函數(shù),除了返回值不同,內(nèi)部取值幾乎一樣。
- 將傳入的TaggedPointer的地址進(jìn)行解碼
- 獲取basicTag的值,也就是頭4位中后三位的值
- 如果basicTag == 7,則認(rèn)定為Ext類型的TaggedPointer。然后獲取value中后52位的值
- 如果basicTag != 7,則認(rèn)定為默認(rèn)類型的TaggedPointer。然后獲取value中的后60位的值
最后,再來說說TaggedPointer的存值。以NSString為例,實際上TaggedPointer只能存儲9個ASCII碼的字符。但自己算下來,64個位,減去頭4位,再減去4位的長度,實際上只有56位,ASCII碼一個字符占1個字節(jié),也就是8位。那么最多也就存7個字符。結(jié)果實驗證明可以存9個。這就證明在存儲時使用了某些壓縮方法,使得9個字符可以存在7個字節(jié)里。至于是什么算法,不知道。。。原代碼里沒找到。
至此,我們基本就介紹完了TaggedPointer類型在runtime中是如何定義,存取值以及它的內(nèi)存機(jī)構(gòu)是什么樣的。打完收工!