iOS刨根问底栏目组iOS DeveloperiOS 深度好文

小谈NSString的内存分配

2017-04-12  本文已影响1022人  PepperCurry

面试的时候有时候会随便问一句,判断两个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

下面简单解释下:

要研究明白为什么使用的是不同的类型,首先要清楚什么是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

由此看来,NSTaggedPointerString根本不是对象,是分配在栈区的。

本来只是想说下为什么不用==的,不知不觉深究了这么多,对自己理解也是个梳理和提升,屡清了之前很多的盲点。现在总算是明白了,NSString真的是很复杂的东西,可能分配在栈区、堆区、常量区,虽然日常中我们基本上可以无视这些区别,看似没有什么用,然而对自己来说,对做技术来说,多较些真,这样才能走的更远吧。

所以,回到最初的问题,我们需要怎么回答,如何判断两个字符串是否相等呢 (=@__@=)
相关文章:
深入理解Tagged Pointer
【译】采用Tagged Pointer的字符串

上一篇下一篇

猜你喜欢

热点阅读