你真的懂NSString吗

我们都知道 NSString是一个Objective-C的类,但是我们有时候发现它的对象在内存管理上貌似和其他对象有一些区别。由于这个类是@”Hello Wrod“的基础,所以往往忽略一些细节。让我们看看 NSString 里面一些特性

1. NSString 内存管理特性分析

1.1 准备

为了方便直观的体现差异,定义一个宏,打印 NSString 的 isa、内存地址、值、retainCount。
注:为了解内存特性,此代码用手动内存管理。

#define TLog(_var) ({ NSString *name = @#_var; NSLog(@"%@: %@ -> %p : %@  %d", name, [_var class], _var, _var, (int)[_var retainCount]); })

1.2 NSString 的代码创建

1.2.1 测试 NSString

在OC中,我们一般通过几种方法来创建 NSString 呢,一般有三种方法,现在我们就分别对这三种情况写段测试代码,如下:

NSString *str1 = @"1234567890";
TLog(str1);
// str1: __NSCFConstantString -> 0x10455e018 : 1234567890  -1
NSString *str2 = [NSString stringWithString:@"1234567890"];
TLog(str2);
//str2: __NSCFConstantString -> 0x10455e018 : 1234567890  -1
NSString *str3 = [NSString stringWithFormat:@"1234567890"];
TLog(str3);
// str3: __NSCFString -> 0x600000330fe0 : 1234567890  1
NSString *str4 = [[NSString alloc] initWithString:@"1234567890"];
TLog(str4);
// str4: __NSCFConstantString -> 0x10455e018 : 1234567890  -1
  • 第一、二、四种方式创建出来的 NSString时一模一样的,isa 是 __NSCFConstantString ,内存地址一样,retainCount 是-1。
  • 第三种方式创建的 NSString和创建其他OC对象类似的,在堆上分配内存,初始retainCount为1.

那么问题来了?
1、什么是 __NSCFConstantString
2、为什么会出现 retainCount 为-1?
3、为什么1、2、4这三个 NSString对象内存地址也一样?

1.2.2 NSString创建的写法

其实上面第一种写法和第二种写法是完全一样的,没有任何区别,从 iOS SDK6 开始,第二种写法已经被遗弃了,如果用第二种写法创建 NSString,编译器就会报方法过期的警告。

1.2.3 retainCount为-1是什么情况

首先retainCount是NSUInteger的类型,其实上面的打印是将它作为int类型打印。所以它其实不是-1,它的实际值是4294967295。

在OC的 retainCount 中.如果对象的 retainCount 为这个值,就意味着“无限的 retainCount ”,这个对象是不能被释放的。

所有的 __NSCFConstantString 对象的retainCount都为-1,这就意味着 __NSCFConstantString 不会被释放,使用第一种方法创建的 NSString,如果值一样,无论写多少遍,都是同一个对象。而且这种对象可以直接用 == 来比较。

assert(str1 == str2);      //一直正确
assert(@"abc" == @"abc");  //一直正确

1.3 NSString 的 retain、copy 和 mutableCopy

我们写一段代码分别对 __NSCFConstantString 和 __NSCFString 进行 retain 和 copy 测试

1.3.1 __NSCFConstantString

NSString *string = @"1234567890";
TLog(string);
// string: __NSCFConstantString -> 0x107bb9020 : 1234567890  -1
NSString *retainString = [string retain];
TLog(retainString);
// retainString: __NSCFConstantString -> 0x107bb9020 : 1234567890  -1
NSString *copyString = [string copy];
TLog(copyString);
// copyString: __NSCFConstantString -> 0x107bb9020 : 1234567890  -1
NSString *mutableCopyString = [string mutableCopy];
TLog(mutableCopyString);
// mutableCopyString: __NSCFString -> 0x6000003a3c00 : 1234567890  1

上面的测试可以看出,对一个__NSCFConstantString进行retain和copy操作都还是自己,没有任何变化,对其mutableCopy操作可将其拷贝到堆上,retainCount为1.

1.3.2 __NSCFString

