macOS 与 iOS 中的 Tagged Pointer

Tagged Pointer 的简介

  • 程序从 32 位 cpu 迁移到 64 位 cpu 所造成的内存空间浪费

    假设要存储一个 NSNumber 对象,其值是一个整数。如果这个整数只是一个 NSInteger 类型的普通变量,那么它所占用的内存大小与 cpu 的位数有关:在 32 位 cpu 下占 4 个字节,在 64 位 cpu 下占 8 个字节。并且,指针类型的变量所占用的内存大小也与 cpu 的位数有关,一个指针类型的变量:在 32 位 cpu 下占 4 个字节,在 64 位 cpu 下占 8 个字节

    // NSObjCRuntime.h
    #if __LP64__ || 0 || NS_BUILD_32_LIKE_64
    typedef long NSInteger;
    typedef unsigned long NSUInteger;
    #else
    typedef int NSInteger;
    typedef unsigned int NSUInteger;
    #endif
    

    所以一个普通的 iOS 程序,在没有使用 Tagged Pointer 的情况下,从 32 位 cpu 的机器迁移到 64 位 cup 的机器后,虽然代码没有任何改变,但是 NSNumberNSDate 这一类对象所占用的内存会翻倍。如下图所示:
    tagged_pointer_before

    并且采用这种方式表示一个只存储数值的 NSNumber 对象,在效率上也会有问题:为了存储和访问一个 NSNumber 对象,程序需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失

  • Tagged Pointer 技术的引入

    为了改进上面提到的内存空间占用和程序运行效率的问题,苹果推出了 Tagged Pointer 技术。由于 NSNumberNSDateNSString 这一类的变量本身的值需要占用的内存大小常常不需要 8 个字节,拿整数来说,4 个字节所能表示的有符号整数就可以达到 20 多亿(注:2^31=2147483648,另外 1 位作为符号位),这对于绝大多数情况都是够用的

    所以可以将一个对象的指针拆成两部分:
    一部分直接保存数据
    另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个内存地址

    引入 Tagged Pointer 技术之后,64 位 cpu 下 NSNumber 的内存图变成了以下这样:
    tagged_pointer_after
    注意:
    如果 8 个字节可以承载用于表示的数值时,则系统就会用 Tagged Pointer 的方式生成指针
    如果 8 个字节承载不了用于表示的数值时,则系统就会用以前的方式来生成普通的指针

  • Tagged Pointer 技术的特点

    以下是苹果在 WWDC2013 《Session 404 Advances in Objective-C》 中对 Tagged Pointer 的介绍(Tagged Pointer 部分从 36:50 左右开始):
    tagged_pointer_innovation
    ① Tagged Pointer 专门用来存储值类型的小内存对象,例如:NSNumberNSDateNSString
    ② 因为 Tagged Pointer 指针的值不再是内存地址,而是真正的数据。所以,Tagged Pointer 类型的小内存对象实际上不是一个真正对象,它只是一个披着对象外衣的普通变量而已。所以,Tagged Pointer 类型的小内存对象的内存并不存储在堆中,也不需要 mallocfree
    ③ Tagged Pointer 类型的小内存对象在内存读取上有着 3 倍的速度提升,在创建和销毁上有着 106 倍的速度提升

    由此可见,苹果引入 Tagged Pointer 技术后,不但减少了 64 位 cpu 下程序的内存占用,而且提高了 64 位 cpu 下程序的运行效率。完美地解决了值类型的小内存对象在存储和访问效率上的问题

  • 示例:NSNumber 对象在未使用 Tagged Pointer 时和使用 Tagged Pointer 时的内存占用

    NSNumber 对象在未使用 Tagged Pointer 时的内存占用

    /*
    因为 Tagged Pointer 无法禁用,所以以下将 NSInteger 类型的变量 num 设成一个很大的数,以让 NSNumber 类型的 number 对象存储在堆上
    
    @note:
    虽然可以在 Target - Run - Arguments - Environment Variables 中添加环境变量 OBJC_DISABLE_TAGGED_POINTERS  = YES 来禁用 Tagged Pointer 技术
    但是如果这么做了,则程序一运行就会 Crash - objc39337: tagged pointers are disabled (lldb)
    因为 RunTime 在程序运行时会判断 Tagged Pointer 是否被禁用,如果 Tagged Pointer 被禁用的话,则会调用 _objc_fatal() 函数杀死当前进程
    所以,虽然苹果提供了 OBJC_DISABLE_TAGGED_POINTERS 这个环境变量给开发者,但是 Tagged Pointer 还是无法被禁用
    */
    #import <malloc/malloc.h>
    -(void)unusedTaggedPointer {
        NSInteger num = 0xFFFFFFFFFFFFFF;
        NSNumber* number = [NSNumber numberWithInteger:num];
        NSLog(@"%zd", sizeof(number));                                  // 8
        NSLog(@"%zd", malloc_size((__bridge const void *)(number)));    // 32
        // 因为 NSNumber 继承自 NSObject,所以 number 对象有 isa 指针,再加上内存对齐的处理,系统给 number 对象在堆中分配了 32 个字节的内存
        // 通过 lldb 指令读取 number 对象在堆中的内存,可以看到,实际上 number 对象并没有用完这 32 个字节的内存
    	// (lldb) p number
    	// (__NSCFNumber *) $0 = 0x00006000032ec020 (long)72057594037927935
    	// (lldb) memory read 0x00006000032ec020
    	// 0x6000032ec020: 40 14 d7 86 ff 7f 00 00 83 16 00 00 01 00 00 00  @...............
    	// 0x6000032ec030: ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00 00  ................
    }
    

    在 64 位的 cpu 下,如果没有使用 Tagged Pointer 技术的话,则为了使用一个 NSNumber 对象就需要在栈中开辟 8 个字节的指针内存和在堆中开辟 32 个字节的对象内存。而如果直接使用一个 NSInteger 类型的普通变量的话,则只需要在栈中开辟 8 个字节的内存。(使用 NSNumber 类型的实例对象的内存开销)比(使用 NSInteger 类型的普通变量的内存开销)多了 4 倍!!!

    NSNumber 对象在使用 Tagged Pointer 时的内存占用

    -(void)usedTaggedPointer {
        NSInteger num = 0x0;
        NSNumber* number = [NSNumber numberWithInteger:num];
        NSLog(@"%zd", sizeof(number));                                  // 8
        NSLog(@"%zd", malloc_size((__bridge const void *)(number)));    // 0
    }
    

    在 64 位的 cpu 下,如果使用 Tagged Pointer 技术的话,则 NSNumber 对象的值直接存储在了指针中,系统不会为其在堆上分配内存,可以节省很多内存开销。此时,NSNumber 对象的指针中存储的数据变成了 Tag + Data 的形式(Tag 为特殊标记,用于区分NSNumberNSDateNSString 等小内存对象的类型;Data 为具体的值)。这样使用一个 NSNumber 对象只需要在栈中开辟 8 个字节的指针内存。当栈中 8 个字节的指针内存不够存储数据时,才会再将 NSNumber 对象存储到堆中

