NSString有哪些创建对象的方法?创建的对象分别存储在什么区域?
一般通过NSString创建对象的方法有:
NSString *string1 = @"123";
NSString *string2 = [[NSString alloc] initWithString:@"123"];
NSString *string3 = [NSString stringWithFormat:@"123"];
NSString *string4 = [[NSString alloc] initWithFormat:@"123"];
这里面涉及到几个问题:
创建的对象存储在什么区?常量区?堆区?还是小对象?
因此,我们做打印,看结果分析:
//目的:看看字符串长度短的情况下
NSString *string0 = @"sun";//常量区
//目的:看看字符串长度很长的情况下
NSString *string1 = @"sundaopjd2221";//常量区NSString *string2 = [[NSString alloc] init];//常量区
NSString *string3 = [[NSString alloc] initWithString:@"sun"];//常量区
//目的:看看字符串长度短的情况下,下一个存储空间地址是啥
NSString *string4 = [[NSString alloc] initWithString:@"sun2"];//常量区
//目的:看看字符串长度很长的情况下,有没有变化
NSString *string5 = [[NSString alloc] initWithString:@"sundaopjd2221"];//常量区//目的:看看字符串长度短的情况下,存储空间地址
NSString *string6 = [NSString stringWithFormat:@"1"];//小对象
//目的:看看字符串长度短的情况下,下一个存储空间地址是啥
NSString *string7 = [NSString stringWithFormat:@"2"];//小对象
//目的:看看字符串长度很长的情况下,有没有变化
NSString *string8 = [NSString stringWithFormat:@"sundaopjd2221"];//存储在堆
//目的:观察堆空间的变化
NSString *string9 = [NSString stringWithFormat:@"sundaopjd222122"];//存储在堆//目的:看看字符串长度短的情况下,存储空间地址
NSString *string10 = [[NSString alloc] initWithFormat:@"1"];//小对象
//目的:看看字符串长度短的情况下,下一个存储空间地址是啥
NSString *string11 = [[NSString alloc] initWithFormat:@"3"];//小对象
//目的:看看字符串长度很长的情况下,有没有变化
NSString *string12 = [[NSString alloc] initWithFormat:@"sundaopjd2221"];//存储在堆
//目的:观察堆空间的变化
NSString *string13 = [[NSString alloc] initWithFormat:@"sundaopjd2221222"];//存储在堆//这个是指针本身的存储地址(栈)
NSLog(@"%p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p", &string0, &string1, &string2, &string3, &string4, &string5, &string6, &string7, &string8, &string9, &string10, &string11, &string12, &string13);//看看string的类型
NSLog(@"%@, %@, %@, %@, %@, %@, %@, %@, %@, %@, %@, %@, %@, %@", [string0 class], [string1 class], [string2 class], [string3 class], [string4 class], [string5 class], [string6 class], [string7 class], [string8 class], [string9 class], [string10 class], [string11 class], [string12 class], [string13 class]);//这个是对象存储的地址
NSLog(@"%p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p", string0, string1, string2, string3, string4, string5, string6, string7, string8, string9, string10, string11, string12, string13);//看看引用计数器个数
NSLog(@"%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d", [string0 retainCount], [string1 retainCount], [string2 retainCount], [string3 retainCount], [string4 retainCount], [string5 retainCount], [string6 retainCount], [string7 retainCount], [string8 retainCount], [string9 retainCount], [string10 retainCount], [string11 retainCount], [string12 retainCount], [string13 retainCount]);打印结果:
0x7ff7bfeff290, 0x7ff7bfeff288, 0x7ff7bfeff280, 0x7ff7bfeff278, 0x7ff7bfeff270, 0x7ff7bfeff268, 0x7ff7bfeff260, 0x7ff7bfeff258, 0x7ff7bfeff250, 0x7ff7bfeff248, 0x7ff7bfeff240, 0x7ff7bfeff238, 0x7ff7bfeff230, 0x7ff7bfeff228__NSCFConstantString, __NSCFConstantString, __NSCFConstantString, __NSCFConstantString, __NSCFConstantString, __NSCFConstantString, NSTaggedPointerString, NSTaggedPointerString, __NSCFString, __NSCFString, NSTaggedPointerString, NSTaggedPointerString, __NSCFString, __NSCFString//混淆关闭前
//0x100004038, 0x100004058, 0x7ff846c7c2d0, 0x100004038, 0x100004078, 0x100004058, 0x9329a7a56318529b, 0x9329a7a56318519b, 0x600000207100, 0x600000c04ba0, 0x9329a7a56318529b, 0x9329a7a56318509b, 0x600000207120, 0x600000c04bd0
//混淆关闭后
0x100004038, 0x100004058, 0x7ff846c7c2d0, 0x100004038, 0x100004078, 0x100004058, 0x3115, 0x3215, 0x600000209340, 0x600000c0cba0, 0x3115, 0x3215, 0x3315, 0x600000c0cbd0-1, -1, -1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, 1
分析:
通过打印[string class]
获取的类型:__NSCFConstantString
,NSTaggedPointerString
,__NSCFString
可以知道创建的对象分别存储在什么区:常量区、小对象、堆区
通过打印[string retainCount]
获取引用计数器:-1,1的结果,可以区分:
-1:常量区或小对象
1:堆区
问:为啥retainCount会为-1?
这是因为retainCount是一个无符号整型typedef unsigned long NSUInteger;
因此,-1并不代表是真正的引用计数器的值
在iOS中,遇到retainCount
返回-1(或者在某些情况下是UINT_MAX
,因为retainCount
返回的是NSUInteger
,一个无符号整型)通常是指对象被标记为不可释放或具有“无限”的引用计数。这种情况通常发生在几种特殊对象上,这些对象由于各种原因,被系统视为“永久存活”的,直到应用程序结束。这样的对象不参与常规的引用计数管理,也就是说,无论对它们进行多少次retain
或release
操作,它们都不会被销毁。
这里的-1或UINT_MAX
并不表示实际的引用计数值,而是一个标志,表明这个对象是由系统以特殊方式管理的。对于开发者来说,这意味着这类对象的内存管理并不需要(也不应该)手动干预。
几个典型的例子包括:
- 单例对象:如
[UIApplication sharedApplication]
或[NSFileManager defaultManager]
,这些单例在应用程序的整个生命周期内都应该保持活跃状态。 - 字符串字面量:如
NSString *str = @"constant string";
,这些字符串在编译时被创建,并存储在应用程序的二进制文件中。它们在整个应用程序的生命周期内都是有效的,因此被认为是具有“无限”引用计数的。 - 标准框架对象:某些由系统框架创建并管理的对象,可能也会被标记为具有“无限”引用计数。
结论:
- 通过
NSString *str = @"1"
和[NSString alloc] initWithString:@"1"]
创建的对象一致,都是存储在常量区
虽然有initWithString,有init,但也是放在常量区,不增加引用计数器
- 通过
[NSString stringWithFormat:@"2"];
和[[NSString alloc] initWithFormat:@"2"];
创建的对象一致,当字符串长度小的时候(小于10)都是小对象,字符串长度过长的时候(大于等于10)存储在堆区域
//9个数
NSString *string11 = [[NSString alloc] initWithFormat:@"123456789"];
//10个数
NSString *string12 = [[NSString alloc] initWithFormat:@"1234567890"];
打印类型结果:
NSTaggedPointerString, __NSCFString
- 小对象的引用,与小对象本身的地址不一致,即
NSString *string6 = [NSString stringWithFormat:@"1"];
string6
的地址,与[NSString stringWithFormat:@"1"];
的地址不一致。
因为string6
是在栈上的,然后string6
内部有一部分特定区域,存放小对象"1"
猜测:小对象存储在栈区
在现在的版本中,为了保证数据安全,苹果对 Tagged Pointer 做了数据混淆,开发者通过打印指针无法判断它是不是一个Tagged Pointer,更无法读取Tagged Pointer的存储数据。
所以在分析Tagged Pointer之前,我们需要先关闭Tagged Pointer的数据混淆,以方便我们调试程序。通过设置环境变量OBJC_DISABLE_TAG_OBFUSCATION为YES。
iOS - 老生常谈内存管理(五):Tagged Pointer