ObjC-Runtime TaggedPointer專題

前言

在之前描述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)中。具體如下圖所示:


taggedpointer.png

上圖中描述的是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ù)上面的代碼。

  1. 判斷類型的值是否小于等于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類型。

  1. 根據(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é)果。

  2. 如果判斷條件為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é)果。

  3. 到這里來看看_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)部取值幾乎一樣。

  1. 將傳入的TaggedPointer的地址進(jìn)行解碼
  2. 獲取basicTag的值,也就是頭4位中后三位的值
  3. 如果basicTag == 7,則認(rèn)定為Ext類型的TaggedPointer。然后獲取value中后52位的值
  4. 如果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)是什么樣的。打完收工!

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

友情鏈接更多精彩內(nèi)容