解除 Tagged Pointer 的数据混淆

macOS 与 iOS 为了保证数据安全,对 Tagged Pointer 类型的指针做了数据混淆,开发者无法通过打印指针的内容来判断一个指针是否为 Tagged Pointer 类型,更无法读取存储在 Tagged Pointer 类型的指针中的数据

为了方便我们在分析 Tagged Pointer 的原理时调试程序,需要先解除系统对 Tagged Pointer 的数据混淆。这里提供 2 种方式:

  • ① 通过设置环境变量

    系统为开发者提供了环境变量 OBJC_DISABLE_TAG_OBFUSCATION 来控制 Tagged Pointer 数据混淆的禁用和启用。默认情况下,Tagged Pointer 的数据混淆处于启用状态。可以通过在 Target - Run - Arguments - Environment Variables 添加环境变量 OBJC_DISABLE_TAG_OBFUSCATION = YES,来禁用系统对 Tagged Pointer 的数据混淆

  • ② 通过还原 RunTime 的混淆函数

    Tagged Pointer 的混淆因子是由 RunTime 动态生成的:

    // path: objc4-756.2/runtime/objc-runtime-new.mm
    /*
    使用随机数初始化 Tagged Pointer 的混淆因子 objc_debug_taggedpointer_obfuscator
    
    Tagged Pointer 的混淆因子 objc_debug_taggedpointer_obfuscator 的主要作用是
    使攻击者在发现(缓冲区溢出漏洞)或者(内存写入控制漏洞)时,更难将特定对象构造成 Tagged Pointer 类型的指针
    
    在设置或者获取 Tagged Pointer 所存储的值时
    将使用混淆因子 objc_debug_taggedpointer_obfuscator 与 Tagged Pointer 进行异或运算(XOR)
    
    所有的 Tagged Pointer 在第一次使用时就会被随机化
    */
    static void initializeTaggedPointerObfuscator(void)
    {
        if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
            DisableTaggedPointerObfuscation) {
            // 如果系统版本较老 or 显式设置了环境变量 OBJC_DISABLE_TAG_OBFUSCATION = YES
            // 则将混淆因子 objc_debug_taggedpointer_obfuscator 设置为 0,即不启用 Tagged Pointer 的数据混淆
            objc_debug_taggedpointer_obfuscator = 0;
        } else {
            // 1.将随机数存入混淆因子 objc_debug_taggedpointer_obfuscator 中
            // 2.将混淆因子 objc_debug_taggedpointer_obfuscator 与取反后的 Tagged Pointer 掩码做 & 运算,并将结果重新存入混淆因子 objc_debug_taggedpointer_obfuscator 中
            arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                           sizeof(objc_debug_taggedpointer_obfuscator));
            objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
        }
    }
    

    并且在 RunTime 中定义了编码和解码 Tagged Pointer 的函数:

    // path: objc4-756.2/runtime/objc-internal.h
    
    // 混淆因子 objc_debug_taggedpointer_obfuscator 是个全局变量
    extern uintptr_t objc_debug_taggedpointer_obfuscator;
    
    // 编码 Tagged Pointer 类型的指针
    static inline void * _Nonnull _objc_encodeTaggedPointer(uintptr_t ptr)
    {
        return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
    }
    
    // 解码 Tagged Pointer 类型的指针
    static inline uintptr_t _objc_decodeTaggedPointer(const void * _Nullable ptr)
    {
        return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    }
    

    虽然我们无法直接使用 RunTime 定义在 objc-internal.h 中的函数
    但是我们知道了混淆因子 objc_debug_taggedpointer_obfuscator 是个全局变量
    并且知道了 RunTime 编码和解码 Tagged Pointer 的过程
    因此,我们可以依葫芦画瓢,实现自定义的编码和解码 Tagged Pointer 的函数:

    // 声明全局变量:混淆因子 objc_debug_taggedpointer_obfuscator
    extern uintptr_t objc_debug_taggedpointer_obfuscator;
    
    // 自定义函数:编码 Tagged Pointer 类型的指针
    static inline void * _Nonnull _hcg_objc_encodeTaggedPointer(uintptr_t ptr)
    {
        return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
    }
    
    // 自定义函数:解码 Tagged Pointer 类型的指针
    static inline uintptr_t _hcg_objc_decodeTaggedPointer(const void * _Nullable ptr)
    {
        return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    }
    
    // macOS Command-Line App
    void decode_Tagged_Pointer() {
        NSNumber* num1 = @1;
        NSNumber* num2 = @2;
        NSNumber* num3 = @3;
        NSLog(@"num1.before = %p, num1.after = 0x%lx", num1, _hcg_objc_decodeTaggedPointer((__bridge const void * _Nullable)(num1)));
        NSLog(@"num2.before = %p, num2.after = 0x%lx", num2, _hcg_objc_decodeTaggedPointer((__bridge const void * _Nullable)(num2)));
        NSLog(@"num3.before = %p, num3.after = 0x%lx", num3, _hcg_objc_decodeTaggedPointer((__bridge const void * _Nullable)(num3)));
        // num1.before = 0xc806440c60b4a8df, num1.after = 0x127
        // num2.before = 0xc806440c60b4abdf, num2.after = 0x227
        // num3.before = 0xc806440c60b4aadf, num3.after = 0x327
    }
    

