這篇文章是在研究autoreleasepool時發現自己看法和網上一些文章的看法有出入,因此寫下自己的見解,和大家一起討論討論。
首先我們來看下面兩個程式碼,大家可以猜猜看兩段程式碼跑起來記憶體的使用是怎樣的:
for (int i = 0; i < 20000000; i++) {
NSString *str = [[NSString alloc] initWithFormat:@"1234567890"];
}
複製程式碼
for (int i = 0; i < 20000000; i++) {
NSString *str = [NSString stringWithFormat:@"1234567890"];
}
複製程式碼
結果是,第一個用initWithFormat
建立的NSString
記憶體幾乎沒有變化。而第二個用stringWithFormat
建立臨時變數的迴圈,會導致記憶體暴漲。
在這裡先說下我的結論:
迴圈是否會導致記憶體暴漲,主要取決於臨時變數是否會加入迴圈外層的 autoreleasepool
中。
不同的物件建立方法和屬性,會導致不同的結果。
這裡分為兩大情況:
1. 使用alloc/new/copy/mutableCopy/init 建立臨時物件
根據ARC的規定(https://clang.llvm.org/docs/AutomaticReferenceCounting.html#method-families), 使用上述方法建立物件,會直接返回被retain物件,不會進入autoreleasepool中。因此該物件在每次迴圈中都會因為離開作用域,被ARC加入的release釋放,所以記憶體並不會上升。我們可以從編譯成的中間語言可以看到裡面只有將物件進行storeStrong
的處理:
2. 使用其它方法建立的臨時物件
這種情況比較複雜。關鍵點在於autoreleaseReturnValue
與retainAutoreleasedReturnValue
兩個方法。
autoreleaseReturnValue
是其它方法建立並返回物件時,ARC會幫我們自動新增的函式,而retainAutoreleasedReturnValue
是變數指向該建立方法返回的物件時會新增的函式。比如
NSString *str = [NSString stringWithFormat:@"1234567890"];
編譯成中間語言時會發現程式碼中加入了retainAutoreleasedReturnValue
:
這兩個方法有什麼用呢?根據ARC的規定(https://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-runtime-objc-autoreleasereturnvalue), 如果這兩個方法成對出現,則這兩個方法會盡可能優化物件,使物件可以直接進行強引用計數,避免進入autoreleasepool
;否則,物件建立完返回時會被加入autoreleasepool
中,等待autoreleasepool
的釋放。
那麼什麼時候會進行優化呢?
其中一個是當指向物件的變數是強引用時會進行。
我們可以加一個Category來試驗下:
然後再次在迴圈中建立臨時物件,我們會看到記憶體並沒有上升:
當然也有沒被優化的情況,比如建立一個weak
變數進行引用:
這裡的臨時變數就會被加進autoreleasepool
中,沒有在作用域結束時釋放,進而造成記憶體暴漲。
現在讓我們看回NSString
的建立方法:
NSString *str = [NSString stringWithFormat:@"1234567890"];
這個會是屬於沒被優化的情況嗎?
答案是並不屬於上述任何一種情況。這個其實屬於“歷史遺留”問題。我找不到NSString的原始碼,但從彙編中可以看到,stringWithFormat
這個方法返回時直接呼叫了autorelease
。
也就是說,NSString
裡依然用著MRC,沒有經過ARC生成autoreleaseReturnValue
與外面的retainAutoreleasedReturnValue
對應,物件返回時都會直接進入autoreleasepool
。因此,才會出現迴圈未結束,記憶體一直在暴漲的情況。
最後,我們還有沒有必要在迴圈中新增@autoreleasepool
呢?我的觀點,是與其搞清楚複雜的優化和歷史問題,不如就直接加@autoreleasepool
吧。