OC-內(nèi)存管理Tagged Pointer

1. 簡介

Tagged Pointer是蘋果在64bit設(shè)備提出的一種存儲小對象的技術(shù),它具有以下特點

  • Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已。
  • 它的內(nèi)存并不存儲在堆中,也不需要 malloc 和 free,不走引用計數(shù)那一套邏輯,由系統(tǒng)來處理釋放
  • 在內(nèi)存讀取上有著 3 倍的效率,創(chuàng)建時比以前快 106 倍。
  • 可以通過設(shè)置環(huán)境變量OBJC_DISABLE_TAGGED_POINTERS來有開發(fā)者決定是否使用這項技術(shù)

2. 源碼

源碼是基于runtime的obj4-779.1版本

2.1 各種標(biāo)記位
#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __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

#define _OBJC_TAG_INDEX_MASK 0x7 // 0b111表示有擴展的標(biāo)記位,擴展標(biāo)記位占8位
// array slot includes the tag bit itself
#define _OBJC_TAG_SLOT_COUNT 16
#define _OBJC_TAG_SLOT_MASK 0xf // 0b1111 taggedpointer + 有擴展標(biāo)記位的mask

#define _OBJC_TAG_EXT_INDEX_MASK 0xff
// array slot has no extra bits
#define _OBJC_TAG_EXT_SLOT_COUNT 256 // 擴展標(biāo)記位能表示的個數(shù)
#define _OBJC_TAG_EXT_SLOT_MASK 0xff // 0b1111 1111

#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63) // 是否是tagged pointer的標(biāo)記位 1表示是 0表示不是
#   define _OBJC_TAG_INDEX_SHIFT 60 // 基礎(chǔ)tag位的偏移,從2-4bit,結(jié)合_OBJC_TAG_INDEX_MASK來獲取到基礎(chǔ)tag的值
#   define _OBJC_TAG_SLOT_SHIFT 60
#   define _OBJC_TAG_PAYLOAD_LSHIFT 4 // LSHIFT和RSHIFT配合使用用來對數(shù)據(jù)移位混淆及恢復(fù)
#   define _OBJC_TAG_PAYLOAD_RSHIFT 4
#   define _OBJC_TAG_EXT_MASK (0xfUL<<60) // 1111 0000 ... 0000 0000 是否有擴展標(biāo)記位 tag位111表示有擴展標(biāo)記位
#   define _OBJC_TAG_EXT_INDEX_SHIFT 52 // 擴展tag位的偏移,從5-12bit共8位,結(jié)合_OBJC_TAG_EXT_INDEX_MASK來獲取擴展tag的值
#   define _OBJC_TAG_EXT_SLOT_SHIFT 52
#   define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 12
#   define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
#else
#   define _OBJC_TAG_MASK 1UL
#   define _OBJC_TAG_INDEX_SHIFT 1
#   define _OBJC_TAG_SLOT_SHIFT 0
#   define _OBJC_TAG_PAYLOAD_LSHIFT 0
#   define _OBJC_TAG_PAYLOAD_RSHIFT 4
#   define _OBJC_TAG_EXT_MASK 0xfUL
#   define _OBJC_TAG_EXT_INDEX_SHIFT 4
#   define _OBJC_TAG_EXT_SLOT_SHIFT 4
#   define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 0
#   define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
#endif

定義了很多位信息,我們需要關(guān)注的幾個:

  • _OBJC_TAG_MASK :標(biāo)記位標(biāo)記該指針是否是tagged pointer
  • _OBJC_TAG_INDEX_MASK :tag的值是7表示有擴展的tag位
  • 其他的都是一些定義,用來通過位運算來獲取tag的值、ext tag的值的mask以及一些其他的左移右移位
2.2 如何判斷是tagged pointer

我們知道有一個標(biāo)記位來標(biāo)識指針是否是tagged pointer的

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

通過位運算獲取標(biāo)識位的值來確定是否是tagged pointer;需要留意的是不同的架構(gòu)標(biāo)記位不太一樣,有的是用最低位、有的使用最高位。

2.3 系統(tǒng)對tagged pointer的加密

在iOS12系統(tǒng)之前,我們發(fā)現(xiàn)是可以直接打印tagged pointer的值的,可讀性非常好,但是12之后再打印就發(fā)現(xiàn)完全看不懂了。

- (void)testCase {
    NSString *stringWithFormat1 = [NSString stringWithFormat:@"y"];
    [self formatedLogObject:stringWithFormat1];
}

- (void)formatedLogObject:(id)object {
    if (@available(iOS 12.0, *)) {
        NSLog(@"%p %@ %@", object, object, object_getClass(object));
    } else {
        NSLog(@"0x%6lx %@ %@", object, object, object_getClass(object));
    }
}

上面的測試代碼,在12之前輸出:
0x79是ASCII對應(yīng)的y字符的值

0xa000000000000791 y NSTaggedPointerString

iOS12之后輸出:

0xcb47b8d98a2fa15f y NSTaggedPointerString

iOS12之前打印指針的值能很清晰的看到數(shù)據(jù)等信息,iOS12之后系統(tǒng)則打印的完全看不懂了,看了源代碼發(fā)現(xiàn)蘋果是做了混淆,讓我們不能直接得到值,從而避免我們?nèi)ズ苋菀拙蛡卧斐鲆粋€tagged pointer對象

