OC内存管理-内存分区、TaggedPointer

一、内存布局

1.1 内存分区

image.png
  • 栈区:存储函数、方法、指针、局部变量、参数等(访问速度快,通过寄存器访问),当局部变量的作用域被执行完毕之后,这个局部变量就会被系统立即回收。内存地址一般以0x7开头。
  • 堆区:开辟内存空间(newalloccopymalloccallocrealloc),一般放对象。内存地址一般以0x6开头。
  • 全局区:分为 BSS段DATA段。内存地址一般以0x1开头。
    • BSS段:存储未初始化的全局变量,静态变量。一旦初始化就回收,并转存到数据段中。
    • DATA段:存放已初始化的全局变量,静态变量。直到程序结束才会被回收。
  • 代码段(text):存储程序代码,加载到内存中。直到程序结束才会被回收。
  • 内核区:系统内核使用(1GB),比如GCD开辟线程所用的空间。0xc0000000也就是对应3GB
  • 保留0x00400000(4MB)。

栈区的内存是通过SP寄存器去定位的。堆区是通过寄存器中地址去定位的。

1.1.1 验证

栈区验证

// 栈区
int a = 10;
int b = 20;
//栈区分配指针,指向堆区开辟的内存空间。
NSObject *obj = [NSObject new];
NSObject *obj2 = obj;
NSLog(@"a address:%p",&a);
NSLog(@"b address:%p",&b);
NSLog(@"obj pointer address:%p",&obj);
NSLog(@"obj address:%p",obj);
NSLog(@"obj2 pointer address:%p",&obj2);
NSLog(@"obj2 address:%p",obj2);

输出:

a address:0x7ffee63e90bc
b address:0x7ffee63e90b8
obj pointer address:0x7ffee63e90b0
obj address:0x600002860060
obj2 pointer address:0x7ffee63e90a8
obj2 address:0x600002860060

ab都是临时变量,分配在栈区以0x7开头。
obj以及obj2分配了两个指针在栈区,指向堆区的同一块内存。

堆区验证

NSObject *object1 = [NSObject new];
NSObject *object2 = [NSObject new];
NSObject *object3 = [NSObject new];
NSObject *object4 = [NSObject new];
NSLog(@"object1 = %@",object1);
NSLog(@"object2 = %@",object2);
NSLog(@"object3 = %@",object3);
NSLog(@"object4 = %@",object4);

输出:

object1 = 
object2 = 
object3 = 
object4 = 

栈区内存地址以0x6开头。寄存器通过分配的栈区指针访问指向堆区的地址。

全局区验证

int A;
int B = 10;

static int bssA;
static NSString *bssStr1;

static int dataB = 10;
static NSString *dataStr2 = @"HP";
static NSString *dataStr3 = @"Cat";

- (void)testConst {
    //bss
    NSLog(@"A: %p",&A);
    NSLog(@"bssA: %p",&bssA);
    NSLog(@"bssStr1: %p",&bssStr1);
    
    //data
    NSLog(@"B: %p",&B);
    NSLog(@"dataB: %p",&dataB);
    NSLog(@"dataStr2: %p",&dataStr2);
    NSLog(@"dataStr3: %p",&dataStr3);
}

输出:

A: 0x10ebb36b0
bssA: 0x10ebb36b8
bssStr1: 0x10ebb36c0

B: 0x10ebb3510
dataB: 0x10ebb3528
dataStr2: 0x10ebb3518
dataStr3: 0x10ebb3520

全局区内存以0x1开始,已初始化地址小于未初始化地址。

全局区安全性问题
有如下代码:
HPObject

static int num = 100;

@interface HPObject : NSObject

- (void)test;

+ (void)test2;

@end

@implementation HPObject

- (void)test {
    num++;
    NSLog(@"HPObject 内部 test num:%@-%p--%d",self,&num,num);
}

+ (void)test2 {
    num++;
    NSLog(@"HPObject 内部 test2 num:%@-%p--%d",self,&num,num);
}

@end

HPObject + HP分类

@implementation HPObject (HP)

- (void)test3 {
    num++;
    NSLog(@"HPObject 分类内部 test3 num:%@-%p--%d",self,&num,num);
}

@end

调用(ViewController.m):