NSString *string = [NSString stringWithFormat:@"1234567890"];
TLog(string);
// string: __NSCFString -> 0x600000a494e0 : 1234567890  1
NSString *retainString = [string retain];
TLog(retainString);
// retainString: __NSCFString -> 0x600000a494e0 : 1234567890  2
NSString *copyString = [string copy];
TLog(copyString);
// copyString: __NSCFString -> 0x600000a494e0 : 1234567890  3
NSString *mutableCopyString = [string mutableCopy];
TLog(mutableCopyString);
// mutableCopyString: __NSCFString -> 0x60000046ab80 : 1234567890  1

上面的测试中,我们发现,对__NSCFString进行retain和mutableCopy操作时,其特性符合正常的对象特性。但是对其copy时,它却变成了一个__NSCFConstantString对象!copy 会使原来的对象引用计数加一,并拷贝对象地址给新的指针。

mutableCopy 不会改变引用计数,会拷贝内容到堆上,生成一个 __NSCFString 对象,新对象的引用计数为1。

1.3.3 __NSTaggedPointerString

NSString *string = [NSString stringWithFormat:@"a"];
TLog(string);
// string: NSTaggedPointerString -> 0xb2590670e6f2b9aa : a  -1
NSString *retainString = [string retain];
TLog(retainString);
// retainString: NSTaggedPointerString -> 0xb2590670e6f2b9aa : a  -1
NSString *copyString = [string copy];
TLog(copyString);
// copyString: NSTaggedPointerString -> 0xb2590670e6f2b9aa : a  -1
NSString *mutableCopyString = [string mutableCopy];
TLog(mutableCopyString);
// mutableCopyString: __NSCFString -> 0x600002647cf0 : a  1

这回我们字符串定义成一个字符长度的的字符串“a”
我们发现怎么类型又变成 NSTaggedPointerString 。而且跟 NSCFConstantString 一样计数器都是-1。

2. 小结

结果是很复杂的,按照产生对象的isa大致可以分为三种情况:
产生的对象是 __NSCFConstantString
产生的对象是 __NSCFString
产生的对象是 __NSTaggedPointerString

__NSCFConstantStringNSTaggedPointerString 计数器为-1,因为 NSString 内部做了某种优化。

类型 引用计数
__NSCFString 1
NSTaggedPointerString、__NSCFConstantString -1

2.1 三种类型分别是什么,分别是在什么情况下产生的,分别处于内存的那个区域?

__NSCFConstantString
字符串常量,是一种编译时常量,它的 retainCount 值很大,是 4294967295,在控制台打印出的数值则是 18446744073709551615==2^64-1,测试证明,即便对其进行 release 操作,retainCount 也不会产生任何变化。是创建之后便是放不掉的对象。相同内容的 __NSCFConstantString 对象的地址相同,也就是说常量字符串对象是一种单例。
这种对象一般通过字面值 @"..."、CFSTR("...") 或者 stringWithString: 方法(需要说明的是,这个方法在 iOS6 SDK 中已经被称为redundant,使用这个方法会产生一条编译器警告。这个方法等同于字面值创建的方法)产生。
这种对象存储在字符串常量区。

__NSCFString
__NSCFConstantString 不同,__NSCFString对象是在运行时创建的一种 NSString子类,他并不是一种字符串常量。所以和其他的对象一样在被创建时获得了 1 的引用计数。
通过 NSString 的 stringWithFormat 等方法创建的 NSString 对象一般都是这种类型。
这种对象被存储在堆上。

__NSTaggedPointerString
理解这个类型,需要明白什么是标签指针,这是苹果在 64 位环境下对 NSString,NSNumber 等对象做的一些优化。简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中,因为在 64 位环境下指针变量的大小达到了 8 位足以容纳一些长度较小的内容。于是使用了标签指针这种方式来优化数据的存储方式。从他的引用计数可以看出,这货也是一个释放不掉的单例常量对象。在运行时根据实际情况创建。

对于 NSString 对象来讲,当非字面值常量的数字,英文字母字符串的长度小于等于 9 的时候会自动成为 NSTaggedPointerString 类型,如果有中文或其他特殊符号(可能是非 ASCII 字符)存在的话则会直接成为 )__NSCFString 类型。
这种对象被直接存储在指针的内容中,可以当作一种伪对象。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。