小谈NSString的内存分配
面试的时候有时候会随便问一句,判断两个NSString的字面量是否相同,为什么要用isEqualToString来判断,而不能用==来判断呢?
有些面试者对这个问题可能都没有想过,回答这是一个约定俗成;
而大多数面试者都会回到:因为==判断的是两个指针是否相等,而NSString是分配到堆上的,每次创建的时候,指针指向的地址的不同的,所以不能用==来判断。
然而这个结果仍然不能令人满意,或者说只是对了前一半,后面的一半有待商榷。我们知道,oc中我们创建的对象,确实大部分都是分配在堆上的,然而,NSString也是这样么?还是让我们敲敲看吧~
NSString *test1 = @"123";
NSString *test2 = @"123";
NSLog(@"%p %p", test1, test2);
打印结果:
MyTestProject[19053:533946] 0x103db9f50 0x103db9f50
调试:
(lldb) p test1==test2
(bool) $0 = true
我们可以看到test1和test2的内存地址是相同的。事实上,@"123"存在于常量存储区也就是_TEXT区,无论你创建、释放多少次,都不会被释放掉。如果你有兴趣打印下它的类型和retainCount,可以发现分别是__NSCFConstantString和1152921504606846975。事实上,所有的__NSCFConstantString类型的实例都是有无限的retainCount的。这就意味着所有的__NSCFConstantString都不会被释放。或者我们换一种写法来创建一个NSString:
NSString *test1 = [[NSString alloc] initWithString:@"123"];
NSString *test2 = @"123";
NSLog(@"%p %p", test1, test2);
这种写法的test1看起来像是新开辟了一块空间,然而我们会发现结果和上面还是一样的。虽然test1的这种写法已经被废弃掉了,但通过打印信息我们其实可以看到test1还是__NSCFConstantString的,和字面量的写法完全没有区别。
那么回到我们最初的问题,到底为什么NSString要使用isEqualToString呢?
让我们来试下其他的写法:
@property (nonatomic, copy) NSString *testStr;
NSString *test1 = [[NSString alloc] initWithString:@"123"];
NSString *test2 = @"123";
NSString *test3 = [NSString stringWithFormat:@"123"];
NSMutableString *test4 = [[NSMutableString alloc] initWithString:@"123"];
NSString *test5 = [test4 copy];
NSString *test6 = [NSString stringWithFormat:@"%@", @"123"];
self.testStr = [test2 copy];
让我们来猜下他们的内存地址,哪些是相同的呢?答案是test1、test2、self.testStr这三个是相同的,test3、test5、test6这三个是相同的,只有test4没有和它相同的。展示下我这的打印1~6以及self.testStr的内存地址的结果:
MyTestProject[19849:666990] 0x10bc0cf50 0x10bc0cf50 0xa000000003332313
0x608000268800 0xa000000003332313 0xa000000003332313 0x10bc0cf50
下面简单解释下:
- self.testStr只是对test2的一个浅拷贝,自然地址和2一样;
- 3,5,6的类型都是NSTaggedPointerString,4的类型是__NSCFString。3,5,6的字面量虽然和1、2一样的,但是类型其实是不同的。
- 上面打印的结果中可以看到3,5,6的地址位置非常高,那它们分配在哪个区呢?
- ** 另外需要注意的是:如果换成较长的字符串,3,5,6的类型也不是NSTaggedPointerString而是__NSCFString**
要研究明白为什么使用的是不同的类型,首先要清楚什么是NSTaggedPointerString,以及为什么直接用字面量赋值给NSString的时候,苹果不采用NSTaggedPointerString类型。就这个问题,其实【译】采用Tagged Pointer的字符串这里已经讲得很清楚了,这里我再赘述下。
比如如下代码:
NSString *a = @"a";
NSString *b = [[a mutableCopy] copy];
NSLog(@"%p %p", a, b);
运行后可以非常明显的看到:a是一个__NSCFConstantString,b是一个NSTaggedPointerString,自然有不同的内存地址。为什么字面量常量苹果不使用NSTaggedPointerString呢?
【译】采用Tagged Pointer的字符串中文版的 翻译有些晦涩,看了下英文版的描述比较易懂些:
although a string like @"a" could be stored as a tagged pointer, constant strings are never tagged pointers. Constant strings must remain binary compatible across OS releases, but the internal details of tagged pointers are not guaranteed.
原因是常量字符串需要在跨系统上保持二进制兼容,而 tagged pointers在技术上并不能保证这个。因此对于这种短的字符串字面量还是使用\ __NSCFConstantString类型。
下面一个问题,tagged pointers在内存上分配在哪个区?
其实如果我们仔细在XCode中多点两下,就可以看到其实tagged pointers是没有isa指针的,说明它根本不是一个对象。究其原因这个要说到tagged pointers是为什么被创造出来。
一般来说,对象所占内存是和CPU位数相关的。在32位的时候,比如一个NSNumber对象占用的空间是4(对象指针)+4(对象的值)=8字节,升级到64位的时候,逻辑不变的话,占用的空间直接翻倍,变成8+8=16字节,这样会产生十分严重的效率问题:为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。
在查找资料的过程中也发现了苹果官方的明确说法(摘自深入理解Tagged Pointer):
我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:
- Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
- Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
- 在内存读取上有着3倍的效率,创建时比以前快106倍。
由此看来,NSTaggedPointerString根本不是对象,是分配在栈区的。
本来只是想说下为什么不用==的,不知不觉深究了这么多,对自己理解也是个梳理和提升,屡清了之前很多的盲点。现在总算是明白了,NSString真的是很复杂的东西,可能分配在栈区、堆区、常量区,虽然日常中我们基本上可以无视这些区别,看似没有什么用,然而对自己来说,对做技术来说,多较些真,这样才能走的更远吧。
所以,回到最初的问题,我们需要怎么回答,如何判断两个字符串是否相等呢 (=@__@=)
相关文章:
深入理解Tagged Pointer
【译】采用Tagged Pointer的字符串