NSString內(nèi)存詳解

內(nèi)存分配

image-20210903112329035.png
image-20210903112409969.png

我們可以看到string1和string2的內(nèi)存地址是相同的。事實上,@"11"存在于常量存儲區(qū),無論你創(chuàng)建、釋放多少次,都不會被釋放掉。如果你有興趣打印下它的類型和retainCount,可以發(fā)現(xiàn)分別是__NSCFConstantString和1152921504606846975。
事實上,所有的__NSCFConstantString類型的實例都是有無限的retainCount的。這就意味著所有的__NSCFConstantString都不會被釋放。

image-20210903154022082.png
image-20210903155143196.png
  • 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(引用唐巧博客)

在 2013 年 9 月,蘋果推出了 iPhone5s ,與此同時,iPhone5s 配備了首個采用 64 位架構(gòu)的 A7 雙核處理器,為了節(jié)省內(nèi)存和提高執(zhí)行效率,蘋果提出了Tagged Pointer的概念。對于 64 位程序,引入 Tagged Pointer 后,相關(guān)邏輯能減少一半的內(nèi)存占用,以及 3 倍的訪問速度提升,100 倍的創(chuàng)建、銷毀速度提升。

我們先看看原有的對象為什么會浪費內(nèi)存。假設(shè)我們要存儲一個 NSNumber 對象,其值是一個整數(shù)。正常情況下,如果這個整數(shù)只是一個 NSInteger 的普通變量,那么它所占用的內(nèi)存是與 CPU 的位數(shù)有關(guān),在 32 位 CPU 下占 4 個字節(jié),在 64 位 CPU 下是占 8 個字節(jié)的。而指針類型的大小通常也是與 CPU 位數(shù)相關(guān),一個指針?biāo)加玫膬?nèi)存在 32 位 CPU 下為 4 個字節(jié),在 64 位 CPU 下也是 8 個字節(jié)。

所以一個普通的 iOS 程序,如果沒有Tagged Pointer對象,從 32 位機器遷移到 64 位機器中后,雖然邏輯沒有任何變化,但這種 NSNumber、NSDate 一類的對象所占用的內(nèi)存會翻倍。如下圖所示:

image.png

我們再來看看效率上的問題,為了存儲和訪問一個 NSNumber 對象,我們需要在堆上為其分配內(nèi)存,另外還要維護它的引用計數(shù),管理它的生命期。這些都給程序增加了額外的邏輯,造成運行效率上的損失。

為了改進上面提到的內(nèi)存占用和效率問題,蘋果提出了Tagged Pointer對象。由于 NSNumber、NSDate 一類的變量本身的值需要占用的內(nèi)存大小常常不需要 8 個字節(jié),拿整數(shù)來說,4 個字節(jié)所能表示的有符號整數(shù)就可以達到 20 多億(注:2^31=2147483648,另外 1 位作為符號位),對于絕大多數(shù)情況都是可以處理的。

所以我們可以將一個對象的指針拆成兩部分,一部分直接保存數(shù)據(jù),另一部分作為特殊標(biāo)記,表示這是一個特別的指針,不指向任何一個地址。所以,引入了Tagged Pointer對象之后,64 位 CPU 下 NSNumber 的內(nèi)存圖變成了以下這樣:

image.png

對此,我們也可以用 Xcode 做實驗來驗證。我們的實驗代碼如下:

image-20210903155741678.png

可見,蘋果確實是將值直接存儲到了指針本身里面。我們還可以猜測,數(shù)字最末尾的 2 以及最開頭的 0xb 是否就是蘋果對于Tagged Pointer的特殊標(biāo)記呢?我們嘗試放一個 8 字節(jié)的長的整數(shù)到NSNumber實例中,對于這樣的實例,由于Tagged Pointer無法將其按上面的壓縮方式來保存,那么應(yīng)該就會以普通對象的方式來保存,我們的實驗代碼如下:
1.png

可見,當(dāng) 8 字節(jié)可以承載用于表示的數(shù)值時,系統(tǒng)就會以Tagged Pointer的方式生成指針,如果 8 字節(jié)承載不了時,則又用以前的方式來生成普通的指針。關(guān)于以上關(guān)于Tag Pointer的存儲細節(jié),我們也可以在這里找到相應(yīng)的討論,但是其中關(guān)于Tagged Pointer的實現(xiàn)細節(jié)與我們的實驗并不相符,筆者認(rèn)為可能是蘋果更改了具體的實現(xiàn)細節(jié),并且這并不影響Tagged Pointer我們討論Tagged Pointer本身的優(yōu)點。

NSTaggedPointerString

2.png

image-20210918172942535.png

由上可知,NSTaggedPointerString是以0xa開頭,中間用特殊方式記錄值,字符串長度作為末尾數(shù)字的一串地址。如果字符串為純數(shù)字,就直接用3X的方式表示,比如test2的值為8,那么值就表示為38再加上后面的字符串長度1就是0xa000000000000381。如果字符串為字符,就以ASCII碼作為值存儲,比如test1。

蘋果對于Tagged Pointer特點的介紹:

  1. Tagged Pointer專門用來存儲小的對象,例如NSNumberNSDate
  2. Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已。所以,它的內(nèi)存并不存儲在堆中,也不需要 malloc 和 free。
  3. 在內(nèi)存讀取上有著 3 倍的效率,創(chuàng)建時比以前快 106 倍。

由此可見,蘋果引入Tagged Pointer,不但減少了 64 位機器下程序的內(nèi)存占用,還提高了運行效率。完美地解決了小內(nèi)存對象在存儲和訪問效率上的問題。

蘋果將Tagged Pointer引入,給 64 位系統(tǒng)帶來了內(nèi)存的節(jié)省和運行效率的提高。Tagged Pointer通過在其最后一個 bit 位設(shè)置一個特殊標(biāo)記,用于將數(shù)據(jù)直接保存在指針本身中。因為Tagged Pointer并不是真正的對象,我們在使用時需要注意不要直接訪問其 isa 變量。



NSString用copy還是strong修飾?

@property (nonatomic, strong) NSString *strongStr;
@property (nonatomic, copy)   NSString *copyedStr;
//不可變字符串賦值
- (void)testString {
    NSString *string = [NSString stringWithFormat:@"lalala"];
    self.strongStr = string;
    self.copyedStr = string;
        
    NSLog(@"origin string: %p, %p", string, &string);
    NSLog(@"strong string: %p, %p", _strongStr, &_strongStr);
    NSLog(@"copyed string: %p, %p", _copyedStr, &_copyedStr);
}
3.png

這種情況下,不管是strong還是copy屬性的對象,其指向的地址都是同一個,即為string指向的地址。

//可變字符串賦值
- (void)testMutbleString {
    NSMutableString *mutbleString = [NSMutableString stringWithFormat:@"hahaha"];
    self.strongStr = mutbleString;
    self.copyedStr = mutbleString;
    
//    [mutbleString appendString:@" wawawa"];
    
    NSLog(@"mut origin string: %p, %p", mutbleString, &mutbleString);
    NSLog(@"mut strong string: %p, %p", _strongStr, &_strongStr);
    NSLog(@"mut copyed string: %p, %p", _copyedStr, &_copyedStr);
}
4.png

此時copy屬性字符串已不再指向string字符串對象,而是深拷貝了string字符串,并讓_copyedStr對象指向這個字符串

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容