蘋果是如何混淆的了
第一步:位移運算,右左橫移

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ù)異或運算一下

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr); // objc_debug_taggedpointer_obfuscator 通過異或混淆一下指針的值
}

這個隨機數(shù)是在dyld加載image的時候去隨機產(chǎn)生的,每次程序加載都不一樣

// map_images -- map_images_nolock -- _read_images -- initializeTaggedPointerObfuscator
static void
initializeTaggedPointerObfuscator(void)
{
    // 初始化一個taggedpointer的掩碼,每次_objc_init的時候都生成一個
    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        DisableTaggedPointerObfuscation) {
        objc_debug_taggedpointer_obfuscator = 0;
    } else {
        // Pull random data into the variable, then shift away all non-payload bits.
        // 隨機一個掩碼,然后再做運算;基本上每次app啟動獲取到的都是不一樣的
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    }
}

簡直喪心病狂啊,嘗試去hook一下,將這個值拿到或者修改成一個固定的值來好debug,最終沒搞成。

混淆之后怎么解密了
正常就是按照上面的混淆的方式,反向操作一波
第一步,異或那個固定的隨機數(shù),得到值

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator; // 再次異或就得到了原始的值
}

這里舉個例子,就一目了然了;可以看到encode的時候異或一次,在decode的時候再異或一次就得到原始值了。

// 假設(shè) objc_debug_taggedpointer_obfuscator位 0010、原始數(shù)據(jù)是1001
encode:1001 ^ 0010 = 1011
decode:1011 ^ 0010 = 1001

第二步,再左右橫移回去

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;
    }
}
2.4 Tagged Pointer對象

系統(tǒng)通過3bit的標(biāo)記位來標(biāo)識tagged pointer對象的類,它的定義在objc_tag_index_t
比如2表示是NSString、6表示是NSDate,我們知道3bit能表示的最大值是7,這個7系統(tǒng)用來預(yù)留,用來標(biāo)記是否有額外的標(biāo)記位,這樣就能支持更多的類支持tagged pointer

#if __has_feature(objc_fixed_enum)  ||  __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
    // 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
};
#if __has_feature(objc_fixed_enum)  &&  !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif

3. 延伸

3.1 字符串編碼

看博客發(fā)現(xiàn)很多都說字符在長度超過10的時候就不再是NSTaggedPointerString了,其實這種說法是不準(zhǔn)確的;

TaggedPointer用來表示值的位數(shù)payload是固定的,但是采用不同的字符編碼格式就能表示的位數(shù)也就不一樣了;

下面的測試?yán)樱?1位的字符串它也是TaggedPointer

NSString *test2 = [NSString stringWithFormat:@"%@", @"11111111111"];
    NSLog(@"%p value:0x%lx %@ %@", test2, _objc_getTaggedPointerValue((__bridge const void *)test2), test2, object_getClass(test2));

輸出結(jié)果:

0xfa976f93575a8e75 value:0x7bdef7bdef7bdeb 11111111111 NSTaggedPointerString

關(guān)于字符串的編碼這一塊,還是挺復(fù)雜的,感興趣的可以參照這篇博客學(xué)習(xí)一下系統(tǒng)是如何在創(chuàng)建一個字符串的時候去編碼的
Tagged Pointer Strings by Mike Ash

Thus we can see that the structure of the tagged pointer strings is:
If the length is between 0 and 7, store the string as raw eight-bit characters.
If the length is 8 or 9, store the string in a six-bit encoding, using the alphabet "eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX".
If the length is 10 or 11, store the string in a five-bit encoding, using the alphabet "eilotrm.apdnsIc ufkMShjTRxgC4013"
摘自Tagged Pointer Strings by Mike Ash

3.2 iOS14對tagged pointer的優(yōu)化

在intel中tagged pointer指針的數(shù)據(jù)結(jié)構(gòu)如下:

圖片.png

  • 通過最低位來標(biāo)記是否是tagged pointer
  • 3位tag來標(biāo)記數(shù)據(jù)的class
  • 擴展的8位來表示更多的類型的tagged pointer

在arm中tagged pointer指針的數(shù)據(jù)結(jié)構(gòu)如下:
iOS13系統(tǒng):

圖片.png

為什么要做這樣的改變了?

圖片.png

主要是針對objc_msgSend的調(diào)用的優(yōu)化,在intel的結(jié)構(gòu)下,判斷tagged pointer和nil需要分2個分支去判斷;而如果將最高位設(shè)置位1了,那么只需要做一次判斷就可以判斷對象是否是正常指針了。
if (ptrValue <= 0) // is tagged or nil其他情況就是正常的指針

iOS14:
iOS14跟iOS13的區(qū)別則將tag放在了最后三位;tagged pointer的標(biāo)記位還是保持在最高位

圖片.png

為什么要這樣做了?

蘋果的解釋是:

  • ARM有個特性就是dyld會忽略指針的前8bit(這是由于ARM的Top Byte Ignore特性);
  • 這樣布局數(shù)據(jù)之后,圖中的payload就跟普通指針的payload是一毛一樣了,也就是tagged pointer的payload(有效負載位)有包含一個正常的指針的能力;
  • 這使得tagged pointer具備了引用二進制文件中的常量數(shù)據(jù)的能力,例如字符串或其他數(shù)據(jù)結(jié)構(gòu),這減少了dirty memory的使用

看到這里我只能說蘋果牛逼666

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

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