Tagged Pointer 的原理:macOS

  • NSNumber 类型的 Tagged Pointer

    下图是 macOS 中 NSNumber 的 Tagged Pointer 位视图:
    tagged_pointer_macos_NSNumber
    由上图可知,在 macOS 中 NSNumber 的 Tagged Pointer 指针中:

    ① 第 0 位是 Tagged Pointer 的标识位。其值为 1 表示该指针是 Tagged Pointer,其值为 0 表示该指针不是 Tagged Pointer

    ② 第 1 ~ 3 位是类标识位。用于表示该指针所属的类对象,是 Tagged Pointer 对象实质上的 isa。类标识位所对应的类对象如下所示:

    // path: objc4-756.2/runtime/objc-internal.h
    // Tagged pointer 在不同操作系统版本上的布局和使用情况可能会有所变化
    // 默认索引值(0 ~ 7)拥有 60 位的有效存储空间,其中索引值 7 为保留字段
    // 扩展索引值(8 ~ 264)拥有 52 位的有效存储空间,其中索引值 264 为保留字段
    enum objc_tag_index_t : uint16_t
    {
        // 60-bit payloads(拥有 60 位的有效存储空间)
        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,
    	
    	// 60-bit reserved(索引值 7 为保留字段)
    	OBJC_TAG_RESERVED_7        = 7, 
    	...
    };
    

    ③ 第 4 ~ 7 位是数据类型标识位。当类标识位 == OBJC_TAG_NSNumber 时,用于表示 NSNumber 中保存的数据类型:

    数据类型标识位NSNumber 中保存的数据类型
    0char
    1short
    2int
    3long
    4float
    5double

    ④ 第 8 ~ 63 位是存储数据位。用于存储 NSNumber 的真实数据

    结合第 ① 点和第 ② 点可知:
    对于 NSNumber 类型的 Tagged Pointer,它的低 4 位(第 0 ~ 3 位)恒定为 0x7(0111)
    可以将这 4 位理解为 NSNumber 类型的 Tagged Pointer 的标识


    代码验证:

    void macOS_tagged_pointer_NSNumber_demo() {
        NSNumber* num1 = @1;
        NSNumber* num2 = @2;
        NSNumber* num3 = @3;
        NSNumber* num4 = @(0xFFFFFFFFFFFFFFFF);
        NSLog(@"num1 = %p", num1);  // num1 = 0x127
        NSLog(@"num2 = %p", num2);  // num2 = 0x227
        NSLog(@"num3 = %p", num3);  // num3 = 0x327
        NSLog(@"num4 = %p", num4);  // num4 = 0x1004bbe30
        // 从以上打印结果可以看出:
        
        // num1 ~ num3 的指针为 Tagged Pointer 类型,对象的值都直接存储在了指针当中,对应 0x1、0x2、0x3
        // num4 因为数据过大,Tagged Pointer 的存储数据位不够存储,所以内存分配在堆中
        
        // 所有 NSNumber 类型的 Tagged Pointer 都以 0x7 结尾
        // 0x7 对应的二进制为 0111
        // 最后一位的  1 是 Tagged Pointer 标识位,代表这个指针是 Tagged Pointer 类型
        // 前面三位的 011 是类标识位,转换成十进制等于 3,对应 enum objc_tag_index_t 的 OBJC_TAG_NSNumber,表示 NSNumber 类
        
        // 0x127 0x227 0x327 中的 0x2 代表的是什么呢?
        // 0x2 是数据类型标识位,用于表示 NSNumber 中存储的数据的类型
        // 0x0 - char、0x1 - short、0x2 - int、0x3 - long、0x4 - float、0x5 - double
        char    aChar   = 'a';
        short   aShort  = 1;
        int     anInt   = 1;
        long    aLong   = 1;
        float    aFloat  = 1.0f;
        double  aDouble = 1.0;
        NSNumber* numChar   = @(aChar);
        NSNumber* numShort  = @(aShort);
        NSNumber* numInt    = @(anInt);
        NSNumber* numLong   = @(aLong);
        NSNumber* numFloat  = @(aFloat);
        NSNumber* numDouble = @(aDouble);
        NSLog(@"numChar   = %p", numChar);      // numChar   = 0x6107
        NSLog(@"numShort  = %p", numShort);     // numShort  = 0x117
        NSLog(@"numInt    = %p", numInt);       // numInt    = 0x127
        NSLog(@"numLong   = %p", numLong);      // numLong   = 0x137
        NSLog(@"numFloat  = %p", numFloat);     // numFloat  = 0x147
        NSLog(@"numDouble = %p", numDouble);    // numDouble = 0x157
    }
    
  • NSString 类型的 Tagged Pointer

    下图是 macOS 中 NSString 的 Tagged Pointer 位视图:
    tagged_pointer_macos_NSNSString
    ① 第 0 位是 Tagged Pointer 的标识位。其值为 1 表示该指针是 Tagged Pointer,其值为 0 表示该指针不是 Tagged Pointer

    ② 第 1 ~ 3 位是类标识位。用于表示该指针所属的类对象,是 Tagged Pointer 对象实质上的 isa。类标识位所对应的类对象如下所示:

    // path: objc4-756.2/runtime/objc-internal.h
    // Tagged pointer 在不同操作系统版本上的布局和使用情况可能会有所变化
    // 默认索引值(0 ~ 7)拥有 60 位的有效存储空间,其中索引值 7 为保留字段
    // 扩展索引值(8 ~ 264)拥有 52 位的有效存储空间,其中索引值 264 为保留字段
    enum objc_tag_index_t : uint16_t
    {
        // 60-bit payloads(拥有 60 位的有效存储空间)
        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,
    	
    	// 60-bit reserved(索引值 7 为保留字段)
    	OBJC_TAG_RESERVED_7        = 7, 
    	...
    };
    

    ③ 第 4 ~ 7 位是数据长度标识位。当类标识位 == OBJC_TAG_NSString 时,用于表示 NSString 中保存的字符串的长度

    ④ 第 8 ~ 63 位是存储数据位。用于存储 NSString 的真实数据

    结合第 ① 点和第 ② 点可知:
    对于 NSString 类型的 Tagged Pointer,它的低 4 位(第 0 ~ 3 位)恒定为 0x5(0101)
    可以将这 4 位理解为 NSString 类型的 Tagged Pointer 的标识


    代码验证:

    void macOS_tagged_pointer_NSString_demo() {
        NSString* str0 = [NSString stringWithFormat:@""];
        NSString* str1 = [NSString stringWithFormat:@"1"];
        NSString* str2 = [NSString stringWithFormat:@"22"];
        NSString* str3 = [NSString stringWithFormat:@"333"];
        NSString* str4 = [NSString stringWithFormat:@"4444"];
        NSString* str5 = [NSString stringWithFormat:@"55555"];
        NSString* str6 = [NSString stringWithFormat:@"666666"];
        NSString* str7 = [NSString stringWithFormat:@"7777777"];
        NSString* str8 = [NSString stringWithFormat:@"88888888"];
        NSString* str9 = [NSString stringWithFormat:@"999999999"];
        NSString* stra = [NSString stringWithFormat:@"aaaaaaaaaa"];
        NSString* strb = [NSString stringWithFormat:@"bbbbbbbbbbb"];
        NSString* strc = [NSString stringWithFormat:@"cccccccccccc"];
        NSString* strd = [NSString stringWithFormat:@"ddddddddddddd"];
        NSString* stre = [NSString stringWithFormat:@"eeeeeeeeeeeeee"];
        NSString* strf = [NSString stringWithFormat:@"fffffffffffffff"];
        NSString* strz = [NSString stringWithFormat:@"中文"];
        HCGLog(str0);   // 内部类 = __NSCFConstantString,  引用计数 = 18446744073709551615, 指针地址 = 0x7fff90fa90e0,     值 =
        HCGLog(str1);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x3115,             值 = 1
        HCGLog(str2);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x323225,           值 = 22
        HCGLog(str3);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x33333335,         值 = 333
        HCGLog(str4);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x3434343445,       值 = 4444
        HCGLog(str5);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x353535353555,     值 = 55555
        HCGLog(str6);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x36363636363665,   值 = 666666
        HCGLog(str7);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x3737373737373775, 值 = 7777777
        HCGLog(str8);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xaaaaaaaaaaaa85,   值 = 88888888
        HCGLog(str9);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x31c71c71c71c7195, 值 = 999999999
        HCGLog(stra);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x1084210842108a5,  值 = aaaaaaaaaa
        HCGLog(strb);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x1004bfd00,        值 = bbbbbbbbbbb
        HCGLog(strc);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x1004bfd20,        值 = cccccccccccc
        HCGLog(strd);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x1004c00f0,        值 = ddddddddddddd
        HCGLog(stre);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x1004c0110,        值 = eeeeeeeeeeeeee
        HCGLog(strf);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x1004c01d0,        值 = fffffffffffffff
        HCGLog(strz);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x1004c0130,        值 = 中文
        
        // 通过 [[NSString alloc] initWithFormat:@"xxx"] 创建的 NSString:
    	// 1.当 @"xxx" 的长度为 0 时,是 __NSCFConstantString 类型
    	// 2.当 @"xxx" 的长度在 1 ~ 10 之间时,是 NSTaggedPointerString 类型
    	// 3.当 @"xxx" 的长度大于 10 时,是 __NSCFString 类型
    	// 4.NSTaggedPointerString 类型的字符串引用计数恒定为 -1(对其进行 retain、release 等操作,其引用计数不会发生任何变化)
    	// 5.NSTaggedPointerString 底层对字符串是按照一个字节一个 ASCII 码的方式进行存储,当字符串的长度超过 7 Byte 的时候,NSTaggedPointerString 并没有立即转换为 __NSCFString,而是使用了一种压缩算法进行编码,把字符串的长度进行压缩存储(具体的压缩算法我暂未深究)
    	// 6.当这个压缩算法产生的字符串长度超过 7 Byte 时,才会将 NSTaggedPointerString 转换为 __NSCFString
    	// 7.因为字符串存在很多编码标准,其它语言的字符不能用标准的 ASCII 码来表示。所以对于中文、日文等非 ASCII 字符,即使只有一个字符也用 __NSCFString 进行存储
     	
    	// 所有 NSString 类型的 Tagged Pointer 都以 0x5 结尾
        // 0x5 对应的二进制为 0101
    	// 最后一位的  1 是 Tagged Pointer 标识位,代表这个指针是 Tagged Pointer 类型
    	// 前面三位的 010 是类标识位,转换成十进制等于 2,对应 enum objc_tag_index_t 的 OBJC_TAG_NSString,表示 NSString 类
    
    	// 0x3115 0x323225 0x33333335 中的 0x1 0x2 0x3 代表的是什么呢?
    	// 0x1 0x2 0x3 是数据长度标识位,用于表示 NSTaggedPointerString 中保存的字符串的长度
    	
    	// 0x3115 0x323225 0x33333335 中的 0x31 0x3232 0x333333 代表的是什么呢?
    	// 0x31 是字符 a 对应的十六进制 ASCII 码,0x32 是字符 b 对应的十六进制 ASCII 码,0x33 是字符 c 对应的十六进制 ASCII 码
    }
    

