iOS底層探索-內存管理-內存管理方案(TaggedPointer&NONPOINTER_ISA)

內存管理方案

  1. TaggedPointer
  2. NONPOINTER_ISA

一、TaggedPointer

2020年WWDC【本】老頭講的關于底層的改變

Intel架構

Intel架構上,最后一位表示Tagged pointers標志位,最后接下來的三位代表Tag數據類型,當Tag的值是小于等于6( <= 6)時,有效負載Payload60位,其代表的 7 種數據類型是:

    // 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,
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_NSMethodSignature = 20,
    OBJC_TAG_UTTypeRecord      = 21,

    OBJC_TAG_FirstUnobfuscatedSplitTag = 136, // 128 + 8, first ext tag with high bit set

    OBJC_TAG_Constant_CFString = 136,

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263,

    OBJC_TAG_RESERVED_264      = 264

如果Tag=7,則接下來的8位擴展標簽Extended代表類型(這就有了2^8=256種可表示類型),如UIColorNSIndexSet等,此時有效負載Payload就只有52 位。
ARM64(iOS13之前(含13))架構上正好反過來,第一位表示Tagged pointers標志位,接下來的三位代表Tag數據類型以此類推,剩下的和Intel架構一樣。但為什么ARM64上要這么做呢?主要是考慮objc_msgSend上的優化,這樣就可以使得msgSend中最常見的路徑盡可能的快,實際上就是放在前面,可以在msgSend消息發送路徑上判斷少了,可以判斷Tagged或nil的情況,不用分開判斷,減少一個分支。

ARM64架構iOS13以前(含13)

但是在ARM架構的iOS14以后(含14)這這些位又發生了一些變化,Tag還是放到了第三位,因為根據字節對齊的規則,這三位總是0,所以可以利用這三位。Extended在Tagged pointers后面的高8位,這樣做的原因是因為ARM的Top Bite lgnore特性,使得會忽略指針的錢8位,所以可以利用這個特性。使用了Tagged pointers使得一些小數據的存儲不用存在dirty memory,而是存在clean memory。

ARM64架構iOS13以前(含14)

當字符串長度小等于7的時候,每次運行的結果都是一樣的,所以當長度小于7的時候,其字符串的值直接存在Payload中,根據相應架構和系統版本,可以打印如下:

iOS14.1真機上

當字符串長度大于7是,其表現形式是怎樣的呢?嘗試了長度位9的情況,其類型還是NSTaggedPointerString類型,而且每次運行的值是一樣的,說明Payload不是地址也是值得形式,但是不想7位長度的時候需要8位二進制表示,此時是6位二進制表示字符編碼,其對應的字符串范圍是如下表:

字符串范圍 小于8 表示類型 [8,10)表示類型 [10,11]表示類型 大于11表示類型
eilotrm.apdnslc ufkMShjTRxgC4013 TaggedPointer TaggedPointer TaggedPointer OC對象
eilotrm.apdnslc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX TaggedPointer TaggedPointer OC對象 OC對象
ASCII碼 TaggedPointer OC對象 OC對象 OC對象
NSString長度位9

NSNumber小數值也是直接存在Payload中,并沒有也不需要malloc和free堆內存。這樣做的好處使得讀取速度快了3倍,創建速度快了100多倍。
且對比了一下tag前面4位的值:
Char ? 是 0000 -- 0
Short ?是 0001 -- 1
Int?? 是 0010 -- 2
Long ??是 0011 -- 3

NSNumber類型

當然可以禁用Tagged Pointers,通過添加環境變量OBJC_DISABLE_TAGGED_POINTERS在前面的復選框內打鉤并在后面的value設置位YES就disable,當然在特定的架構系統下,運行在發送objcMsgSend的時候就crash,是因為在底層Tagged pointers是必須的,否則斷言報錯。

源碼

static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
    // PAYLOAD_LSHIFT 和 PAYLOAD_RSHIFT 是一些宏擴展,根據不同的架構值不一樣.
    // OBJC_TAG_Last60BitPayload = 6
    if (tag <= OBJC_TAG_Last60BitPayload) {   // tag<=6時 makeTaggedPointer ,Payload是60位
      
        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 {  // 52位Payload
        
        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);
    }
}

// 編碼過程
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#if OBJC_SPLIT_TAGGED_POINTERS  // 如果支持Tagged pointers ,以下分析是M1電腦的
    // _OBJC_TAG_NO_OBFUSCATION_MASK代表最高兩位為1,最后三位也為1,其他59位都是0
    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
        return (void *)ptr; // 如果已經encode過直接返回
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK; // 將低3位置1,basicTag值是7
    uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag); // permutedTag 值為7
    value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT); // 將value的值保留,并將最低3位置0
    value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;  // 將value上面的低三位置1
#endif
    return (void *)value;
}

// 解碼過程
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;

    value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
    value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT;
#endif
    return value;
}

二、NONPOINTER_ISA

??在【 iOS底層探索--isa位域 】 這篇文章中我們可以知道對象的isa指針不僅僅是指向了類的地址,因為64位二進制,如果僅僅表現類的指針,而在Runtime的運行機制、msgSend消息發送機制、消息轉發機制、對象類的內存管理機制中需要很多額外的輔助標記才能提高效率,所以如果額外定義難免浪費不必要的內存,所以利用isa的64位中非shiftcls的位用于標志諸如,關聯對象標志,C++析構器標志,弱引用,是否正在銷毀,引用計數表等等,可以提高效率的同時節省了很多內存開銷。
可通過修改環境變量OBJC_DISABLE_NONPOINTER_ISA = YES得到一個純的ISA數據。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容