iOS 内存管理(二):tagged pointer原理分析

1,492 阅读4分钟

准备

一、什么是 tagged pointer

tagged pointer 引入

2013年苹果推出iphone5s之后,iOS的寻址空间扩大到了64位。我们可以用63位来表示一个数字(一位做符号位)。那么这个数字的范围是2^63,很明显我们一般不会用到这么大的数字,那么在我们定义一个数字时NSNumber *num = @10,实际上内存中浪费了很多的内存空间。

为了节省内存和提高执行效率,于是就引入了tagged pointer,tagged pointer是一种特殊的”指针“,其特殊在于,其实它存储的并不是地址,而是真实的数据和一些附加的信息。

tagged pointer 特点

我们可以在 WWDC 2020 的视频中,看到苹果对于tagged pointer特点的介绍,tagged pointer部分从14:52左右开始。

[WWDC 2013]的视频中有这样的一段介绍:

image.png

  • tagged pointer专门用来存储小的对象,例如NSNumberNSDateNSString
  • tagged pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要mallocfree
  • 在内存读取上有着3倍的效率,创建时比以前快106倍。

二、tagged pointer 的数据混淆

先看下下面代码的执行:

- (void)test {
    NSString *str = [NSString stringWithFormat:@"L"];
    NSLog(@"%p - %@ - %@",str,str,str.class);
}

执行结果:
0xd2cb3e3937e0be52 - L - NSTaggedPointerString
  • 字符串的地址是0xd2cb3e3937e0be52,通过NSTaggedPointerString我们可以知道他是一个tagged pointer,这个地址看上去就很奇怪并不是我们平时看到的样子,这是因为它做了数据混淆。

数据混淆 原理

打开 objc4-818.2 源码,可以找到下面的代码:

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
    ...
    return value;
}

static inline uintptr_t
_objc_decodeTaggedPointer_noPermute(const void * _Nullable ptr)
{
    uintptr_t value = (uintptr_t)ptr;
    ...
    // 混淆
    return value ^ objc_debug_taggedpointer_obfuscator;
}
  • 可以看到混淆算法,是地址objc_debug_taggedpointer_obfuscator进行异或操作。

查找objc_debug_taggedpointer_obfuscator初始化的地方:

/***********************************************************************
* initializeTaggedPointerObfuscator
* Initialize objc_debug_taggedpointer_obfuscator with randomness.
**********************************************************************/
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;
        ...
    } else {
        objc_debug_taggedpointer_obfuscator = 0;
    }
}
  • initializeTaggedPointerObfuscator()这个初始化函数,是在_read_images中调用的。
  • 是否开启混淆通过DisableTaggedPointerObfuscation来控制,如果开启了混淆会给objc_debug_taggedpointer_obfuscator赋值一个随机数。
  • 如果没有开启混淆会将objc_debug_taggedpointer_obfuscator赋值为0,下面将通过两种方式来解决数据混淆。

解密函数 解决数据混淆

extern uintptr_t objc_debug_taggedpointer_obfuscator;

- (void)test {
    NSString *str = [NSString stringWithFormat:@"L"];
    NSLog(@"%p - %@ - %@ - 0x%lx",str,str,str.class,ssl_objc_decodeTaggedPointer(str));
}

uintptr_t
ssl_objc_decodeTaggedPointer(id ptr)
{
    // 再次异或 解密
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

执行结果:
0xb9baff764fa37ce8 - L - NSTaggedPointerString - 0xa0000000000004c1
  • 通过解密函数得到地址0xa0000000000004c1,是我们想要的结果。

环境配置 关闭数据混淆

通过设置环境变量OBJC_DISABLE_TAG_OBFUSCATIONYES,关闭数据混淆:

image.png

再次执行程序:

0xa0000000000004c1 - L - NSTaggedPointerString - 0xa0000000000004c1
  • 现在直接打印str的地址和解密后的地址是一样的,都是0xa0000000000004c1

三、x86-64下的tagged pointer结构

创建一个MacOS的工程,执行下面代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"a"];
        NSNumber *num1 = @3;
        NSNumber *num2 = @(0xFFFFFFFFFFFFFFFF);
        NSLog(@"%p- %p - %p",str,num1,num2);
    }
    return 0;
}

执行结果:
0x6115- 0x327 - 0x100529c70
  • 从执行结果可以看出,strnum1tagged pointer类型,值存储在了指针中,对应0x61150x327num2并不是tagged pointer类型。
  • 0x6115的二进制表示为110000100010101
  • 0x327的二进制表示为1100100111

3.1 MacOS下 tagged pointer标志位

下面的源码是判断是否为tagged pointer类型:

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

这里用到了_OBJC_TAG_MASK掩码,来看下它的定义:

#if __arm64__
#   define OBJC_SPLIT_TAGGED_POINTERS 1
#else
#   define OBJC_SPLIT_TAGGED_POINTERS 0
#endif

#if OBJC_SPLIT_TAGGED_POINTERS   // arm64时为 1
#   define _OBJC_TAG_MASK (1UL<<63)
#else 
#   define _OBJC_TAG_MASK 1UL
  • x86-64环境下_OBJC_TAG_MASK的值为1ptr & 1还是等于1,也就是说指针地址最低位是1的时候,代表这个指针是tagged pointer类型。
  • 前面的num1str二进制表示最低位都为1,是tagged pointer类型在这里得到验证。