Tagged Pointer 的原理: iOS

  • NSNumber 类型的 Tagged Pointer

    下图是 iOS 中 NSNumber 的 Tagged Pointer 位视图:
    tagged_pointer_ios_NSNumber

    由上图可知,在 iOS 中 NSNumber 的 Tagged Pointer 指针中:

    ① 第 63 位是 Tagged Pointer 的标识位。其值为 1 表示该指针是 Tagged Pointer,其值为 0 表示该指针不是 Tagged Pointer

    ② 第 60 ~ 62 位是类标识位。用于表示该指针所属的类对象,是 Tagged Pointer 对象实质上的 isa。类标识位所对应的类对象如下所示:

    // path: objc4-756.2/runtime/objc-internal.h
    // Tagged pointer 在不同操作系统版本上的布局和使用情况可能会有所变化
    // 默认索引值(0 ~ 7)拥有 60 位的有效存储空间,其中索引值 7 为保留字段
    // 扩展索引值(8 ~ 264)拥有 52 位的有效存储空间,其中索引值 264 为保留字段
    enum objc_tag_index_t : uint16_t
    {
        // 60-bit payloads(拥有 60 位的有效存储空间)
        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,
    	
    	// 60-bit reserved(索引值 7 为保留字段)
    	OBJC_TAG_RESERVED_7        = 7, 
    	...
    };
    

    ③ 第 0 ~ 3 位是数据类型标识位。当类标识位 == OBJC_TAG_NSNumber 时,用于表示 NSNumber 中保存的数据类型:

    数据类型标识位NSNumber 中保存的数据类型
    0char
    1short
    2int
    3long
    4float
    5double

    ④ 第 4 ~ 59 位是存储数据位。用于存储 NSNumber 的真实数据

    结合第 ① 点和第 ② 点可知:
    对于 NSNumber 类型的 Tagged Pointer,它的高 4 位(第 60 ~ 63 位)恒定为 0xb(1011)
    可以将这 4 位理解为 NSNumber 类型的 Tagged Pointer 的标识


    代码验证:

    void iOS_tagged_pointer_NSNumber_demo() {
        NSNumber* num1 = @1;
        NSNumber* num2 = @2;
        NSNumber* num3 = @3;
        NSNumber* num4 = @(0xFFFFFFFFFFFFFFFF);
        NSLog(@"num1 = %p", num1);  // num1 = 0xb000000000000012
        NSLog(@"num2 = %p", num2);  // num2 = 0xb000000000000022
        NSLog(@"num3 = %p", num3);  // num3 = 0xb000000000000032
        NSLog(@"num4 = %p", num4);  // num4 = 0x280d7b740
        // 从以上打印结果可以看出:
        
        // num1 ~ num3 的指针为 Tagged Pointer 类型,对象的值都直接存储在了指针当中,对应 0x1、0x2、0x3
        // num4 因为数据过大,Tagged Pointer 的存储数据位不够存储,所以内存分配在堆中
        
        // 所有 NSNumber 类型的 Tagged Pointer 都以 0xb 开头
        // 0xb 对应的二进制为 1011
        // 前面一位的  1 是 Tagged Pointer 标识位,代表这个指针是 Tagged Pointer 类型
        // 最后三位的 011 是类标识位,转换成十进制等于 3,对应 enum objc_tag_index_t 的 OBJC_TAG_NSNumber,表示 NSNumber 类
        
        // 0xb000000000000012 0xb000000000000022 0xb000000000000032 中的 0x2 代表的是什么呢?
        // 0x2 是数据类型标识位,用于表示 NSNumber 中存储的数据的类型
        // 0x0 - char、0x1 - short、0x2 - int、0x3 - long、0x4 - float、0x5 - double
        char    aChar   = 'a';
        short   aShort  = 1;
        int     anInt   = 1;
        long    aLong   = 1;
        float   aFloat  = 1.0f;
        double  aDouble = 1.0;
        NSNumber* numChar   = @(aChar);
        NSNumber* numShort  = @(aShort);
        NSNumber* numInt    = @(anInt);
        NSNumber* numLong   = @(aLong);
        NSNumber* numFloat  = @(aFloat);
        NSNumber* numDouble = @(aDouble);
        NSLog(@"numChar   = %p", numChar);      // numChar   = 0xb000000000000610
        NSLog(@"numShort  = %p", numShort);     // numShort  = 0xb000000000000011
        NSLog(@"numInt    = %p", numInt);       // numInt    = 0xb000000000000012
        NSLog(@"numLong   = %p", numLong);      // numLong   = 0xb000000000000013
        NSLog(@"numFloat  = %p", numFloat);     // numFloat  = 0xb000000000000014
        NSLog(@"numDouble = %p", numDouble);    // numDouble = 0xb000000000000015
    }
    
  • NSString 类型的 Tagged Pointer

    下图是 iOS 中 NSString 的 Tagged Pointer 位视图:
    tagged_pointer_ios_NSNSString

    ① 第 63 位是 Tagged Pointer 的标识位。其值为 1 表示该指针是 Tagged Pointer,其值为 0 表示该指针不是 Tagged Pointer

    ② 第 60 ~ 62 位是类标识位。用于表示该指针所属的类对象,是 Tagged Pointer 对象实质上的 isa。类标识位所对应的类对象如下所示:

    // path: objc4-756.2/runtime/objc-internal.h
    // Tagged pointer 在不同操作系统版本上的布局和使用情况可能会有所变化
    // 默认索引值(0 ~ 7)拥有 60 位的有效存储空间,其中索引值 7 为保留字段
    // 扩展索引值(8 ~ 264)拥有 52 位的有效存储空间,其中索引值 264 为保留字段
    enum objc_tag_index_t : uint16_t
    {
        // 60-bit payloads(拥有 60 位的有效存储空间)
        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,
    	
    	// 60-bit reserved(索引值 7 为保留字段)
    	OBJC_TAG_RESERVED_7        = 7, 
    	...
    };
    

    ③ 第 0 ~ 3 位是数据长度标识位。当类标识位 == OBJC_TAG_NSString 时,用于表示 NSString 中保存的字符串的长度

    ④ 第 4 ~ 59 位是存储数据位。用于存储 NSString 的真实数据

    结合第 ① 点和第 ② 点可知:
    对于 NSString 类型的 Tagged Pointer,它的高 4 位(第 60 ~ 63 位)恒定为 0xa(1010)
    可以将这 4 位理解为 NSString 类型的 Tagged Pointer 的标识


    代码验证:

    void iOS_tagged_pointer_NSString_demo() {
        NSString* str0 = [NSString stringWithFormat:@""];
        NSString* str1 = [NSString stringWithFormat:@"1"];
        NSString* str2 = [NSString stringWithFormat:@"22"];
        NSString* str3 = [NSString stringWithFormat:@"333"];
        NSString* str4 = [NSString stringWithFormat:@"4444"];
        NSString* str5 = [NSString stringWithFormat:@"55555"];
        NSString* str6 = [NSString stringWithFormat:@"666666"];
        NSString* str7 = [NSString stringWithFormat:@"7777777"];
        NSString* str8 = [NSString stringWithFormat:@"88888888"];
        NSString* str9 = [NSString stringWithFormat:@"999999999"];
        NSString* stra = [NSString stringWithFormat:@"aaaaaaaaaa"];
        NSString* strb = [NSString stringWithFormat:@"bbbbbbbbbbb"];
        NSString* strc = [NSString stringWithFormat:@"cccccccccccc"];
        NSString* strd = [NSString stringWithFormat:@"ddddddddddddd"];
        NSString* stre = [NSString stringWithFormat:@"eeeeeeeeeeeeee"];
        NSString* strf = [NSString stringWithFormat:@"fffffffffffffff"];
        NSString* strz = [NSString stringWithFormat:@"中文"];
        HCGLog(str0);   // 内部类 = __NSCFConstantString,  引用计数 = 18446744073709551615, 指针地址 = 0x1fbfb3410,        值 =
        HCGLog(str1);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa000000000000311, 值 = 1
        HCGLog(str2);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa000000000032322, 值 = 22
        HCGLog(str3);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa000000003333333, 值 = 333
        HCGLog(str4);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa000000343434344, 值 = 4444
        HCGLog(str5);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa000035353535355, 值 = 55555
        HCGLog(str6);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa003636363636366, 值 = 666666
        HCGLog(str7);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa373737373737377, 值 = 7777777
        HCGLog(str8);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa00aaaaaaaaaaaa8, 值 = 88888888
        HCGLog(str9);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa31c71c71c71c719, 值 = 999999999
        HCGLog(stra);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xa01084210842108a, 值 = aaaaaaaaaa
        HCGLog(strb);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x28258abe0,        值 = bbbbbbbbbbb
        HCGLog(strc);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x28258ac20,        值 = cccccccccccc
        HCGLog(strd);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x28258ac40,        值 = ddddddddddddd
        HCGLog(stre);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x28258ac60,        值 = eeeeeeeeeeeeee
        HCGLog(strf);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x282bd19e0,        值 = fffffffffffffff
        HCGLog(strz);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x28258ac80,        值 = 中文
        
        // 通过 [[NSString alloc] initWithFormat:@"xxx"] 创建的 NSString:
        // 1.当 @"xxx" 的长度为 0 时,是 __NSCFConstantString 类型
        // 2.当 @"xxx" 的长度在 1 ~ 10 之间时,是 NSTaggedPointerString 类型
        // 3.当 @"xxx" 的长度大于 10 时,是 __NSCFString 类型
        // 4.NSTaggedPointerString 类型的字符串引用计数恒定为 -1(对其进行 retain、release 等操作,其引用计数不会发生任何变化)
        // 5.NSTaggedPointerString 底层对字符串是按照一个字节一个 ASCII 码的方式进行存储,当字符串的长度超过 7 Byte 的时候,NSTaggedPointerString 并没有立即转换为 __NSCFString,而是使用了一种压缩算法进行编码,把字符串的长度进行压缩存储(具体的压缩算法我暂未深究)
        // 6.当这个压缩算法产生的字符串长度超过 7 Byte 时,才会将 NSTaggedPointerString 转换为 __NSCFString
        // 7.因为字符串存在很多编码标准,其它语言的字符不能用标准的 ASCII 码来表示。所以对于中文、日文等非 ASCII 字符,即使只有一个字符也用 __NSCFString 进行存储
         
        // 所有 NSString 类型的 Tagged Pointer 都以 0xa 开头
        // 0xa 对应的二进制为 1010
        // 前面一位的  1 是 Tagged Pointer 标识位,代表这个指针是 Tagged Pointer 类型
        // 最后三位的 010 是类标识位,转换成十进制等于 2,对应 enum objc_tag_index_t 的 OBJC_TAG_NSString,表示 NSString 类
    
        // 0xa000000000000311 0xa000000000032322 0xa000000003333333 中的 0x1 0x2 0x3 代表的是什么呢?
        // 0x1 0x2 0x3 是数据长度标识位,用于表示 NSTaggedPointerString 中保存的字符串的长度
        
        // 0xa000000000000311 0xa000000000032322 0xa000000003333333 中的 0x31 0x3232 0x333333 代表的是什么呢?
        // 0x31 是字符 a 对应的十六进制 ASCII 码,0x32 是字符 b 对应的十六进制 ASCII 码,0x33 是字符 c 对应的十六进制 ASCII 码
    }
    

