本文為投稿文章,原文
起因
最開始只是想試一試寫在方法內部的局部變量釋放時經不經過autoreleasepool。
例如,下圖這樣的代碼。
為了不影響對象本身的引用計數影響它的銷毀過程,使用一個weak指針,不出所料的,打印出來了如下結果
但是這個實驗如果換成NSString得到的則是完全不一樣的結果。
如下代碼
打印出來的結果卻是:
這個看上去也很好似乎也很好理解,NSString初始化的時候是存放在常量區的,所以沒有釋放嘛。
深入研究
為了觀察對象的釋放過程,我們在str賦值的地方加一個斷點
走到該斷點的時候通過lldb命令watchpoint set variable str來觀察,可以看到str由0x0000000000000000變成0x00000001056b3250。
然後一路點擊Continue program execution,發現str會變成0x0000000000000000,控制台只打印了一次str的值,也就是說viewwillappear還沒有執行,這點跟雷大博客(Objective-C Autorelease Pool 的實現原理)中的略不一樣,我猜是apple改了。
然後看左側的方法調用棧,會發現這個過程經過了objc_store,AutoreleasePoolPage::pop(void *)等函數通過autoreleasepool釋放了。現在修改一下log語句。
看了幾個大神之前的博客,大都還打印了retainCount,但是今天這裡研究的是arc,就不打印retainCount了。
執行如下代碼:
得到打印結果:
可以看到這裡其實是有三種String的,而references指向了cstr,此時在viewWillAppear和viewDidAppear裡打印references得到的則是null。
三種String
NSCFConstantString: 字符串常量,放在常量區,對其retain或者release不影響它的引用計數,程序結束後釋放。用字面量語法創建出來的string就是這種,所以在出了viewDidLoad方法以後在其他地方也能打印出值,壓根就沒釋放。
NSTaggedPointerString: Tagged Point,標簽指針,蘋果在64位環境下對NSString和NSNumber做的一些優化,簡單來說就是把對象的內容存放在了指針裡,這樣就不需要在堆內存裡在開辟一塊空間存放對象了,一般用來優化長度較小的內容。關於標簽指針的內容可以參考唐巧的介紹:深入理解Tagged Pointer
對於NSString,當非字面量的數字,英文字母字符串的長度小於等於9的時候會自動成為NSTaggedPointerString類型。代碼中的bstr如果再加一位或者有中文在裡面就是變成NSCFString。而NSTaggedPointerString也是不會釋放的,它的內容就在本身的指針裡,又沒有對象釋放個啥啊。所以如果把references的賦值代碼改為
在viewWillAppear和viewDidAppear中也是能打印出值來的。
NSCFString: 這種string就和普通的對象很像了,儲存在堆上,有正常的引用計數,需要程序員分配釋放。所以references = cstr時,會打印出null,cstr出了方法作用域在runloop結束時就被autoreleasepool釋放了。
stringWithFormat
到這裡還是有問題
根據以上說法,當string超過10位數時,bstr和cstr都是NSCFString,可是兩種情況viewWillAppear和viewDidAppear在打印的結果不一樣。
bstr:
cstr:
根據太陽神黑幕背後的Autorelease中的說法,是因為viewWillAppear和viewDidLoad在一個runloop中導致bstr在willappear中能打印出來值。如果真的是這樣那cstr講道理應該也能打印出值來,文章最開始用NSObject做實驗時在viewWillAppear時應該也能打印出值來。
所以其實並不是同一個runloop的問題,問題出在stringWithFormat這個工廠方法上。
查資料得知以 alloc, copy, init,mutableCopy和new這些方法打頭的方法,返回的都是 retained return value,例如[[NSString alloc] initWithFormat:],而其他的則是unretained return value,例如 [NSString stringWithFormat:]。對於前者調用者是要負責釋放的,對於後者就不需要了。而且對於後者ARC會把對象的生命周期延長,確保調用者能拿到並且使用這個返回值,但是並不一定會使用 autorelease,在worst case 的情況下才可能會使用,因此調用者不能假設返回值真的就在 autorelease pool中。從性能的角度,這種做法也是可以理解的。如果我們能夠知道一個對象的生命周期最長應該有多長,也就沒有必要使用 autorelease 了,直接使用 release 就可以。如果很多對象都使用 autorelease 的話,也會導致整個 pool 在 drain 的時候性能下降。
When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, then leaves all local scopes, and then balances out the retain while ensuring that the value lives across the call boundary. In the worst case, this may involve an autorelease, but callers must not assume that the value is actually in the autorelease pool.
ARC performs no extra mandatory work on the caller side, although it may elect to do something to shorten the lifetime of the returned value.
也就是說通過工程方法得到的string生命周期被延長了,所以才會在viewWillAppear裡依然可以打印出來。為了證實這一點,我們換成array來做個實驗。
第一種情況通過字面量創建array,打印台輸出:
第二種情況通過工廠方法創建,打印台輸出:
可以看到通過工廠方法創建的array生命周期確實被延長了。
總結
1.方法裡的臨時變量是會通過autoreleasepool釋放的
2.NSCFString跟普通對象一樣是可以釋放的
3.NSString和NSArray的工廠方法可以延長對象的生命周期(同理,NSDictionary也是一樣的,有興趣的可以試一下)
參考資料
黑幕背後的Autorelease
Objective-C Autorelease Pool 的實現原理
Objective-C 內存管理——你需要知道的一切