NSLog(@"vc:%p--%d",&num,num); // 100
num = 10000;
NSLog(@"vc:%p--%d",&num,num); // 10000
[[HPObject new] test]; // 100 + 1 = 101
NSLog(@"vc:%p--%d",&num,num); // 10000
[HPObject test2]; // 102
NSLog(@"vc:%p--%d",&num,num); // 10000

[[HPObject alloc] test3];
NSLog(@"vc:%p--%d",&num,num); // 10000

输出:

vc:0x104e68688--100
vc:0x104e68688--10000
HPObject 内部 test num:-0x104e68690--101
vc:0x104e68688--10000
HPObject 内部 test2 num:HPObject-0x104e68690--102
vc:0x104e68688--10000
HPObject 分类内部 test3 num:-0x104e68758--101
vc:0x104e68688--10000

可以看到num虽然可以修改但是是以文件为单位的,只是文件内有效,不同的文件对应的num地址不同。

静态全局变量针对文件有效

num的定义修为:

//.h
extern int num;
//.m
int num = 100;

输出:

vc:0x10965e688--100
vc:0x10965e688--10000
HPObject 内部 test num:-0x10965e688--10001
vc:0x10965e688--10001
HPObject 内部 test2 num:HPObject-0x10965e688--10002
vc:0x10965e688--10002
HPObject 分类内部 test3 num:-0x10965e688--10003
vc:0x10965e688--10003

这个时候修改全局有效,num所有文件中是同一个地址。extern全局有效

1.2 内存管理方案

  • MRC & ARC
  • 垃圾回收(Garbage Collection):目前已经不支持了。
  • TaggedPointer:小对象(NSNumberNSDate)。
  • NONPOINTER_ISA:非指针型isa
  • 散列表:引用计数表、弱引用表。

静态全局变量针对文件有效,extern全局有效。

二、TaggedPointer

Tagged Pointer 是一种特殊标记的对象,通过在其最后一个 bit 位设置为特殊标记位,并将数据直接保存在指针自身中。
64 位系统中,有 64 位空间可以表示一个对象指针。由于内存对齐,通常没有真正使用到所有这些位。对象必须位于指针大小倍数的地址中,低位和高位均被 0填充,因此只用到了中间部分的位,出现了大量的内存浪费。

x86:

x86

arm64:
arm64

  • Tagged Pointer 标记x86最后一位是标记位,arm64最高位是标记位。1表示是Tagged Pointer对象,0表示是普通对象。
  • Tag:对象类型标记。x861~3位,arm640~27表示有扩展信息。
  • Extendedx864~11位,arm6454~62。用来扩展更多类型。
  • payload:有效负载。存储真正的数据(除了标记位、tag以及extended),不过为了安全苹果做了编码。

小结:

  • Tagged Pointer(指针标记)专⻔用来存储小的对象,例如NSNumberNSDate(针对64位指针)。
  • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,是一个披着对象皮的普通变量而已。内存并不存储在堆中,也不需要mallocfree
  • 在内存读取上有着3倍的效率,创建与释放比以前快106倍。

arm64使用高位存储标记位,为了objc_msgSend的优化。在最高位能一次排查Tagged Pointer 指针和 nil两种类型,节省了一个case的逻辑。

2.1 TaggedPointer 结构

NSString *str = [NSString stringWithFormat:@"hp"];
NSLog(@"%p - %@ - %@",str,str,str.class);

NSString *str1 = @"hp";
NSLog(@"%p - %@ - %@",str1,str1,str1.class);

NSString *str2 = [[NSString alloc] initWithString:@"hp"];
NSLog(@"%p - %@ - %@",str2,str2,str2.class);

NSString *str3 = [str copy];
NSLog(@"%p - %@ - %@",str3,str3,str3.class);

NSString *str4 = [str1 copy];
NSLog(@"%p - %@ - %@",str4,str4,str4.class);

NSString *str5 = [[NSString alloc] initWithString:str];
NSLog(@"%p - %@ - %@",str5,str5,str5.class);

NSString *str6 = [[NSString alloc] initWithFormat:@"hp"];
NSLog(@"%p - %@ - %@",str6,str6,str6.class);

NSString *str7 = [[NSString alloc] initWithCString:"hp" encoding:NSASCIIStringEncoding];
NSLog(@"%p - %@ - %@",str7,str7,str7.class);

NSString *str8 = [[NSString alloc] initWithUTF8String:"hp"];
NSLog(@"%p - %@ - %@",str8,str8,str8.class);