Tagged Pointer 的判断与内存管理

  • Tagged Pointer 的判断

    通过前面的介绍我们知道了:可以通过 Tagged Pointer 的标识位来判断一个指针是否为 Tagged Pointer

    并且 RunTime 的源码中提供了相应的函数来判断一个指针是否为 Tagged Pointer:

    // path: objc4-756.2/runtime/objc-object.h
    inline bool objc_object::isTaggedPointer() 
    {
        return _objc_isTaggedPointer(this);
    }
    
    // path: objc4-756.2/runtime/objc-internal.h
    // 如果给定的指针 ptr 是一个 tagged pointer 对象,则返回 YES。否则,返回 NO
    // 不检查给定指针 ptr 所属类的有效性(即不检查类标识位的有效性)
    static inline bool _objc_isTaggedPointer(const void * _Nullable ptr)
    {
        return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
    }
    

    可以看到,_objc_isTaggedPointer 函数的内部实现是将指针 ptr 的值与一个宏定义的掩码 _OBJC_TAG_MASK 进行按位 & 运算,查看该掩码的定义:

    #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
    
    // macOS 下采用 LSB(Least Significant Bit:最低有效位)为 Tagged Pointer 标识位
    // iOS 下采用 MSB(Most Significant Bit:最高有效位)为 Tagged Pointer 标识位
    #if OBJC_MSB_TAGGED_POINTERS
    #   define _OBJC_TAG_MASK (1UL<<63)	// 100000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    	...
    #else
    #   define _OBJC_TAG_MASK 1UL		// 000000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
    	...
    #endif
    
  • Tagged Pointer 的内存管理

    Tagged Pointer 类型的对象,其引用计数恒定为 -1(对其进行 retainrelease 等操作,其引用计数不会发生任何变化)

    // path: objc4-756.2/runtime/objc-object.h
    ALWAYS_INLINE id 
    objc_object::rootRetain(bool tryRetain, bool handleOverflow)
    {
    	// Tagged Pointer 的 Retain 逻辑
        if (isTaggedPointer()) return (id)this;
    	
    	// 非 Tagged Pointer 的 Retain 逻辑
    	...
    }
    
    // path: objc4-756.2/runtime/objc-object.h
    ALWAYS_INLINE bool 
    objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
    {
    	// Tagged Pointer 的 Release 逻辑
        if (isTaggedPointer()) return false;
    
    	// Tagged Pointer 的 Release 逻辑
    	...
    }
    

