美文网首页
[iOS] 内存管理-Tagged Pointer

[iOS] 内存管理-Tagged Pointer

作者: 沉江小鱼 | 来源:发表于2021-04-10 19:16 被阅读0次

前言:在之前分析 isa结构,还有引用计数的管理时,会遇到 isTaggedPointer的判断,这里就来学习一下Tagged Pointer到底是个啥?

1. Tagged Pointer介绍

1.1 为什么要使用 Tagged Pointer

2013 年,苹果推出了iPhone 5s,配备了首个采用64 位架构A7 双核处理器,指针变量的内存占用由32 位(4字节)变成了 64 位(8字节)OC 对象都是存储在上面的,并且在栈上有一个指针指向了堆的内存地址,对于一些变量,比如@(1)@"abc",如果同时在栈和堆上都取分配内存空间的话,就很浪费了,另外还要维护它的引用计数,管理它的生命周期,这些都给程序增加了额外的逻辑,影响运行效率。

为了改进上面上面所提出的问题,苹果提出了Tagged Pointer对象。由于 NSNumber、NSDate、NSString一类的变量本身的值占用的内存大小多数情况下不需要 8 个字节,就比如整数,4 个字节所能表示的有符号整数就可以达到2147483648,对于绝大多数情况都是可以处理的。

1.2 Tagged Pointer到底是是什么?

所以苹果将一个对象的指针拆成了两部分:本身数据 + 特殊标记,表示这是一个特别的指针,不指向任何一个地址,这个特别的指针就是 Tagged Pointer,如下图所示:

image.jpeg

由于Tagged Pointer 不指向任何一个地址,所以在对象进行retain或者release操作时,都会判断isTaggedPointer,如果为true,则不进行操作,也就是说我们并需要去管理它的生命周期,如下:

__attribute__((aligned(16), flatten, noinline))
id 
objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}


__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}

2. 判断是否为Tagged Pointer

我们运行下面的代码,去看下 Tagged Pointer 指针地址的含义:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    NSNumber *number1 = @1;
    NSLog(@"number1 %p %@", number1, [number1 class]);
    NSNumber *number2 = @2;
    NSLog(@"number2 %p %@", number2, [number2 class]);

    NSString *a = [[@"a" mutableCopy] copy];
    NSLog(@"a %p %@", a, [a class]);
    NSString *ab = [[@"ab" mutableCopy] copy];
    NSLog(@"ab %p %@", ab, [ab class]);
}

如果在之前,输出的结果会如下所示:

number1 0xb000000000000012 __NSCFNumber
number2 0xb000000000000022 __NSCFNumber
a 0xa000000000000611 NSTaggedPointerString
ab 0xa000000000062612 NSTaggedPointerString

去除前后的标识,剩下的中间值就是我们设置的变量的值。

但是实际上,输出如下:

number1 0xe629750fa0a06e3c __NSCFNumber
number2 0xe629750fa0a06e0c __NSCFNumber
a 0xf629750fa0a0683f NSTaggedPointerString
ab 0xf629750fa0a6483c NSTaggedPointerString

我们可以看到字符串的类型仍然为NSTaggedPointerString,但是对象地址看起来跟正常的一样了,这是因为在某个时间结点(不太清楚啥时候,但不重要)之后,对于 TaggedPointer 地址进行了混淆操作,那我们可以通过查看isTaggedPointer方法实现,去判断一个针是否为TaggedPointer,源码如下:

#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

#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL
#endif

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

从上面可以看到判断是否Tagged Pointer,仅仅是将地址和_OBJC_TAG_MASK进行一个与运算,将其结果和_OBJC_TAG_MASK进行比对。_OBJC_TAG_MASK在Mac OS下为1UL,其他平台下为1UL<<63

这个方法其实相当于判断地址最高位是否为 1,为 1 则说明是Tagged Pointer,为啥是判断地址最高位是否为 1?需要去学习一下&与操作了。

我们将上面打印出的第一个字符串的地址:0xf629750fa0a0683f,转成二进制:

image.png

可以看到最高位为 1,说明确实是Tagged Pointer

我们将 NSNumber 对象的地址转成 2 进制之后,首位也是 1,说明也是Tagged Pointer,但是打印出的类型不像字符串那样NSTaggedPointerString直接能看出来是Tagged Pointer,可能是苹果并没有为 NSNumber 类设计一个单独的 class 来表示Tagged Pointer

3. Tagged Pointer类型区分

上面我们知道了如何判断一个对象指针是否为Tagged Pointer,但是NSNumber 、NSString 都有可能是Tagged Pointer,怎么去区分其类型呢?我们需要获取到混淆之前的地址,可以在源码中通过objc_debug_taggedpointer_obfuscator查找TaggedPointer的编码和解码,来查看底层是如何混淆处理的:

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

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

我们看到在编码时是将指针地址和objc_debug_taggedpointer_obfuscator进行了一次异或,objc_debug_taggedpointer_obfuscator的赋值方法,如下:

static void
initializeTaggedPointerObfuscator(void)
{
    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.
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        // #   define _OBJC_TAG_MASK (1UL<<63) 就是判断时 & 的 _OBJC_TAG_MASK
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    }
}

为了获取小对象的真实地址,我们可以将解码的源码拷贝到外面,将混淆之后的地址进行解码,如下所示:


image.jpeg

观察解码后的小对象地址,其中的62表示bASCII码,再以NSNumber为例,同样可以看出,1就是我们实际的值:

image.png

到这里,我们验证了小对象指针地址中确实存储了值,那么小对象地址高位其中的0xa、0xb又是什么含义呢?

//NSString
0xa000000000000621

//NSNumber
0xb000000000000012
0xb000000000000025

0xa转换成二进制为 1 010(64为为1,63~61后三位表示 tagType类型 - 2),表示NSString类型

0xb 转换为二进制为 1 011(64为为1,63~61后三位表示 tagType类型 - 3),表示NSNumber类型,这里需要注意一点,如果NSNumber的值是-1,其地址中的值是用补码表示的。

这里可以通过_objc_makeTaggedPointer方法的参数tag类型objc_tag_index_t进入其枚举,其中 2表示NSString,3表示NSNumber

image.png

4. 总结

  • Tagged Pointer 其实是地址 + 值,并不是真正的对象,只是一个披着对象皮的普通变量。可以直接进行读取,占用空间小,节省内存;
  • Tagged Pointerretainrelease时,都直接返回了,不需要管理其生命周期;
  • Tagged Pointer 并不会存储在堆中,而是在常量区,也不需要malloc和free,所以可以直接读取,相比存储在堆区的数据读取,效率上快了3倍左右,创建的效率相比堆区快了近100倍左右;
  • Tagged Pointer的64位地址中,前4位代表类型,后4位主要适用于系统做一些处理,中间56位用于存储值
  • 对于NSString来说,当字符串较小时,建议直接通过@""初始化,因为存储在常量区,可以直接进行读取,会比WithFormat初始化方式更加快速。

相关文章

网友评论

      本文标题:[iOS] 内存管理-Tagged Pointer

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