输出:

0xf67b106ca2ef7750 - hp - NSTaggedPointerString
0x10b1e4020 - hp - __NSCFConstantString
0x10b1e4020 - hp - __NSCFConstantString
0xf67b106ca2ef7750 - hp - NSTaggedPointerString
0x10b1e4020 - hp - __NSCFConstantString
0xf67b106ca2ef7750 - hp - NSTaggedPointerString
0xf67b106ca2ef7750 - hp - NSTaggedPointerString
0xf67b106ca2ef7750 - hp - NSTaggedPointerString
0xf67b106ca2ef7750 - hp - NSTaggedPointerString
  • stringWithFormat以及char * 字符串创建的字符串是NSTaggedPointerString类型。
  • copy创建的字符串类型与它拷贝的目标字符串有关。
  • 字面量创建的字符串为__NSCFConstantString类型,其它创建字符串与传递的字符串类型有关。

很明显NSTaggedPointerString类型的字符串地址是0xf67b106ca2ef7750与前面分析的内存地址完全不同。它是一个TaggedPointer类型的指针。

objc源码中taggedpointer有如下注释:

image.png

可以看到要得到payload需要解码后进行位移操作。
decodeencode对应如下:

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;

    value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
    value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT;
#endif
    return value;
}

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
        return (void *)ptr;
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
    value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
    value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
#endif
    return (void *)value;
}

_objc_decodeTaggedPointer调用了_objc_decodeTaggedPointer_noPermute:

static inline uintptr_t
_objc_decodeTaggedPointer_noPermute(const void * _Nullable ptr)
{
    uintptr_t value = (uintptr_t)ptr;
#if OBJC_SPLIT_TAGGED_POINTERS
    //没有混淆
    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
        return value;
#endif
    //有混淆
    return value ^ objc_debug_taggedpointer_obfuscator;
}

objc_debug_taggedpointer_obfuscator定义如下:

extern uintptr_t objc_debug_taggedpointer_obfuscator;

它是在initializeTaggedPointerObfuscator中设置的:

static void
initializeTaggedPointerObfuscator(void)
{
//    if (!DisableTaggedPointerObfuscation && dyld_program_sdk_at_least(dyld_fall_2018_os_versions))
    //允许混淆
    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));
        //获取tag。
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;

#if OBJC_SPLIT_TAGGED_POINTERS
        // The obfuscator doesn't apply to any of the extended tag mask or the no-obfuscation bit.
        objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);

        // Shuffle the first seven entries of the tag permutator.
        int max = 7;
        for (int i = max - 1; i >= 0; i--) {
            int target = arc4random_uniform(i + 1);
            swap(objc_debug_tag60_permutations[i],
                 objc_debug_tag60_permutations[target]);
        }
#endif
    } else {//不允许混淆
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        objc_debug_taggedpointer_obfuscator = 0;
    }
}
  • objc_debug_taggedpointer_obfuscator是一个随机数。
  • DisableTaggedPointerObfuscation可以通过配置OBJC_DISABLE_TAG_OBFUSCATION来控制是否开启。
  • initializeTaggedPointerObfuscator是在类的加载_read_images的时候调用的。

那么可以模仿decode的逻辑,实现自己的解码函数(这里不是真机,真机还有额外处理):

//声明
extern uintptr_t objc_debug_taggedpointer_obfuscator;
//模仿解码
uintptr_t hp_objc_decodeTaggedpointer(id ptr) {
    return (uintptr_t)ptr^objc_debug_taggedpointer_obfuscator;
}

由于objc_debug_taggedpointer_obfuscator是全局静态变量,所以可以直接extern声明就好了。

当然更简单的方法是配置OBJC_DISABLE_TAG_OBFUSCATIONYES关闭混淆。

payload获取_objc_getTaggedPointerValue如下:

//获取payload
static inline uintptr_t
_objc_getTaggedPointerValue(const void * _Nullable ptr) 
{
    // ASSERT(_objc_isTaggedPointer(ptr));
    //解码
    uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
    //_OBJC_TAG_INDEX_SHIFT 为 0/1/60,_OBJC_TAG_INDEX_MASK 为 7
    // (value >> 0/60) & 0x7。arm64 为 0/60 iOS14后为0,之前为60 ,x86为1
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    // == 7,也就是有extend
    if (basicTag == _OBJC_TAG_INDEX_MASK) {
        //(value << 0/9/12) >> 12。 arm64 iOS14 9,之前为12,x86 为 0。由于都左移了,所以还原需要 >> 12(加起来是12)
        return (value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
    } else {
        // (value << 0/1/4) >> 4。arm64 iOS14 1,之前 4。 x86 为 0。需要右移 >> 4还原。
        return (value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
    }
}

通过位移实现解码后的数据还原,为了方便只模拟iOS14mac的逻辑:

static inline uintptr_t hp_objc_getTaggedPointerValue(id ptr) {
    uintptr_t value = hp_objc_decodeTaggedpointer(ptr);
    uintptr_t basicTag = 0x0;
#if TARGET_OS_IPHONE
    basicTag = value & 0x7;
#else
    basicTag = (value >> 1) & 0x7;
#endif
    //有extend
    if (basicTag == 0x7) {
    #if TARGET_OS_IPHONE
        return (value << 9) >> 12;
    #else
        return value >> 12;
    #endif
    } else {
    #if TARGET_OS_IPHONE
        return (value << 1) >> 4;
    #else
        return value >> 4;
    #endif
    }
}

2.1.1 x86 TaggedPointer 结构 (Mac)

NSString *str = [NSString stringWithFormat:@"hp"];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",str,str,str.class,hp_objc_decodeTaggedpointer(str),hp_objc_getTaggedPointerValue(str));

输出:

0x495f9cbcde9767b - hp - NSTaggedPointerString - decode value:0x706825, payload:0x70682

解码后的值为0x706825payload0x70682
payload解析如下:

image.png

同样的对于其它类型:

NSNumber *number = @6;
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",number,number,number.class,hp_objc_decodeTaggedpointer(number),hp_objc_getTaggedPointerValue(number));

NSDate *date = [NSDate date];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",date,date,date.class,hp_objc_decodeTaggedpointer(date),hp_objc_getTaggedPointerValue(date));

NSIndexPath *indexPath = [NSIndexPath indexPathWithIndex:3];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",indexPath,indexPath,indexPath.class,hp_objc_decodeTaggedpointer(indexPath),hp_objc_getTaggedPointerValue(indexPath));

输出:

0x9da3a6c50fc90087 - 6 - __NSCFNumber - decode value:0x627, payload:0x62
0xb094e947cc26e7fd - Thu Sep  9 15:33:44 2021 - __NSTaggedDate - decode value:0x2d374f82c3efe15d, payload:0x2d374f82c3efe15
0x9da3a6c50fc93649 -  {length = 1, path = 3} - NSIndexPath - decode value:0x30e9, payload:0x30e
image.png

2.1.2 arm64 TaggedPointer 结构(真机)

由于真机上面解码有一些额外操作,直接配置OBJC_DISABLE_TAG_OBFUSCATION更方便。

NSString *str = [NSString stringWithFormat:@"hp"];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",str,str,str.class,hp_objc_decodeTaggedpointer(str),hp_objc_getTaggedPointerValue(str));

输出:

 0x967c561218f83f2f - hp - NSTaggedPointerString - decode value:0xa000000000070682, payload:0x40000000000e0d0

解码后的值为0xa000000000070682payload0x40000000000e0d0
payload解析如下:

image.png

那么除了存储hp外还存储了额外信息。

同样的对于其它类型:

NSNumber *number = @6;
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",number,number,number.class,hp_objc_decodeTaggedpointer(number),hp_objc_getTaggedPointerValue(number));

NSDate *date = [NSDate date];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",date,date,date.class,hp_objc_decodeTaggedpointer(date),hp_objc_getTaggedPointerValue(date));

NSIndexPath *indexPath = [NSIndexPath indexPathForRow:1 inSection:2];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",indexPath,indexPath,indexPath.class,hp_objc_decodeTaggedpointer(indexPath),hp_objc_getTaggedPointerValue(indexPath));

输出:

0x8dbe59f3baa78095 - 6 - __NSCFNumber - decode value:0x8000000000000315, payload:0x62
0x9b25fe4764c5d200 - Thu Sep  9 15:20:23 2021 - __NSTaggedDate - decode value:0x969ba7b4de625180, payload:0x2d374f69bcc4a30
0x8dbe59f3bba79332 -  {length = 2, path = 2 - 1} - NSIndexPath - decode value:0x80000000010010b2, payload:0x200216
image.png

2.1.3 存储额外信息解析

上面分析payload的时候除了存储存储内容本身外,还有额外信息。
对于字符串

NSString *str = [NSString stringWithFormat:@"h"];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",str,str,str.class,hp_objc_decodeTaggedpointer(str),hp_objc_getTaggedPointerValue(str));

NSString *str1 = [NSString stringWithFormat:@"hp"];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",str1,str1,str1.class,hp_objc_decodeTaggedpointer(str1),hp_objc_getTaggedPointerValue(str1));

NSString *str2 = [NSString stringWithFormat:@"HotpotCat"];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",str2,str2,str2.class,hp_objc_decodeTaggedpointer(str2),hp_objc_getTaggedPointerValue(str2));

image.png

字符串除了存储字符串本身外,还存储了字符串长度,占用4位。

NSTaggedPointerString的存储有三种编码方式:ASCII码六位编码五位编码

  • ASCII码:除去第一位和最后一位,用8位的ascll码的话最多可以存储7个字符。字符串数目0~7之间。
  • 六位编码: 六位二进制编码,(144)/6=9.333…;最多存储9位字符。字符数目在8~9使用。
  • 五位编码:五位二进制编码,(144)/5 = 11.2;最多存储11位字符。字符数目在10~11使用。

当然编码也与字符类型有关,具体对应关系如下:


NSTaggedPointerString长度与范围对应关系

对于NSNumber:

NSNumber *number = @6;
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",number,number,number.class,hp_objc_decodeTaggedpointer(number),hp_objc_getTaggedPointerValue(number));

NSNumber *number1 = @(6.0);
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",number1,number1,number1.class,hp_objc_decodeTaggedpointer(number1),hp_objc_getTaggedPointerValue(number1));

image.png

0代表char1代表short2代表int3代表long4代表float5代表double

2.2 案例

@property (nonatomic, strong) NSString *nameStr;

- (void)taggedPointerTest {
    dispatch_queue_t  queue = dispatch_queue_create("com.hotpot.cn", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 10000; i++) {
        dispatch_async(queue, ^{
            self.nameStr = [NSString stringWithFormat:@"hotpot"];
             NSLog(@"%@",self.nameStr);
        });
    }
}

- (void)taggedPointerTest1 {
    dispatch_queue_t  queue = dispatch_queue_create("com.hotpot1.cn", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 10000; i++) {
        dispatch_async(queue, ^{
            //多线程的读和写,对新值的retain,旧值的release。有可能在这个过程中访问了release的值,操作了野指针。
            self.nameStr = [NSString stringWithFormat:@"hotpothotpothotpothotpothotpothotpot"];
            NSLog(@"%@",self.nameStr);
        });
    }
}

taggedPointerTest的调用不会发生crashtaggedPointerTest1有可能发生crash
taggedPointerTest1发生crash的原因是发生了多线程读和写,在setter过程中有对新值的retain,旧值的release。在这个过程中getter有可能访问了release的值,操作了野指针。
那么taggedPointerTest为什么不会发生crash呢?
因为在taggedPointerTest的过程中self.nameStrNSTaggedPointerString类型并不会发生retainrelease(直接返回)。

TaggedPointer retain & release

TaggedPointer 总结:

  • TaggedPointer专⻔用来存储小的对象,指针的值不再是地址了而是真正的值。实际上不再是一个对象,是一个披着对象皮的普通变量。
  • 内存并不存储在堆中,也不需要mallocfreeretainrelease直接返回)。
  • 在内存读取上有着3倍的效率,创建与释放比以前快106倍。
  • 为了安全进行了混淆,值存储和读取需要相应的编码和解码。
  • arm64真机上面TaggedPointer标记在最高位63tag0~2extended54~62位,其余位置存储真正的值payload
  • x86_64 TaggedPointer标记在最低位0tag1~3extended4~11,其余位置存储真正的值payload
  • payload并不仅仅只存储真正的值,还存储额外信息(4位)。对于字符串存储了字符串长度。对于NSNumber存储了类型。
    • 0char1short1int3long4float5double
    • 对于字符串通过stringWithFormat以及char *(包含拷贝以及传值)创建的字符串是NSTaggedPointerString类型。字面量方式创建的不是。
    • 是否是NSTaggedPointerString也与长度和编码方式有关。

你可能感兴趣的:(OC内存管理-内存分区、TaggedPointer)