Tagged Pointer 的注意点

  • LSB 与 MSB

    macOS 下采用 LSB(Least Significant Bit:最低有效位)为 Tagged Pointer 标识位
    iOS 下采用 MSB(Most Significant Bit:最高有效位)为 Tagged Pointer 标识位

    在 64 位的 macOS 下:
    Tagged Pointer 类型的指针最低位为 1,非 Tagged Pointer 类型的指针最低位为 0
    因为内存对其机制的存在,64 位的 macOS 在堆空间中会以 8 Byte 进行内存对齐。举个例子:
    在 64 位的 macOS 下,Tagged Pointer 的指针是长这样的 0x0000000000003115
    在 64 位的 macOS 下,普通的指针是长这样的 0x00000001004bfd00
    综上所述,在 64 位的 macOS 下采用 LSB 并不会干扰到普通指针的使用

    在 64 位的 iOS 下:
    Tagged Pointer 类型的指针最高位为 1,非 Tagged Pointer 类型的指针最高位为 0
    虽然 64 位的指针能表示的最大内存地址有 16 EB,但是程序中使用的内存地址在通常情况下远远小于该值。举个例子:
    在 64 位的 iOS 下,Tagged Pointer 的指针是长这样的 0xa000000000000311
    在 64 位的 iOS 下,普通的指针是长这样的 0x000000028258ac20
    综上所述,在 64 位的 iOS 下采用 MSB 并不会干扰到普通指针的使用

  • Tagged Pointer 与 isa

    我们知道,所有 Objective-C 对象都有 isa 指针。因为 Tagged Pointer 并不是真正的 Objective-C 对象,所以 Tagged Pointer 没有 isa 指针。如果直接访问 Tagged Pointer 的 isa 成员的话,在编译时将会有如下警告:

    Tagged Pointer 与 isa
    对于 Tagged Pointer 类型的对象,应该将访问 isa 指针转换成相应的方法调用,如:isKindOfClass 方法和 object_getClass 函数。只要避免在代码中直接访问 Tagged Pointer 对象的 isa 指针,即可避免这个问题

    当然,现在也不允许在代码中直接访问对象的 isa 指针了,否则编译会不通过

    通过 lldb 打印 Tagged Pointer 对象的 isa 指针,会提示如下错误:

    NSNumber* num1 = @1;
    
    (lldb) p num1->isa
    error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory
    

    通过 lldb 打印普通 Objective-C 对象的 isa 指针,是没有问题的:

    NSNumber* num4 = @(0xFFFFFFFFFFFFFFFF);
    
    (lldb) p num4->isa
    (Class) $1 = __NSCFNumber
    
  • 如何创建 NSString 类型的 Tagged Pointer ?

    简单来说,NSString 是一个采用抽象工厂模式设计的类簇(Class Cluster)。NSStringNSMutableString 在外部提供了很多方法接口,这些方法接口的实现由具体的内部类来完成。当使用 NSStringNSMutableString 的外部接口生成一个实例对象时,初始化方法会判断哪个内部类最适合生成这个实例对象,然后根据此内部类生成具体的实例对象返回给调用者。不同的创建方式以及不同的字符长度都可能影响最终得到的内部类的类型

    NSStringNSMutableString 所使用的内部类如下所示:

    1. NSTaggedPointerString 的数据直接存储在了指针中,不需要维护引用计数
    2. NSCFConstantString 用于表示字符串常量,存储在字符串常量区,不需要维护引用计数。相同内容的 NSCFConstantString 对象的内存地址相同,也就是说字符串常量对象是一种单例对象。NSCFConstantString 对象一般通过字面量 @"xxx" 创建
    3. NSCFString 存储在堆区,需要维护其引用计数。通过 stringWithFormat: 等方法创建的NSString 对象(且字符串长度过长,无法使用 Tagged Pointer 存储)一般都是这种类型

    NSStringNSMutableString 所使用的内部类的继承关系如下所示:

    1. NSTaggedPointerString 继承自 NSString
    2. NSCFConstantString 继承自 NSCFString 继承自 NSMutableString 继承自 NSString

    通过字面量 @"xxx" 创建的 NSString

    1. 无论字面量 @"xxx" 多长或者多短,都是 __NSCFConstantString 类型
    2. 无论字面量 @"xxx" 是中文或者英文,都是 __NSCFConstantString 类型
    3. 在创建相同内容的字符串时,得到的内存地址相同
    4. __NSCFConstantString 类型的字符串引用计数恒定为 -1(对其进行 retainrelease 等操作,其引用计数不会发生任何变化)
    5. [[NSString alloc] initWithString:@"xxx"][NSString stringWithString:@"xxx"] 与直接用字面量赋值的结果是一样的,创建的都是 __NSCFConstantString 类型的字符串
    void NSStringDemo0() {
        NSString* str0 = @"中文";
        NSString* str1 = @"a";
        NSString* str2 = @"a";
        NSString* str3 = @"abcdefghijklmnopqrstuvwxyz";
        NSString* str4 = [[NSString alloc] initWithString:@"a"];
        NSString* str5 = [NSString stringWithString:@"a"];
        HCGLog(str0);   // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x100004150, 值 = 中文
        HCGLog(str1);   // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x100004170, 值 = a
        HCGLog(str2);   // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x100004170, 值 = a
        HCGLog(str3);   // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x100004190, 值 = abcdefghijklmnopqrstuvwxyz
        HCGLog(str4);   // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x100004170, 值 = a
        HCGLog(str5);   // 内部类 = __NSCFConstantString, 引用计数 = 18446744073709551615, 指针地址 = 0x100004170, 值 = a
    }
    

    通过 [[NSString alloc] initWithFormat:@"xxx"] 创建的 NSString

    1. @"xxx" 的长度为 0 时,是 __NSCFConstantString 类型
    2. @"xxx" 的长度在 1 ~ 10 之间时,是 NSTaggedPointerString 类型
    3. @"xxx" 的长度大于 10 时,是 __NSCFString 类型
    4. NSTaggedPointerString 类型的字符串引用计数恒定为 -1(对其进行 retainrelease 等操作,其引用计数不会发生任何变化)
    5. NSTaggedPointerString 底层对字符串是按照一个字节一个 ASCII 码的方式进行存储
    6. 当字符串的长度超过 7 Byte 的时候,NSTaggedPointerString 并没有立即转换为 __NSCFString,而是使用了一种压缩算法进行编码,把字符串的长度进行压缩存储(具体的压缩算法我暂未深究)
    7. 当这个压缩算法产生的字符串长度超过 7 Byte 时,才会将 NSTaggedPointerString 转换为 __NSCFString
    8. 因为字符串存在很多编码标准,其它语言的字符不能用标准的 ASCII 码来表示。所以对于中文、日文等非 ASCII 字符,即使只有一个字符也用 __NSCFString 进行存储
    void NSStringDemo1() {
        NSString* str0 = [[NSString alloc] initWithFormat:@""];
        NSString* str1 = [[NSString alloc] initWithFormat:@"1"];
        NSString* str2 = [[NSString alloc] initWithFormat:@"22"];
        NSString* str3 = [[NSString alloc] initWithFormat:@"333"];
        NSString* str4 = [[NSString alloc] initWithFormat:@"4444"];
        NSString* str5 = [[NSString alloc] initWithFormat:@"55555"];
        NSString* str6 = [[NSString alloc] initWithFormat:@"666666"];
        NSString* str7 = [[NSString alloc] initWithFormat:@"7777777"];
        NSString* str8 = [[NSString alloc] initWithFormat:@"88888888"];
        NSString* str9 = [[NSString alloc] initWithFormat:@"999999999"];
        NSString* stra = [[NSString alloc] initWithFormat:@"aaaaaaaaaa"];
        NSString* strb = [[NSString alloc] initWithFormat:@"bbbbbbbbbbb"];
        NSString* strc = [[NSString alloc] initWithFormat:@"cccccccccccc"];
        NSString* strd = [[NSString alloc] initWithFormat:@"ddddddddddddd"];
        NSString* stre = [[NSString alloc] initWithFormat:@"eeeeeeeeeeeeee"];
        NSString* strf = [[NSString alloc] initWithFormat:@"fffffffffffffff"];
        NSString* strz = [[NSString alloc] initWithFormat:@"中文"];
        HCGLog(str0);   // 内部类 = __NSCFConstantString,  引用计数 = 18446744073709551615, 指针地址 = 0x7fff90fa90e0,     值 =
        HCGLog(str1);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x3115,             值 = 1
        HCGLog(str2);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x323225,           值 = 22
        HCGLog(str3);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x33333335,         值 = 333
        HCGLog(str4);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x3434343445,       值 = 4444
        HCGLog(str5);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x353535353555,     值 = 55555
        HCGLog(str6);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x36363636363665,   值 = 666666
        HCGLog(str7);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x3737373737373775, 值 = 7777777
        HCGLog(str8);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0xaaaaaaaaaaaa85,   值 = 88888888
        HCGLog(str9);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x31c71c71c71c7195, 值 = 999999999
        HCGLog(stra);   // 内部类 = NSTaggedPointerString, 引用计数 = 18446744073709551615, 指针地址 = 0x1084210842108a5,  值 = aaaaaaaaaa
        HCGLog(strb);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x100607030,        值 = bbbbbbbbbbb
        HCGLog(strc);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x100605e60,        值 = cccccccccccc
        HCGLog(strd);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x100605700,        值 = ddddddddddddd
        HCGLog(stre);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x1006068f0,        值 = eeeeeeeeeeeeee
        HCGLog(strf);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x100607170,        值 = fffffffffffffff
        HCGLog(strz);   // 内部类 = __NSCFString,          引用计数 = 1,                    指针地址 = 0x100604f80,        值 = 中文
    }
    

    通过 [[NSMutableString alloc] initWithFormat:@"xxx"] 创建的 NSMutableString

    1. 无论 @"xxx" 为何值,都是 __NSCFString 类型
    2. __NSCFString 根据引用计数可以知道其存储在堆上,需要对其进行内存管理
    void NSStringDemo2() {
        NSMutableString* str0 = [[NSMutableString alloc] initWithFormat:@""];
        NSMutableString* str1 = [[NSMutableString alloc] initWithFormat:@"1"];
        NSMutableString* str2 = [[NSMutableString alloc] initWithFormat:@"fffffffffffffff"];
        NSMutableString* str3 = [[NSMutableString alloc] initWithFormat:@"中文"];
        HCGLog(str0);   // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x1004bdfb0, 值 =
        HCGLog(str1);   // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x1004be370, 值 = 1
        HCGLog(str2);   // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x100407610, 值 = fffffffffffffff
        HCGLog(str3);   // 内部类 = __NSCFString, 引用计数 = 1, 指针地址 = 0x100407660, 值 = 中文
    }
    

