NSString 在 OC 的內存管理策略中是一個特殊的存在,因為其在編譯和運行中做了一些優化處理,不同于普通對象的存在,看看下面代碼打印情況(此代碼測試環境為MRC,可以在ARC工程下,設置當前測試代碼文件為手動內存管理,找到路徑Build Phases---->Compile Sources---->XXX.m,將XXX.m的Compiler Flags設置為"-fno-objc-arc".):
//宏定義
#define XWLog(_var) ({ NSString *name = @#_var; NSLog(@"變量名=%@,類型=%@, 地址=%p,值=%@,引用計數=%d", name, [_var class], _var, _var, (int)[_var retainCount]); })
//測試代碼
NSString *a = @"string";
NSString *b = [[NSString alloc]init];
NSString *c = [[NSString alloc]initWithString:@"string"];
NSString *d = [[NSString alloc]initWithFormat:@"string"];
NSString *e = [NSString stringWithFormat:@"string"];
NSString *f = [NSString stringWithFormat:@"123456789"];
NSString *g = [NSString stringWithFormat:@"1234567890"];
XWLog(a); XWLog(b); XWLog(c); XWLog(d); XWLog(e); XWLog(f); XWLog(g);
//打印結果
變量名=a,類型=__NSCFConstantString, 地址=0x1015f3120,值=string,引用計數=-1
變量名=b,類型=__NSCFConstantString, 地址=0x1019808d0,值=,引用計數=-1
變量名=c,類型=__NSCFConstantString, 地址=0x1015f3120,值=string,引用計數=-1
變量名=d,類型=NSTaggedPointerString, 地址=0xa00676e697274736,值=string,引用計數=-1
變量名=e,類型=NSTaggedPointerString, 地址=0xa00676e697274736,值=string,引用計數=-1
變量名=f,類型=NSTaggedPointerString, 地址=0xa1ea1f72bb30ab19,值=123456789,引用計數=-1
變量名=g,類型=__NSCFString, 地址=0x60800002b580,值=1234567890,引用計數=1
從打印結果看出,變量 b 到 f 的引用計數為-1
,若是無符號格式輸出,應該是一個很大的數字,與我們理解的對象初始化后引用計數為 1
所不同,不同的創建方式,字符串的類型不同,引用計數也有區別,創建的字符串有三種類型:
__NSCFConstantString
__NSCFString
NSTaggedPointerString
造成這種情況是由于 OC 對 NSString 的內存優化產生的。
- __NSCFConstantString
從字面就可以看出,這是一個常量字符串,該類型的字符串是以字面量創建的,是在編譯期創建的,保存在常量區。通過 a 與 c 的打印結果看出,當創建的字符串變量值在常量區存在時,變量會指向那個字符串,這是編譯期做的優化,c 指向同一字符串 @"string",地址與a相同。
文字常量區存放常量字符串,程序結束后由系統釋放,也就是說指向常量表的指針不受引用計數管理。所以對于
NSCFConstantString
類型的變量,OC 的內存管理策略對其無效。
__NSCFString
表示這是一個對象類型的字符串,在運行時創建,存儲在堆區,服從OC 的對象內存管理策略。該類型的字符串由Format
創建,無論是實例方法還是類方法且其長度不能太小(內容若包含中文字符,不論長度大小,都是NSCFString),否則創建的是NSTaggedPointerString
類型,例如上例的變量 f 與 g。NSTaggedPointerString
對于64位程序,為了節省內存和提高運行速度,蘋果引入了 Tagged Point 技術。NSTaggedPointerString
是對NSCFString
優化后的存在,在運行時創建時對字符串的內容和長度做出判斷,若字符串內容是由ASCII字符
構成且長度較小(大概十個字符以內),這時候創建的字符串就是NSTaggedPointerString
類型,字符串直接存儲在指針里,引用計數同樣為-1,不適用對象的內存管理策略。
Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已。所以,它的內存并不存儲在堆中,OC 對象的內存管理方式對其無效。
現在,我們在看看這道題:
在MRC下,會不會造成內存泄漏?會不會奔潰?
NSString *str = [[NSString alloc] initWithString:@"ABC"];
str = @"123";
[str release];
NSLog(@"%@".str);
一目了然,str指向字符串常量,對象的內存管理方式對其無效,程序結束時,系統才會銷毀常量區的值。所以不會造成內存泄漏更不會奔潰。
接下來我們再看看NSArray的平時注意不到的問題:
NSArray *a1 = @[@"1",@"2"];
NSArray *a2 = [[NSArray alloc]init];
NSArray *a3 = [[NSArray alloc]initWithObjects:@"1", nil];
NSArray *a4 = [[NSArray alloc]initWithArray:@[@"a",@"b"]];
NSArray *a5 = [NSArray arrayWithObjects:@"m",@"n", nil];
NSArray *a6 = [[NSArray alloc]init];
NSArray *a7 = @[];
XWLog(a1); XWLog(a2); XWLog(a3); XWLog(a4); XWLog(a5); XWLog(a6);XWLog(a7);
打印結果:
變量名=a1,類型=__NSArrayI, 地址=0x608000029400,值=(1,2),引用計數=1
變量名=a2,類型=__NSArray0, 地址=0x618000012490,值=(),引用計數=-1
變量名=a3,類型=__NSSingleObjectArrayI, 地址=0x608000012890,值=(1),引用計數=1
變量名=a4,類型=__NSArrayI, 地址=0x608000029440,值=(a,b),引用計數=1
變量名=a5,類型=__NSArrayI, 地址=0x608000029460,值=(m,n),引用計數=1
變量名=a6,類型=__NSArray0, 地址=0x618000012490,值=(),引用計數=-1
變量名=a7,類型=__NSArray0, 地址=0x618000012490,值=(),引用計數=-1
從結果看出,只有a2 a6 a7
這三個的引用計數為-1,其他變量的引用計數為1,顯示正常。我們仔細發現,這三個的地址相同,這就是說三個變量指向了同一塊內存,說明a2 a6 a7
三個創建方式創建的空實例是一個特殊值的存在,其應該存儲在常量靜態區。我們可以猜測,OC為了優化內存,在常量靜態區創建一個空值的特殊存在,應該是靜態常量對象,無論什么方式創建的空實例,其都指向靜態區這個空值的。
不僅是NSArray,Foundation中如NSString, NSDictionary, NSSet等區分可變和不可變版本的類,空實例都是靜態對象(NSString的空實例對象是常量區的@""),對象的內存管理策略對其無效。