iOS NSString的内存分配

面试的时候有时候会随便问一句,判断两个NSString的字面量是否相同,为什么要用isEqualToString来判断,而不能用==来判断呢?
有些面试者对这个问题可能都没有想过,回答这是一个约定俗成;
而大多数面试者都会回到:因为==判断的是两个指针是否相等,而NSString是分配到堆上的,每次创建的时候,指针指向的地址的不同的,所以不能用==来判断。

然而这个结果仍然不能令人满意,或者说只是对了前一半,后面的一半有待商榷。我们知道,oc中我们创建的对象,确实大部分都是分配在堆上的,然而,NSString也是这样么?还是让我们敲敲看吧~

    NSString *test1 = @"123";
    NSString *test2 = @"123";
    
    NSLog(@"%p  %p", test1, test2);

    打印结果:
    zbcDemo[36100:5525616] 0x102226df0  0x102226df0
    调试:
    (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);
打印结果:
[36206:5529499] 0x100802df0  0x100802df0

这种写法的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];
   
    NSLog(@"%p  %p  %p  %p  %p  %p", test1, test2, test3, test4, test5, test6);
    打印结果:
    zbcDemo[36297:5532377] 0x102ae2df0  0x102ae2df0 
             0x9a59d631d155838a  0x281287060  0x9a59d631d155838a  
              0x9a59d631d155838a  0x102ae2df0

让我们来猜下他们的内存地址,哪些是相同的呢?答案是test1、test2、self.testStr这三个是相同的,test3、test5、test6这三个是相同的,只有test4没有和它相同的。

下面简单解释下:查看图片

  • 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的字符串

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,000评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,745评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,561评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,782评论 1 298
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,798评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,394评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,952评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,852评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,409评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,483评论 3 341
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,615评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,303评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,979评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,470评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,571评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,041评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,630评论 2 359

推荐阅读更多精彩内容