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数据。

你可能感兴趣的:(iOS底层探索-内存管理-内存管理方案(TaggedPointer&NONPOINTER_ISA))