NSString的内存管理

在 Objective-C 的 Fondation 框架中 NSString 对象是很复杂的存在,各种方式创建以及不同长度的字符串都会影响 NSString 对象在内存中所处的位置。Objective-C 在运行时也对其做了很多优化。今天就来研究一下 NSString 这个复杂的对象。

构建一些测试代码:

为了观察 NSString 的内存管理情况,我选择关闭 ARC 使用 MRC 来进行测试。以观察其引用计数等状况。

先写一个 Log 宏。

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

NSString揭秘

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
NSString *str1 = @"sa";
TLog(str1);
//str1: __NSCFConstantString -> 0x100001050 : sa 18446744073709551615

NSString *str2 = [NSString stringWithString:@"sa"];
TLog(str2);
//str2: __NSCFConstantString -> 0x100001050 : sa 18446744073709551615


NSString *str3 = @"1234567890";
TLog(str3);
//str3: __NSCFConstantString -> 0x100001110 : 1234567890 18446744073709551615

NSString *str4 = [NSString stringWithFormat:@"sa"];
TLog(str4);
//str4: NSTaggedPointerString -> 0x617325 : sa 18446744073709551615

NSString *str5 = [NSString stringWithFormat:@"sa"];
TLog(str5);
//str5: NSTaggedPointerString -> 0x617325 : sa 18446744073709551615


NSString *str6 = [NSString stringWithFormat:@"123456789"];
TLog(str6);
//str6: NSTaggedPointerString -> 0x1ea1f72bb30ab195 : 123456789 18446744073709551615

NSString *str7 = [NSString stringWithFormat:@"1234567890"];
TLog(str7);
//str7: __NSCFString -> 0x100300800 : 1234567890 1

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

  1. 产生的对象是 __NSCFConstantString
  2. 产生的对象是 __NSCFString
  3. 产生的对象是 NSTaggedPointerString

而且可以看到,在 MRC 下的引用计数也是不尽相同的:

引用计数 类型
1 __NSCFString
18446744073709551615(2^64-1) NSTaggedPointerString, __NSCFConstantString

这样的话就提出了几个疑问:

  • 三种类型分别是什么,有什么不同?
  • 三种类型的字符串指针分别是在什么情况下产生的?
  • 三种类型的字符串分别处于内存的那个区域?
  • 引用计数为什么会是18446744073709551615?

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

__NSCFConstantString

字符串常量,是一种编译时常量,它的 retainCount 值很大,是 4294967295,在控制台打印出的数值则是 18446744073709551615==2^64-1,测试证明,即便对其进行 release 操作,retainCount 也不会产生任何变化。是创建之后便是放不掉的对象。相同内容的 __NSCFConstantString 对象的地址相同,也就是说常量字符串对象是一种单例。

这种对象一般通过字面值 @"..."CFSTR("...") 或者 stringWithString: 方法(需要说明的是,这个方法在 iOS6 SDK 中已经被称为redundant,使用这个方法会产生一条编译器警告。这个方法等同于字面值创建的方法)产生。

这种对象存储在字符串常量区。

__NSCFString

__NSCFConstantString 不同, __NSCFString 对象是在运行时创建的一种 NSString 子类,他并不是一种字符串常量。所以和其他的对象一样在被创建时获得了 1 的引用计数。

通过 NSStringstringWithFormat 等方法创建的 NSString 对象一般都是这种类型。

这种对象被存储在堆上。

NSTaggedPointerString

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

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

这种对象被直接存储在指针的内容中,可以当作一种伪对象。

0x01 引用计数为什么会是18446744073709551615?

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

分析NSString的 copy,retain,mutableCopy表现

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
NSString *testOutput;

NSString *str9 = @"as";

TLog(str9);
TLog(str9);
[str9 retain];
TLog(str9);
NSString *str = [str9 copy];
TLog(str);
TLog(str9);
str = [str9 mutableCopy];
TLog(str);
TLog(str9);

NSString *str10 = [NSString stringWithFormat:@"as"];
TLog(str10);
[str10 retain];
TLog(str10);
str = [str10 copy];
TLog(str);
TLog(str10);
str = [str10 mutableCopy];
TLog(str);
TLog(str10);

NSString *str11 = [NSString stringWithFormat:@"1234567890"];

TLog(str11);
[str11 retain];
TLog(str11);
str = [str11 copy];
TLog(str);
TLog(str11);
str =[str11 mutableCopy];
TLog(str);
TLog(str11);

实验证明:copy 会使原来的对象引用计数加一,并拷贝对象地址给新的指针。
mutableCopy 不会改变引用计数,会拷贝内容到堆上,生成一个 __NSCFString 对象,新对象的引用计数为1.