一道面试题

  • Question

    执行以下两段代码,有什么区别?

    @property (nonatomic, strong) NSString* name;
    
    -(void)demo1 {
        dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
        for (int i = 0; i < 1000; i ++) {
            dispatch_async(queue1, ^{
                self.name = [NSString stringWithFormat:@"abcdefghi"];
            });
        }
        NSLog(@"end");
    }
    
    @property (nonatomic, strong) NSString* name;
    
    -(void)demo2 {
        dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
        for (int i = 0; i < 1000; i ++) {
            dispatch_async(queue2, ^{
                self.name = [NSString stringWithFormat:@"abcdefghij"];
            });
        }
        NSLog(@"end");
    }
    
  • Analyse

    刚看到这道面试题时,心里一万个草泥马?奔腾而过~~~,两段代码的差别不就是:demo1 中字符串的长度比 demo2 中字符串的长度少了一位吗,哪有什么差别?

    结果一运行,哎呀!第二段代码居然 Crash 了!而第一段代码却没有问题!奇了怪了?

    分别打印两段代码中 self.name 的类型看看:
    第一段代码 demo1self.nameNSTaggedPointerString 类型
    第二段代码 demo2self.name__NSCFString 类型

    我们再来看一下第二段代码 demo2 Crash 的地方:
    Crash

    想必 Carsh 的原因你已经猜到了:
    第二段代码 demo2 中的 self.name__NSCFString 类型,存储在堆上,它是个正常的 Objective-C 对象,需要维护引用计数
    self.name = [NSString stringWithFormat:@"abcdefghij"] 是通过 name 属性的 setter 方法进行赋值的。而 name 属性的 setter 方法的实现如下所示:

    -(void)setName:(NSString *)name {
        if(_name != name) {
            [_name release];
            _name = [name retain]; // or [name copy]
        }
    }
    

    程序异步并发执行 name 属性的 setter 方法,可能就会有多条线程同时执行 [_name release] ,连续对 _name 进行两次 release 就会造成对象的过度释放,导致 Crash

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

  • Answer

    解决方法 ①
    self.name 属性改为用 atomic 修饰

    @property (atomic, strong) NSString* name;
    

    解决方法 ②
    将并发执行的任务改为串行执行的任务

    dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_SERIAL);
    

    解决方法 ③
    将异步执行改为同步执行

    dispatch_sync(queue2, ^{
    	self.name = [NSString stringWithFormat:@"abcdefghij"];
    });
    

    解决方法 ④
    对赋值过程进行加锁

    -(void)demo2 {
        NSLock* locker = [[NSLock alloc] init];
        dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
        for (int i = 0; i < 1000; i ++) {
            dispatch_async(queue2, ^{
                [locker lock];
                self.name = [NSString stringWithFormat:@"abcdefghij"];
                [locker unlock];
            });
        }
        NSLog(@"end");
    }
    
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值