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

引言

最近有個大佬考察了我關於autoreleasepool的瞭解, 以前一直認爲本身瞭解, 可是稍微一問深, 本身卻啞口無言. 仔細思考了下, 決定要將這個問題結合以前的知識重新梳理一下, 固然, 實踐是必不可少的.git

  • main函數中的autoreleasepool的做用?
  • 系統的autoreleasepool咱們本身建立的autoreleasepool釋放時機差異在哪?
  • 在ARC的環境中, 什麼狀況下須要使用autoreleasepool? 不使用autoreleasepool變量何時會被釋放?

帶着這三個問題, 一塊兒進行一下下面的思考.github

正文

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

分兩種狀況:手動干預釋放時機、系統自動去釋放。服務器

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

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

先不談上面是否徹底正確, 基於以上認知, 當時我靈光一閃推測main函數中autoreleasepool的做用可能爲下面兩種之一:app

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

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: stackoverflow.com/questions/2…

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

回到最初的問題, main函數中的autoreleasepool的做用, 我翻閱了大量資料, 在StackOverflow上讚的比較高的回答是沒卵用... 暫且只能先這樣認爲了.. 但願有了解的同窗能夠講解一下~

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

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

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

相關文章
相關標籤/搜索