3.2 MacOS下 类标志位

objc4源码中查看NSNumberNSString等类的标志位:

// objc-internal.h
enum
{
    // 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,
    ...
};
  • 前面的num1二进制是1100100111,第1-3位是类标志位,值是011,十进制是3NSNumber类型,跟枚举值匹配。
  • 前面的str二进制是1100001000101011-3位是类标志位,值是010,十进制是2NSString类型,跟枚举值匹配。

3.3 MacOS下 数据类型&字符串长度

如果是NSNumber类型,tagged pointer4-7位对应数据类型,对应关系如下:

Tagged Pointer 4-7位对应数据类型
0char
1short
2int
3long
4float
5double
  • 前面的num1二进制是11001001114-7对应的是00100010的十进制是2,对应这里的int类型。

如果是NSString类型,tagged pointer4-7位对应字符串长度

  • 前面的str二进制是1100001000101014-7位对应的是00010001的十进制是1,代表字符串长度为1

3.4 MacOS下 数据存储

tagged pointer8-63位用来存储数据:

  • 前面的num1二进制是11001001118-63对应的是1111的十进制是3,符合这里的说法。
  • 前面的str二进制是1100001000101018-63对应的是11000011100001的十进制是97,符合这里的说法,ASCII码表: src=http___img2020.cnblogs.com_blog_1299297_202005_1299297-20200503124701257-1589775940.png&refer=http___img2020.cnblogs.jpeg

3.5 MacOS下 tagged pointer结构图

image.png

四、arm64下的tagged pointer结构

在真机环境下,执行下面代码:

- (void)viewDidLoad {
    [super viewDidLoad];
        
    NSString *str = [NSString stringWithFormat:@"a"];
    NSNumber *num1 = @3;
    NSLog(@"%p- %p - %p",str,num1,num2);
}

执行结果:
0x800000000000308a - 0x8000000000000193
  • str地址0x800000000000308a的二进制表示为:
    1000000000000000000000000000000000000000000000000011000010001010
    
  • num1地址0x8000000000000193的二进制表示为:
    1000000000000000000000000000000000000000000000000000000110010011
    

4.1 真机下 tagged pointer标志位

是否为tagged pointer源码查看:

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

#if OBJC_SPLIT_TAGGED_POINTERS   // arm64时
#   define _OBJC_TAG_MASK (1UL<<63)
#else 
#   define _OBJC_TAG_MASK 1UL
#endif
  • arm64环境下_OBJC_TAG_MASK的值为 (1UL<<63)ptr & (1UL<<63)还是等于(1UL<<63),也就是说指针地址第63位是1的时候,代表这个指针是tagged pointer类型。
  • 通过上面的打印结果,可以看到第63位确实为1

4.2 真机下 类标志位

  • 通过上面的打印结果,可以看到真机下的类标志位放在了地址的前三位,即0-2位。
  • str的前三位是010,十进制表示是1,代表NSString类。
  • num1的前三位是011,十进制表示是3,代表NSNumber类。

4.3 真机下 数据类型&字符串长度

  • 通过上面的打印结果,可以看到真机下的数据类型&字符串长度放在了地址的3-6位。
  • str3-6位是0001,十进制是1,代表字符串长度为1
  • num13-6位是0010,十进制是2,代表存储的是int类型。

4.4 真机下 数据存储

tagged pointer762位用来存储数据:

  • str7-62位是1100001,十进制是97,也就是字符a
  • num17-62位是11,十进制是3,就是int类型的3

4.5 真机下 tagged pointer结构图

image.png

五、taggedPointer 面试题

1)执行下面两段代码,有什么区别?

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
    dispatch_async(queue, ^{
        self.name = [NSString stringWithFormat:@"abcdefghij"];
    });
}
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
    dispatch_async(queue, ^{
        self.name = [NSString stringWithFormat:@"abcdefghi"];
    });
}

两段代码基本就是一样的,区别只是第二段代码的字符串少了一位而已,这样运行能有什么区别吗?

运行代码,第二段代码正常运行没有问题,第一段代码竟然Crash了。
分别打印两段代码的self.name类型,第一段代码中self.name__NSCFString类型,而第二段代码中为NSTaggedPointerString类型。

来看一下第一段代码Crash的地方:

image.png

  • 看到这里我们就明白了,__NSCFString存储在堆上,它是个正常对象,需要维护引用计数的。self.name通过setter方法为其赋值,而setter方法的实现如下:
    - (void)setName:(NSString *)name {
        if(_name != name) {
            [_name release];
            _name = [name retain]; // or [name copy]
        }
    }
    
  • 我们异步并发执行setter方法,可能就会有多条线程同时执行[_name release],连续release两次就会造成对象的过度释放,导致Crash
  • 我们可以使用加锁操作解决这个问题,使用atomic属性也能解决这个问题。

第二段代码中的NSStringNSTaggedPointerString类型,在objc_release函数中会判断指针是不是TaggedPointer类型,是的话就不对对象进行release操作,也就避免了因过度释放对象而导致的Crash,因为根本就没执行释放操作。

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