面試的時候有時候會隨便問一句,判斷兩個NSString的字面量是否相同,為什么要用isEqualToString來判斷,而不能用==來判斷呢?
有些面試者對這個問題可能都沒有想過,回答這是一個約定俗成;
而大多數(shù)面試者都會回到:因為==判斷的是兩個指針是否相等,而NSString是分配到堆上的,每次創(chuàng)建的時候,指針指向的地址的不同的,所以不能用==來判斷。
然而這個結(jié)果仍然不能令人滿意,或者說只是對了前一半,后面的一半有待商榷。我們知道,oc中我們創(chuàng)建的對象,確實大部分都是分配在堆上的,然而,NSString也是這樣么?還是讓我們敲敲看吧~
NSString *test1 = @"123";
NSString *test2 = @"123";
NSLog(@"%p %p", test1, test2);
打印結(jié)果:
zbcDemo[36100:5525616] 0x102226df0 0x102226df0
調(diào)試:
(lldb) p test1==test2
(bool) $0 = true
我們可以看到test1和test2的內(nèi)存地址是相同的。事實上,@"123"存在于常量存儲區(qū)也就是_TEXT區(qū),無論你創(chuàng)建、釋放多少次,都不會被釋放掉。
如果你有興趣打印下它的類型和retainCount,可以發(fā)現(xiàn)分別是__NSCFConstantString和1152921504606846975。
事實上,所有的__NSCFConstantString類型的實例都是有無限的retainCount的。這就意味著所有的__NSCFConstantString都不會被釋放。
或者我們換一種寫法來創(chuàng)建一個NSString:
NSString *test1 = [[NSString alloc] initWithString:@"123"];
NSString *test2 = @"123";
NSLog(@"%p %p", test1, test2);
打印結(jié)果:
[36206:5529499] 0x100802df0 0x100802df0
這種寫法的test1看起來像是新開辟了一塊空間,然而我們會發(fā)現(xiàn)結(jié)果和上面還是一樣的。雖然test1的這種寫法已經(jīng)被廢棄掉了,但通過打印信息我們其實可以看到test1還是__NSCFConstantString的,和字面量的寫法完全沒有區(qū)別。
那么回到我們最初的問題,到底為什么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);
打印結(jié)果:
zbcDemo[36297:5532377] 0x102ae2df0 0x102ae2df0
0x9a59d631d155838a 0x281287060 0x9a59d631d155838a
0x9a59d631d155838a 0x102ae2df0
讓我們來猜下他們的內(nèi)存地址,哪些是相同的呢?答案是test1、test2、self.testStr這三個是相同的,test3、test5、test6這三個是相同的,只有test4沒有和它相同的。
下面簡單解釋下:查看圖片
- self.testStr只是對test2的一個淺拷貝,自然地址和2一樣;
- 3,5,6的類型都是NSTaggedPointerString,4的類型是__NSCFString。3,5,6的字面量雖然和1、2一樣的,但是類型其實是不同的。
- 上面打印的結(jié)果中可以看到3,5,6的地址位置非常高,那它們分配在哪個區(qū)呢?
- ** 另外需要注意的是:如果換成較長的字符串,3,5,6的類型也不是NSTaggedPointerString而是__NSCFString**
要研究明白為什么使用的是不同的類型,首先要清楚什么是NSTaggedPointerString,以及為什么直接用字面量賦值給NSString的時候,蘋果不采用NSTaggedPointerString類型。就這個問題,其實【譯】采用Tagged Pointer的字符串這里已經(jīng)講得很清楚了,這里我再贅述下。
比如如下代碼:
NSString *a = @"a";
NSString *b = [[a mutableCopy] copy];
NSLog(@"%p %p", a, b);
運行后可以非常明顯的看到:a是一個__NSCFConstantString,b是一個NSTaggedPointerString,自然有不同的內(nèi)存地址。為什么字面量常量蘋果不使用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.
原因是常量字符串需要在跨系統(tǒng)上保持二進制兼容,而 tagged pointers在技術(shù)上并不能保證這個。因此對于這種短的字符串字面量還是使用\ __NSCFConstantString類型。
下面一個問題,tagged pointers在內(nèi)存上分配在哪個區(qū)?點擊查看圖片
其實如果我們仔細在XCode中多點兩下,就可以看到其實tagged pointers是沒有isa指針的,說明它根本不是一個對象。究其原因這個要說到tagged pointers是為什么被創(chuàng)造出來。
一般來說,對象所占內(nèi)存是和CPU位數(shù)相關(guān)的。在32位的時候,比如一個NSNumber對象占用的空間是4(對象指針)+4(對象的值)=8字節(jié),升級到64位的時候,邏輯不變的話,占用的空間直接翻倍,變成8+8=16字節(jié),這樣會產(chǎn)生十分嚴重的效率問題:為了存儲和訪問一個NSNumber對象,我們需要在堆上為其分配內(nèi)存,另外還要維護它的引用計數(shù),管理它的生命期。這些都給程序增加了額外的邏輯,造成運行效率上的損失。
在查找資料的過程中也發(fā)現(xiàn)了蘋果官方的明確說法(摘自深入理解Tagged Pointer):
我們也可以在WWDC2013的《Session 404 Advanced in Objective-C》視頻中,看到蘋果對于Tagged Pointer特點的介紹:
- Tagged Pointer專門用來存儲小的對象,例如NSNumber和NSDate
- Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已。所以,它的內(nèi)存并不存儲在堆中,也不需要malloc和free。
- 在內(nèi)存讀取上有著3倍的效率,創(chuàng)建時比以前快106倍。
由此看來,NSTaggedPointerString根本不是對象,是分配在棧區(qū)的。
本來只是想說下為什么不用==的,不知不覺深究了這么多,對自己理解也是個梳理和提升,屢清了之前很多的盲點。現(xiàn)在總算是明白了,NSString真的是很復(fù)雜的東西,可能分配在棧區(qū)、堆區(qū)、常量區(qū),雖然日常中我們基本上可以無視這些區(qū)別,看似沒有什么用,然而對自己來說,對做技術(shù)來說,多較些真,這樣才能走的更遠吧。
所以,回到最初的問題,我們需要怎么回答,如何判斷兩個字符串是否相等呢 (=@__@=)
相關(guān)文章:
深入理解Tagged Pointer
【譯】采用Tagged Pointer的字符串