探究是否需要@autoreleasepool優化迴圈

Anejo發表於2020-03-15

這篇文章是在研究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優化迴圈

探究是否需要@autoreleasepool優化迴圈

在這裡先說下我的結論:

迴圈是否會導致記憶體暴漲,主要取決於臨時變數是否會加入迴圈外層的 autoreleasepool中。 不同的物件建立方法和屬性,會導致不同的結果。

這裡分為兩大情況:

1. 使用alloc/new/copy/mutableCopy/init 建立臨時物件

根據ARC的規定(https://clang.llvm.org/docs/AutomaticReferenceCounting.html#method-families), 使用上述方法建立物件,會直接返回被retain物件,不會進入autoreleasepool中。因此該物件在每次迴圈中都會因為離開作用域,被ARC加入的release釋放,所以記憶體並不會上升。我們可以從編譯成的中間語言可以看到裡面只有將物件進行storeStrong的處理:

探究是否需要@autoreleasepool優化迴圈

2. 使用其它方法建立的臨時物件

這種情況比較複雜。關鍵點在於autoreleaseReturnValueretainAutoreleasedReturnValue 兩個方法。

autoreleaseReturnValue 是其它方法建立並返回物件時,ARC會幫我們自動新增的函式,而retainAutoreleasedReturnValue是變數指向該建立方法返回的物件時會新增的函式。比如

NSString *str = [NSString stringWithFormat:@"1234567890"];

編譯成中間語言時會發現程式碼中加入了retainAutoreleasedReturnValue:

探究是否需要@autoreleasepool優化迴圈

這兩個方法有什麼用呢?根據ARC的規定(https://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-runtime-objc-autoreleasereturnvalue), 如果這兩個方法成對出現,則這兩個方法會盡可能優化物件,使物件可以直接進行強引用計數,避免進入autoreleasepool;否則,物件建立完返回時會被加入autoreleasepool中,等待autoreleasepool的釋放。

那麼什麼時候會進行優化呢?

其中一個是當指向物件的變數是強引用時會進行。

我們可以加一個Category來試驗下:

探究是否需要@autoreleasepool優化迴圈
探究是否需要@autoreleasepool優化迴圈

然後再次在迴圈中建立臨時物件,我們會看到記憶體並沒有上升:

探究是否需要@autoreleasepool優化迴圈

當然也有沒被優化的情況,比如建立一個weak變數進行引用:

探究是否需要@autoreleasepool優化迴圈

這裡的臨時變數就會被加進autoreleasepool中,沒有在作用域結束時釋放,進而造成記憶體暴漲。

現在讓我們看回NSString的建立方法:

NSString *str = [NSString stringWithFormat:@"1234567890"];

這個會是屬於沒被優化的情況嗎?

答案是並不屬於上述任何一種情況。這個其實屬於“歷史遺留”問題。我找不到NSString的原始碼,但從彙編中可以看到,stringWithFormat這個方法返回時直接呼叫了autorelease

探究是否需要@autoreleasepool優化迴圈

也就是說,NSString裡依然用著MRC,沒有經過ARC生成autoreleaseReturnValue與外面的retainAutoreleasedReturnValue對應,物件返回時都會直接進入autoreleasepool。因此,才會出現迴圈未結束,記憶體一直在暴漲的情況。

最後,我們還有沒有必要在迴圈中新增@autoreleasepool呢?我的觀點,是與其搞清楚複雜的優化和歷史問題,不如就直接加@autoreleasepool吧。

相關文章