ObjC-Runtime TaggedPointer专题

前言

在之前描述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中是如何定义,存取值以及它的内存机构是什么样的。打完收工!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343