iOS内存管理—Tagged Pointer

作者: 正_文 | 来源:发表于2020-05-20 17:05 被阅读0次

    在介绍Tagged Pointer之前,先简单介绍一下ios的内存布局。

    这里大部分内容是从其他地方整理搬运而来,加上部分自己的理解,站在巨人的肩膀上,让自己能看的更远。

    一、内存布局

    内存布局.png
    整个内存由高到低主要分为五大块:
    内核区:手机内存总共4GB,我们只用到了3GB,剩余1GB给内核区,部分给保留字段。
    栈区:函数,方法 ,是由系统编译器自动管理,不需要程序员手动管理
    堆区:通过alloc、malloc、block copy等生成对象所分配的内存空间,释放工作由程序员手动管理
    BSS段:未初始化的全局变量,静态变量
    数据段:初始化的全局变量,静态变量
    代码段:编译之后的代码
    保留区

    0xc0000000用计算机兑换为10进制,计算结果为3GB。
    不绝对准确的内存首地址:0x6在堆区,0x7在栈区,0x1数据段、BSS内存地址。

    问题1:堆区为什么比栈区慢?堆区,先从栈区找到地址,通过地址找到变量(po objc),最后找到变量指向的堆区空间(po &objc);栈区,直接通过cpu的寄存器查找。

    注意点:

    1. static修饰的静态全局变量只针对文件有效,与类、分类都没有关系。
    2. extern,用于跨文件访问。

    二、Tagged Pointer

    早在在2013年9月,苹果推出了iPhone5s,与此同时,iPhone5s配备了首个采用64位架构的A7双核处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。对于64位程序,引入Tagged Pointer后,相关逻辑能减少一半的内存占用,以及3倍的访问速度提升,100倍的创建、销毁速度提升。

    一般8~10位的小对象,苹果会自动将其转换为Tagged Pointer类型。

    2.1 源码分析

    他的初始化在main方法之前,通过_read_images函数调用initializeTaggedPointerObfuscator实现,在10.14后又做了处理,不在直接展示值:

    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));
            objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
        }
    }
    

    TaggedPointer编码和解码函数:

    static inline uintptr_t
    _objc_decodeTaggedPointer(const void * _Nullable ptr)
    {
        return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    }
    
    static inline bool 
    _objc_taggedPointersEnabled(void)
    {
        extern uintptr_t objc_debug_taggedpointer_mask;
        return (objc_debug_taggedpointer_mask != 0);
    }
    

    decod和dencode都是异或^同一个变量,为什么呢?

    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    

    2.2 指针 + 值

    如果打印Tagged Pointer的指针地址,你会发现是一个奇怪的数字和常规的地址表示不一样。
    因为实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。Tagged Pointer = 指针 +
    所以,它的内存并不存储在堆中,也不需要malloc和free,以及引用计数的处理。
    这在retainrelease方法中有很直观的体现。

    objc_retain(id obj)
    {
        if (!obj) return obj;
        if (obj->isTaggedPointer()) return obj;
        return obj->retain();
    }
    

    前面提到在10.14后苹果又做了处理,不在直接展示值,如果需要直接展示需要做相应的解码处理:

    //引入
    extern uintptr_t objc_debug_taggedpointer_obfuscator;
    uintptr_t _objc_decodeTaggedPointer_(id ptr){
        return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSString *str1 = [NSString stringWithFormat:@"a"];
        NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str1));
        
        NSNumber *number1 = [NSNumber numberWithInt:1];//@1;
        NSLog(@"%@ - %p - %@ - 0x%lx",object_getClass(number1),number1,number1,_objc_decodeTaggedPointer_(number1));
    }
    

    打印结果:

    [55584:651418] 0xa000000000000611
    [55584:651418] __NSCFNumber - 0xf2c99228254ad581 - 1 - 0xb000000000000012
    

    2.3 存储格式 NSNumber & NSString

    - (void)testNumber{
        
        NSNumber *charNumber = [NSNumber numberWithChar:'1'];
        NSNumber *shortNumber = [NSNumber numberWithShort:1];
        NSNumber *intNumber = [NSNumber numberWithInt:1];
        NSNumber *longNumber = [NSNumber numberWithLong:1];
        NSNumber *floatNumber = [NSNumber numberWithFloat:1.0];
        NSNumber *doubleNumber = [NSNumber numberWithDouble:1.0];
        
        NSLog(@"%p - 0x%lx", charNumber, _objc_decodeTaggedPointer_(charNumber));
        NSLog(@"%p - 0x%lx", shortNumber, _objc_decodeTaggedPointer_(shortNumber));
        NSLog(@"%p - 0x%lx", intNumber, _objc_decodeTaggedPointer_(intNumber));
        NSLog(@"%p - 0x%lx", longNumber, _objc_decodeTaggedPointer_(longNumber));
        NSLog(@"%p - 0x%lx", floatNumber, _objc_decodeTaggedPointer_(floatNumber));
        NSLog(@"%p - 0x%lx", doubleNumber, _objc_decodeTaggedPointer_(doubleNumber));
    }
    
    

    打印结果:

    [64055:782611] 0xc0566246b12ba50d - 0xb000000000000310
    [64055:782611] 0xc0566246b12ba60c - 0xb000000000000011
    [64055:782611] 0xc0566246b12ba60f - 0xb000000000000012
    [64055:782611] 0xc0566246b12ba60e - 0xb000000000000013
    [64055:782611] 0xc0566246b12ba609 - 0xb000000000000014
    [64055:782611] 0xc0566246b12ba608 - 0xb000000000000015
    

    将charNumber十六进制结果转换为二进制:


    Tagged Pointer.png

    第64位:最高位, 说明这个指针是一个Tagged Pointer
    第61-63位:是11(十进制是3),也就是OBJC_TAG_NSNumber(查上面的枚举)
    中间56位:就是真正的值了,0011 0001对应ASCII的1
    第1-4位:NSNumber的类型:char是0、short是1、int是2、float是4

    OBJC_MSB_TAGGED_POINTERS:64-bit的mac,tag存储在LSB(Least Significant Bit 最低位)。其它情况比如64位的真机和模拟器,tag存储在MSB(Most Significant Bit 最高位)。

    NSString与NSNumber类似:

    第64位:最高位,说明这个指针是一个Tagged Pointer
    第61-63位:10(十进制是2),也就是OBJC_TAG_NSString
    中间56位:就是真正的值了
    第1-4位:字符串长度

    关于具体的计算方式,我没有找到相关的苹果开源文件,但是从_objc_makeTaggedPointer方法可以简单了解一下,这个方法做了一系列位运算,但是并不是最终数据。

    2.4 面试题

    代码如下:

    for (int i = 0; i<100000; i++) {
            dispatch_async(dispatch_queue_create(0, 0), ^{
                self.nameStr = [NSString stringWithFormat:@"学习taggedpointer,我们来了!"];
                NSLog(@"%@",self.nameStr);
            });
        }
    

    这段代码运行会发生问题,但是如果把代码稍作改动:self.nameStr = [NSString stringWithFormat:@"tagged"];,一切都正常了。
    为什么?因为两个对象不一样,一个是NSTaggedPointerString,一个是NSCFString。

    2.4 总结

    • Tagged Pointer 并非真的对象,没有isa指针,只是看起来和对象一致。
    • Tagged Pointer 存储在栈中,其存取值,均已tag+data形式。
    • Tagged Pointer 不支持retain、release、autorelease、malloc、free等关于对象的内存管理。

    相关文章

      网友评论

        本文标题:iOS内存管理—Tagged Pointer

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