在ARC環境中autoreleasepool(runloop)的研究

引言

最近有個大佬考察了我關于autoreleasepool的了解, 之前一直認為自己了解, 但是稍微一問深, 自己卻啞口無言. 仔細思考了下, 決定要將這個問題結合之前的知識從新梳理一下, 當然, 實踐是必不可少的.

  • main函數中的autoreleasepool的作用?
  • 系統的autoreleasepool我們自己創建的autoreleasepool釋放時機差別在哪?
  • 在ARC的環境中, 什么情況下需要使用autoreleasepool? 不使用autoreleasepool變量什么時候會被釋放?

帶著這三個問題, 一起進行一下下面的思考.

正文

對于autoreleasepool釋放時機, 我們很容易在網上搜到這樣的說法:

分兩種情況:手動干預釋放時機、系統自動去釋放。

手動干預釋放時機--指定autoreleasepool 就是所謂的:當前作用域大括號結束時釋放。

系統自動去釋放--不手動指定autoreleasepool

先不談上面是否完全正確, 基于以上認知, 當時我靈光一閃推測main函數中autoreleasepool的作用可能為下面兩種之一:

1.系統主線程中的默認的autoreleasepool.

2.整個App相對于iOS系統的一個autoreleasepool.

其他的解釋其實在網上可以搜到很多, 所以這里我們可以做一個小實驗.

第一點其實很好驗證, 將main函數中的autoreleasepool注釋掉, 運行

for (int i = 0; i < 10e5 * 2; i++) {
    NSString *str = [NSString stringWithFormat:@"hi + %d", i];
}
NSLog(@"finished!");

實際結果表明, 內存波動并沒有什么區別:

  • 未注釋Main函數中的autoreleasepool


  • 注釋Main函數中的autoreleasepool


所以我們可以認為第二種是對的嗎, 后來自己一想也覺得不對, 對于系統內存管理相關代碼怎么會在程序里面呢, 不符合蘋果的風格. 結果很明顯我自己推測的都不對, 所以到底起什么作用呢? 待會再細說, 先驗證一下釋放時機的問題.

同樣是上面一段函數, 在for循環中加入autoreleasepool:

for (int i = 0; i < 10e5 * 2; i++) {
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"hi + %d", i];
    }
}
NSLog(@"finished!");

我相信稍微了解一點的同學已經知道了運行結果:

為臨時變量分配的內存已經得到平穩的釋放, 所以結論就是最上面我們看到的認知? 其實本身每個Runloop已經默認會創建一個autoreleasepool了, 所以我們這里添加相當于嵌套(便于理解)了一個, 并沒有弄清楚autoreleasepool自身的釋放時機. 下面做另外一個小測試:

這一次在代碼中新增對Runloop的Observer, 及時獲取Runloop的狀態變化確認釋放時機, 代碼如下:

// 添加一個監聽者
- (void)addRunLoopObserver {
    
    // 1. 創建監聽者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"進入RunLoop");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"即將處理Timer事件");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"即將處理Source事件");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"即將休眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"被喚醒");
                break;
            case kCFRunLoopExit:
                NSLog(@"退出RunLoop");
                break;
            default:
                break;
        }
    });
    
    // 2. 添加監聽者
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}

另外上面的方法運行連續運行兩次, 不手動添加autoreleasepool, 大概是這樣:

- (void)test1 {

    NSLog(@"test1 begin!");
    for (int i = 0; i < 10e5 * 2; i++) {
        //@autoreleasepool {
            NSString *str = [NSString stringWithFormat:@"hi + %d", i];
        //}
    }
    NSLog(@"test1 finished!");
}

- (void)test2 {
    
    NSLog(@"test2 begin!");
    for (int i = 0; i < 10e5 * 2; i++) {
        //@autoreleasepool {
            NSString *str = [NSString stringWithFormat:@"hi + %d", i];
        //}
    }
    NSLog(@"test2 finished!");
}

運行之后的效果是這樣的:

很清楚的看到Runloop沒有完成一次循環之前所有內存都未釋放, 即使局部變量出了作用域也必須等待Runloop循環完成.

下面同樣, 手動添加autoreleasepool觀察釋放時機.

結果是意外也合理的. 即使Runloop未完成循環, 內存也即使釋放了.

總結

@autoreleasepool{} 

等價于

void *context = objc_autoreleasePoolPush();
// {}中的代碼
objc_autoreleasePoolPop(context);

每次出了{}時objc_autoreleasePoolPop()就被調用, 所以直接釋放掉了. 當然, 系統自動創建的autoreleasepool也是一樣, 只是調用的時機不同: 線程與Runloop是一一對應, Runloop與系統創建的autoreleasepool也是一一對應, 所以不論是Runloop完成了一次循環還是線程被關閉時, autoreleasepool都會釋放, 當然手動添加的也會被管理, 上面為了方便理解, 說的是嵌套, 本質上是沒有嵌套這個說法的, 對@autoreleasepool{}本質的一些個人總結:

主要就是一個類:AutoreleasePoolPage

兩個函數: objc_autoreleasePoolPush()、objc_autoreleasePoolPop()

運作方式: autoreleasepool由若干個autoreleasePoolPage類以雙向鏈表的形式組合而成, 當程序運行到@autoreleasepool{時, objc_autoreleasePoolPush()將被調用, runtime會向當前的AutoreleasePoolPage中添加一個nil對象作為哨兵,
在{}中創建的對象會被依次記錄到AutoreleasePoolPage的棧頂指針,
當運行完@autoreleasepool{}時, objc_autoreleasePoolPop(哨兵)將被調用, runtime就會向AutoreleasePoolPage中記錄的對象發送release消息直到哨兵的位置, 即完成了一次完整的運作.

另外根據官方文檔:

Threads

If you are making Cocoa calls outside of the Application Kit’s main thread—for example if you create a Foundation-only application or if you detach a thread—you need to create your own autorelease pool......

主線程中的自動釋放池是自動創建的, 文檔中說子線程中的自動釋放池是需要手動創建的, 但實測, 其實我們常用的多線程管理方式(GCD, NSOprationQueue, NSThread)都已經幫我們處理好了, 其中NSThread在iOS7之后才自動創建線程中的AutoreleasePool, 這個在官方文檔中找不到記錄, 參考StackOverflow: https://stackoverflow.com/questions/24952549/does-nsthread-create-autoreleasepool-automatically-now

另外網上有說法AutoreleasePool會影響性能, 其實看上面的函數運行的時間就可以發現, 并沒有影響, 甚至加入了AutoreleasePool運行快了2秒(不嚴謹).

回到最初的問題, main函數中的autoreleasepool的作用, 我翻閱了大量資料, 在StackOverflow上贊的比較高的回答是沒卵用... 暫且只能先這樣認為了.. 希望有了解的同學可以講解一下~

在實際中的使用場景其實很明確了, 在程序中中有大量臨時變量的時候最好手動創建.

最常出現大量變量的時候顯然是循環/遍歷, 我們常用的for循環, 以及enumerate其實跟autoreleasepool也有關, for循環是不自動創建autoreleasepool的, 而enumerate中已經自動創建了autoreleasepool, 值得注意的是高并發enumerate常常會出一些意外的問題, 例如對象被提前釋放, 所以建議高并發情況下使用for循環(性能高于enumerate), 再手動添加autoreleasepool.

本人前幾篇文章中提到的一個App: 直播伴侶中就是手機端對彈幕進行高并發計算, 分詞, 對比.. 使用了autoreleasepool之后明顯在斗魚彈幕服務器"炸魚"時有所改善..歡迎Star: https://github.com/syik/BulletAnalyzer

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容