美文网首页iOS 底层探索
三、Tagged Pointer对象

三、Tagged Pointer对象

作者: 那样风采 | 来源:发表于2018-09-29 17:14 被阅读8次

    1、原有系统的问题

    假设我们要存储一个NSNumber对象,其值是一个整数,正常情况下,如果这个整数只是一个NSInteger的普通变量,那么它所占用的内存与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的,而指针类型的大小通常通常也与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节。
    所以,一个普通的iOS程序,如果没有Tagged Pointer(标记指针)对象,从32位机器迁移到64位机器中后,虽然逻辑上没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。

    再看看效率,为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命周期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。 image.png

    2、Tagged Pointer介绍

    由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31 = 2147483648,另外一位作为符号位),对于绝大多数情况都是可以处理的!

    所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了Tagged Pointer对象之后,64位CPU下NSNumber的内存图变成了下面这样: image.png 对此我们可以用代码进行验证:
            NSNumber *number1 = @1;
            NSNumber *number2 = @2;
            NSNumber *number3 = @3;
            NSNumber *numberFFFF = @(0xFFFF);
            
            NSLog(@"number1 pointer is %p", number1);
            NSLog(@"number2 pointer is %p", number2);
            NSLog(@"number3 pointer is %p", number3);
            NSLog(@"numberFFFF pointer is %p", numberFFFF);
    
    image.png

    我们将NSNumber类型的指针在64位CPU下直接输出,除去末尾的2和开头的0xb其他的数字刚好表示响应NSNumber的值。猜测:末尾的2和最开头0xb就是Tagged Pointer的特殊标记!?
    我们继续验证,尝试方一个8字节长的整数到NSNumber实例中,这样的实例,Tagged Pointer无法将其按上面的压缩方式来保存:

            NSNumber *bigNumber = @(0xFFFFFFFFFFFFFFFF);
            NSLog(@"bigNumber pointer is %p", bigNumber);
    

    打印结果:bigNumber pointer is 0x600000029520

    Tagged Pointer特点:

    1. Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate。
    2. Tagged Pointer指针的值不再是地址,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象"皮"的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free!
    3. 在内存读取上有着以前3倍的效率,创建时比以前快106倍!

    结论:

    • 当8个字节可以承载用于表示的数值时,系统会以Tagged Pointer的方式生成指针,如果8个字节承载不了时,则又用以前的方式生产普通的指针!!
    • 引入Tagged Pointer,不但减少了64位机器下程序的内存占用,还提高了运行效率,完美地解决了小内存对象在存储和访问效率上的问题!!

    3、注意事项和实现细节

    3.1 isa指针

    Tagged Pointer的引入也带来了问题,即Tagged Pointer并不是真正的对象,而是一个伪对象,所以你如果完全把它当做对象来使用,可能会出问题,比如:所有对象都有isa指针,而Tagged Pointer其实是没有的,因为它不是真正的对象,以为不是真正的对象,所以你如果直接访问Tagged Pointer的isa成员的话,在编译时会有警告:

    obj->isa
    

    我们应该尽量避免上述写法,应该换成相应的方法调用,如isKindOfClass和object_getClass。只要避免在代码中直接访问对象的isa就可以了!

    3.2 引用计数

    对于64位设备,苹果除了引入Tagged Pointer来优化小的对象外,对于普通的对象,其isa指针也进行了优化和调整!
    在32位环境下,对象的引用计数都保存在一个外部的表中,每一个对象的Retain操作,实际上包括如下5个步骤:

    1. 获得全局的记录引用计数的hash表
    2. 为了线程安全,给该hash表枷锁
    3. 查找到目标对象的引用计数值
    4. 将该引用计数值加1,写回hash表
    5. 给该hash表解锁

    从上面步骤来看,为了保证线程安全,对引用计数的增减操作都要先锁定这个表,这从性能上看是非常差的!
    而在64位环境下,isa指针也是64位的,实际作为指针部分只用到了33位,剩余31位苹果使用了类似Tagged Pointer的概念,其中19位将保存对象的引用计数,这样对引用计数的操作只需要修改这个指针即可。只有当引用计数超出19位,才会将引用计数保存到外部表,而这种情况是很少,所以这样引用计数的更改销量会更高!
    在64位环境下,新的retain操作包括如下5个步骤:

    1. 检查isa指针上面的标记位,看引用计数是否保存在isa变量中,如果不是,则使用以前的步骤,否则执行第2步
    2. 检查当前对象是否正在释放,如果是,则不做任何事情
    3. 增加该对象的引用计数,但是并不是马上写回到isa变量中
    4. 检查增加后的引用计数的值是否能够被19位表示,如果不是,则切换成以前的办法,否则执行第5步
    5. 进行一个原子的写操作,将isa的值写回

    由于没有了全局的加锁操作,所以引用计数的更改更快了!

    3.3 isa的bit位

    image.png
    union isa_t 
    {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
        Class cls;
        uintptr_t bits;
    #if SUPPORT_NONPOINTER_ISA
    # if __arm64__
    #   define ISA_MASK        0x00000001fffffff8ULL
    #   define ISA_MAGIC_MASK  0x000003fe00000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a400000001ULL
        struct {
            uintptr_t indexed           : 1;
            uintptr_t has_assoc         : 1;
            uintptr_t has_cxx_dtor      : 1;
            uintptr_t shiftcls          : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000
            uintptr_t magic             : 9;
            uintptr_t weakly_referenced : 1;
            uintptr_t deallocating      : 1;
            uintptr_t has_sidetable_rc  : 1;
            uintptr_t extra_rc          : 19;
    #       define RC_ONE   (1ULL<<45)
    #       define RC_HALF  (1ULL<<18)
        };
    # elif __x86_64__
    #   define ISA_MASK        0x00007ffffffffff8ULL
    #   define ISA_MAGIC_MASK  0x0000000000000001ULL
    #   define ISA_MAGIC_VALUE 0x0000000000000001ULL
        struct {
            uintptr_t indexed           : 1;
            uintptr_t has_assoc         : 1;
            uintptr_t has_cxx_dtor      : 1;
            uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
            uintptr_t weakly_referenced : 1;
            uintptr_t deallocating      : 1;
            uintptr_t has_sidetable_rc  : 1;
            uintptr_t extra_rc          : 14;
    #       define RC_ONE   (1ULL<<50)
    #       define RC_HALF  (1ULL<<13)
        };
    # else
        // Available bits in isa field are architecture-specific.
    #   error unknown architecture
    # endif
    // SUPPORT_NONPOINTER_ISA
    #endif
    };
    

    SUPPORT_NONPOINTER_ISA 用于标记是否支持优化的 isa 指针,其字面含义意思是 isa 的内容不再是类的指针了,而是包含了更多信息,比如引用计数,析构状态,被其他 weak 变量引用情况。判断方法也是根据设备类型:

    #if !__LP64__  ||  TARGET_OS_WIN32  ||  TARGET_IPHONE_SIMULATOR  ||  __x86_64__
    #   define SUPPORT_NONPOINTER_ISA 0
    #else
    #   define SUPPORT_NONPOINTER_ISA 1
    #endif
    

    我们可以看到,模拟器也是不支持Tagged Pointer的!
    参考:isa 指针 和 IMP 指针

    相关文章

      网友评论

        本文标题:三、Tagged Pointer对象

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