美文网首页
ObjC-Runtime TaggedPointer专题

ObjC-Runtime TaggedPointer专题

作者: 一张懵逼的脸 | 来源:发表于2020-10-19 11:26 被阅读0次

    前言

    在之前描述isa和objc_object的结构体的时候,都有涉及到TaggedPointer的概念。考虑到TaggedPointer本身也有其自己的一套内存结构和特征,因此,专门拿出来做一个专题。

    何为TaggedPointer

    TaggedPointer直译的话,就是“带有标记的指针”。实际上TaggedPointer是一种及其特殊的对象。我们都知道在iOS中,多有的对象都是objc_object的机构体。当我们声明一个指针后,指针的地址就指向它。而TaggedPointer不一样,它不能称其为一个指针,但它确实也是64位长。在这64位当中,不仅标记了TaggedPointer到底是什么类型的值。更关键的是,TaggedPointer的值本身也被存在了这个64位长度当中。具体如下图所示:


    taggedpointer.png

    上图中描述的是iOS设备上的内存布局。如果是其他设备上内存布局会有所变化,但这不在我们的讨论范围。
    我们可以看到TaggedPointer中主要由4部分组成。

    第一部分只占1位,是nonpointer位,这与isa中的内存布局是一样的,且含义也一样。
    第二部分占3个位,其作用是标记当前TaggedPointer的实际类型的编号。
    第三部分占56个位,主要用来存储TaggedPointer的值。
    第四部分占4个位,用来记录当前值的长度。

    这里要注意的是,如果直接在设备上打印地址,即使你看到它是一个TaggedPointer对象,但其地址仍旧不会展现成上图中的内存分布。这是因为系统为TaggedPointer的地址做了混淆。

    源码解析

    我们从源码当中就可以看出端倪

    //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);
        }
    }
    

    这个函数就是用来生成一个TaggedPointer的方法,其入参是一个Tag类型,和一个64位的值。这里可以先说结论,入参tag就是TaggedPointer的类型索引下标。也就是之前篇幅里提到的从objc_tag_classes数组中获取类型。第二个64位的参数就是TaggedPointer的值,可以认定的是,这64位中,后4位是值的长度,接着的56位都是值的存储空间。
    现在回到源码上,根据上面的代码。

    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,也就是说上面的代码规定了系统定义的标准的TaggedPointer只有7种,也就是最开头的0~6的类型。剩下的都被认定为扩展的TaggedPointer类型。

    1. 根据前面的条件语句判断,先来看看如果为true的情况:
      声明一个64位的值,然后将tag和value一顿操作,最终获得一个result的值,接着调用_objc_encodeTaggedPointer函数来进行编码。
      先来看看那一顿操作都是什么
      (1)((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) 将tag的值左移动60位(_OBJC_TAG_INDEX_SHIFT的值为60)。这样就相当于只保留了tag原值的最后4位。根据前面的定义,tag的系统类型值有7种,因此用4位也足以保存了。
      (2)((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) 将value的值先左移4位,再右移4位。这就相当于把value值的前4位去掉,再去掉由于左移自动填上的尾部4个0的,最终掐头去尾的值。
      (3)用第一步和第二步的值进行或运算,相当于把第一步的值填在了第二步值的前4位上。此时,TaggedPointer的值已经是头4位为类型,后面是value+长度的值。
      (4)第三步的值与_OBJC_TAG_MASK做或运算。_OBJC_TAG_MASK的定义是1UL<<63,相当于是1后面跟着63个0。此时第三部的值的第一位就变成了1。如果按照isa来看,这就相当于第一位nonpointer位设置为了1。
      (5)将最终的值赋值给result变量,并传入_objc_encodeTaggedPointer函数进行编码,并返回结果。

    2. 如果判断条件为false的情况:
      false的情况就意味着tag的值一定大于6。而从上面的定义看,7为保留字段,因此可以断定扩展的taggedPointer的tag值一定大于等于8。在明确这一点后,仍旧是先声明一个64为的result变量,然后再对tag和value进行操作
      (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)第一步和第二步进行合并,将第一步的头12位写到底二步的值的里面。
      (4)与_OBJC_TAG_EXT_MASK(oxff)做或运算,即,将最终值的头4位全部变成1。
      (5)将最终的值赋值给result变量,并传入_objc_encodeTaggedPointer函数进行编码,并返回结果。

    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);
    }
    

    从上面的代码上就可以看出,所谓的编码就是用传进来的值,也就是前面说的result与一个objc_debug_taggedpointer_obfuscator的值进行异或。这样做完,你看到的TaggedPointer的值就更像一个指针的地址而不是结构明显的值了。顺便说一下objc_debug_taggedpointer_obfuscator的值也是一个ptr类型,系统每次初始化时会对其进行初始化。
    当然,也由此知道编码即然是这样做的,那么解码必然是再次与objc_debug_taggedpointer_obfuscator的值进行异或。有代码为证

    static inline uintptr_t
    _objc_decodeTaggedPointer(const void * _Nullable ptr)
    {
        return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    }
    

    其他

    下面,看一看系统是如何针对以上协议来获取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的指针(其实就是个值),然后使用解码函数进行解码。这个解码函数上面已经介绍过了,这里就不再赘述。
    其次,使用解码后的value右移60位(_OBJC_TAG_INDEX_SHIFT=60)。这样就获得了高4位的值。然后在与0x7进行与操作(_OBJC_TAG_INDEX_MASK=0x7)。0x7就是0111,这样与value与操作后,等于就要头4位的后3位的值作为basicTag的值。
    再次,使用解码后的value右移52位(_OBJC_TAG_EXT_INDEX_SHIFT=52)。这样就获得了高12位的值,然后再与0xff进行与操作(_OBJC_TAG_EXT_INDEX_MASK 0xff)。这就相当于这就相当于只要12位中的后8位。这后8位的值就做为extTag的值。
    再次,判断basicTag是不是等于7,如果是,则认定当前TaggedPointer的类型为扩展型(ext)。再之上面介绍过,ext的类型值是被减去8的值。所以这里要加上8然后返回。
    最后,如果不等于7,则认为是默认类型的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;
        }
    }
    
    

    以上两个函数,除了返回值不同,内部取值几乎一样。

    1. 将传入的TaggedPointer的地址进行解码
    2. 获取basicTag的值,也就是头4位中后三位的值
    3. 如果basicTag == 7,则认定为Ext类型的TaggedPointer。然后获取value中后52位的值
    4. 如果basicTag != 7,则认定为默认类型的TaggedPointer。然后获取value中的后60位的值

    最后,再来说说TaggedPointer的存值。以NSString为例,实际上TaggedPointer只能存储9个ASCII码的字符。但自己算下来,64个位,减去头4位,再减去4位的长度,实际上只有56位,ASCII码一个字符占1个字节,也就是8位。那么最多也就存7个字符。结果实验证明可以存9个。这就证明在存储时使用了某些压缩方法,使得9个字符可以存在7个字节里。至于是什么算法,不知道。。。原代码里没找到。

    至此,我们基本就介绍完了TaggedPointer类型在runtime中是如何定义,存取值以及它的内存机构是什么样的。打完收工!

    相关文章

      网友评论

          本文标题:ObjC-Runtime TaggedPointer专题

          本文链接:https://www.haomeiwen.com/subject/iublpktx.html