OC-内存管理Tagged Pointer

1. 简介

Tagged Pointer是苹果在64bit设备提出的一种存储小对象的技术,它具有以下特点

  • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。
  • 它的内存并不存储在堆中,也不需要 malloc 和 free,不走引用计数那一套逻辑,由系统来处理释放
  • 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。
  • 可以通过设置环境变量OBJC_DISABLE_TAGGED_POINTERS来有开发者决定是否使用这项技术

2. 源码

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

2.1 各种标记位
#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表示有扩展的标记位,扩展标记位占8位
// array slot includes the tag bit itself
#define _OBJC_TAG_SLOT_COUNT 16
#define _OBJC_TAG_SLOT_MASK 0xf // 0b1111 taggedpointer + 有扩展标记位的mask

#define _OBJC_TAG_EXT_INDEX_MASK 0xff
// array slot has no extra bits
#define _OBJC_TAG_EXT_SLOT_COUNT 256 // 扩展标记位能表示的个数
#define _OBJC_TAG_EXT_SLOT_MASK 0xff // 0b1111 1111

#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63) // 是否是tagged pointer的标记位 1表示是 0表示不是
#   define _OBJC_TAG_INDEX_SHIFT 60 // 基础tag位的偏移,从2-4bit,结合_OBJC_TAG_INDEX_MASK来获取到基础tag的值
#   define _OBJC_TAG_SLOT_SHIFT 60
#   define _OBJC_TAG_PAYLOAD_LSHIFT 4 // LSHIFT和RSHIFT配合使用用来对数据移位混淆及恢复
#   define _OBJC_TAG_PAYLOAD_RSHIFT 4
#   define _OBJC_TAG_EXT_MASK (0xfUL<<60) // 1111 0000 ... 0000 0000 是否有扩展标记位 tag位111表示有扩展标记位
#   define _OBJC_TAG_EXT_INDEX_SHIFT 52 // 扩展tag位的偏移,从5-12bit共8位,结合_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

定义了很多位信息,我们需要关注的几个:

  • _OBJC_TAG_MASK :标记位标记该指针是否是tagged pointer
  • _OBJC_TAG_INDEX_MASK :tag的值是7表示有扩展的tag位
  • 其他的都是一些定义,用来通过位运算来获取tag的值、ext tag的值的mask以及一些其他的左移右移位
2.2 如何判断是tagged pointer

我们知道有一个标记位来标识指针是否是tagged pointer的

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

通过位运算获取标识位的值来确定是否是tagged pointer;需要留意的是不同的架构标记位不太一样,有的是用最低位、有的使用最高位。

2.3 系统对tagged pointer的加密

在iOS12系统之前,我们发现是可以直接打印tagged pointer的值的,可读性非常好,但是12之后再打印就发现完全看不懂了。

- (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字符的值

0xa000000000000791 y NSTaggedPointerString

iOS12之后输出:

0xcb47b8d98a2fa15f y NSTaggedPointerString

iOS12之前打印指针的值能很清晰的看到数据等信息,iOS12之后系统则打印的完全看不懂了,看了源代码发现苹果是做了混淆,让我们不能直接得到值,从而避免我们去很容易就伪造出一个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);
    }
}

第二步,再来一个随机数异或运算一下

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

这个随机数是在dyld加载image的时候去随机产生的,每次程序加载都不一样

// 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,最终没搞成。

混淆之后怎么解密了
正常就是按照上面的混淆的方式,反向操作一波
第一步,异或那个固定的随机数,得到值

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

这里举个例子,就一目了然了;可以看到encode的时候异或一次,在decode的时候再异或一次就得到原始值了。

// 假设 objc_debug_taggedpointer_obfuscator位 0010、原始数据是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对象

系统通过3bit的标记位来标识tagged pointer对象的类,它的定义在objc_tag_index_t
比如2表示是NSString、6表示是NSDate,我们知道3bit能表示的最大值是7,这个7系统用来预留,用来标记是否有额外的标记位,这样就能支持更多的类支持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 字符串编码

看博客发现很多都说字符在长度超过10的时候就不再是NSTaggedPointerString了,其实这种说法是不准确的;

TaggedPointer用来表示值的位数payload是固定的,但是采用不同的字符编码格式就能表示的位数也就不一样了;

下面的测试例子,11位的字符串它也是TaggedPointer

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

输出结果:

0xfa976f93575a8e75 value:0x7bdef7bdef7bdeb 11111111111 NSTaggedPointerString

关于字符串的编码这一块,还是挺复杂的,感兴趣的可以参照这篇博客学习一下系统是如何在创建一个字符串的时候去编码的
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的优化

在intel中tagged pointer指针的数据结构如下:

图片.png

  • 通过最低位来标记是否是tagged pointer
  • 3位tag来标记数据的class
  • 扩展的8位来表示更多的类型的tagged pointer

在arm中tagged pointer指针的数据结构如下:
iOS13系统:

图片.png

为什么要做这样的改变了?

图片.png

主要是针对objc_msgSend的调用的优化,在intel的结构下,判断tagged pointer和nil需要分2个分支去判断;而如果将最高位设置位1了,那么只需要做一次判断就可以判断对象是否是正常指针了。
if (ptrValue <= 0) // is tagged or nil其他情况就是正常的指针

iOS14:
iOS14跟iOS13的区别则将tag放在了最后三位;tagged pointer的标记位还是保持在最高位

图片.png

为什么要这样做了?

苹果的解释是:

  • ARM有个特性就是dyld会忽略指针的前8bit(这是由于ARM的Top Byte Ignore特性);
  • 这样布局数据之后,图中的payload就跟普通指针的payload是一毛一样了,也就是tagged pointer的payload(有效负载位)有包含一个正常的指针的能力;
  • 这使得tagged pointer具备了引用二进制文件中的常量数据的能力,例如字符串或其他数据结构,这减少了dirty memory的使用

看到这里我只能说苹果牛逼666

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