内存管理-TaggedPointer

·  阅读 1025
内存管理-TaggedPointer

TaggedPointer 概念

2013 年 9 月苹果推出了首个采用 64 位架构的 A7 双核处理器的手机 iPhone5s,为了改进从 32 位 CPU 迁移到 64 位 CPU 的内存浪费和效率问题,在 64 位 CPU 环境下,苹果工程师提出了 Tagged Pointer 的概念。采用这一机制,系统会对 NSStringNSNumberNSDate 等对象进行优化。建议大家看看 WWDC2020 这个视频的介绍。

image.png

一般我们存储一个对象都是通过指针找到堆区的内存地址,Tagged Pointer 其实也是一个指针,但是与普通的指针相比会有一点特殊,它相当于在 pointer 的基础上加上 tagged 标记,作用就是针对一些小对象 NSStringNSNumberNSDate 等,以 NSString *str = [NSString stringWithFormat:@"cx"] 这句代码为例,当只存储 cx 这两个字符的时候,其实是用不了 64 位的,如果还用 指针->堆区 的存储形式就会造成内存上的浪费。所以这时候苹果工程师提出了用 Tagged Pointer 来存储这些小对象,Tagged Pointer 就相当于指针加上内容,这样的话这些数据就会存在栈区,既节约了内存又大大提升了这些对象的开辟及销毁速度。所以了解 Tagged Pointer 是很有必要的,而且在 Swift 中我们可以自己创建 Tagged Pointer

x86-64 下的 Tagged Pointer 结构

image.png

如图我们通过 stringWithFormat 创建一个字符串,打印可以看到是 NSTaggedPointerString 类型,而且地址既不是 0x6 开头也不是 0x7 开头,可以看得出这是一个小对象类型。

image.png

objc 源码中搜索 TaggedPointer 可以看到这样一段注释,在 WWDC2020 这个视频也介绍了 payload 代表有效负载,是字符串真正存储的地方,这里可以看出 payload 需要经过 decoded_obj 加上一些位运算操作得到,说明是一个加密解密的过程,下面我们来搜索一下 decod

image.png image.png

搜索可以看到 _objc_decodeTaggedPointer_noPermute 函数,这里 ptr 就是传入的地址,最后返回的 value 等于 value ^ objc_debug_taggedpointer_obfuscator,而 objc_debug_taggedpointer_obfuscator 代表混淆,在 initializeTaggedPointerObfuscator 中可以看到,会判断 DisableTaggedPointerObfuscation(是否开启混淆),是的话 objc_debug_taggedpointer_obfuscator 会被赋值一个随机数,不是的话 objc_debug_taggedpointer_obfuscator 就是 0。这说明我们在上面案例中输出的 str 地址是 encode 过的,所以我们要进行一次 decode

image.png

如图可以看出在 encode 之前会根据架构的不同,会进行一些位移操作。

image.png

image.png image.png

如图 $0 中的 0b 代表二进制,1 代表 Tagged Pointer 类型指针,我们将 $0 右移 3 位,然后分别输出最右边的两个字节,然后通过 ASCII 码对照表查看就是 c, x 这两个字符。

image.png image.png

WWDC2020 这个视频也介绍了第二位到第四位是表示类型的,这里我们新增一种 NSNumber 类型,打印可以看到分别是 2 和 3,与源码对照也是正确的。这么我们分析的是 x86-64 下的 Tagged Pointer 的结构,下面我们分析一下 arm64 架构下的结构。

arm64 下的 Tagged Pointer 结构

image.png

如图可以看到如果真机环境下需要多处理一些红色圈中部分,会比较麻烦一点。下面我们用一个比较取巧的方式。

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);
    }
}
复制代码

_objc_makeTaggedPointer 函数中我们可以看到,在 _objc_encodeTaggedPointer 之前需要遵循一个原则,就是 _objc_taggedPointersEnabled

static void
initializeTaggedPointerObfuscator(void)
{
    if (!DisableTaggedPointerObfuscation) {
        // Pull random data into the variable, then shift away all non-payload bits.
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;

#if OBJC_SPLIT_TAGGED_POINTERS
        // The obfuscator doesn't apply to any of the extended tag mask or the no-obfuscation bit.
        objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);

        // Shuffle the first seven entries of the tag permutator.
        int max = 7;
        for (int i = max - 1; i >= 0; i--) {
            int target = arc4random_uniform(i + 1);
            swap(objc_debug_tag60_permutations[i],
                 objc_debug_tag60_permutations[target]);
        }
#endif
    } else {
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        objc_debug_taggedpointer_obfuscator = 0;
    }
}
复制代码

initializeTaggedPointerObfuscator 函数中可以看到是通过 DisableTaggedPointerObfuscation 来对 objc_debug_taggedpointer_obfuscator 赋值,当 objc_debug_taggedpointer_obfuscator 等于 0 的时候就不需要再进行复杂的位运算。

image.png

image.png

所以我们搜索 DisableTaggedPointerObfuscation 可以看到 OBJC_DISABLE_TAG_OBFUSCATION,代表关闭混淆,我们在项目中进行配置就可以关闭混淆。这样我们就不需要再进行一些位运算操作了。

image.png

在真机环境下可以看出有些不同,现在左边第一位还是代表 Tagged Pointer 类型,但是原来是左边第二位到第四位是代表类型的,但是 arm64 下是最右边 3 位代表类型,第 4 位到第七位代表字符串的长度,arm64 是小端模式,从第 8 位开始才是内容的存储位。

image.png

但是 NSNumber 类型,第 4 位到第七位代表的意义就有些变化,是代表 NSNumber 承载的类型的,总结一下就是 0 代表 char,1 代表 short,2 代表 int,3 代表 long,4 代表 float,5 代表 double。大家也可以自己验证下。

Tagged Pointer 相关问题

image.png

类似这样一个案例,taggedPointerDemo 方法执行没有问题,当 touchesBegan 执行的时候就会崩溃,这是因为 taggedPointerDemoself.nameStrNSTaggedPointerString 类型,而 touchesBeganself.nameStr__NSCFString 类型。所以 touchesBegan 就涉及到多线程的写和读,当对 self.nameStr 赋值时就会涉及到对旧值的 release 和对新值的 retian。这就可能涉及到对一个已经 release 过的内存进行访问,就会出现坏地址的访问,所以会崩溃。而 touchesBeganself.nameStr__NSCFString 类型是因为字符串的长度超过了有效负载位能承载的最大长度,所以就需要开辟堆空间来存储。

image.png image.png

通过源码也可以看到,当是 taggedPointer 类型时 rootRetainrootRelease 函数不进行任何操作,所以不涉及堆内存的释放与回收以及一些下层方法的处理,所以 taggedPointer 类型速度会很快,效率会更高。

分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改