iOS-autorelease與autoreleasepool

weixin_34292287發表於2017-05-19

Autoreleasepool自動釋放池塊提供了一個持有物件的所有權的機制,可以避免它立刻釋放(如你從一個方法返回一個物件時).正常情況下,我們不需要建立自己的自動釋放池塊,但也有一些情況下,建立自動釋放池是非常明智的(子執行緒開啟新的任務,for迴圈生成大量物件的時候).

autorelease 與 runloop

autorelease 本質上就是延遲呼叫 release,實際上autorelease物件是在當前的runloop迭代結束時釋放的,以下是在iOS 8.2模擬器中的測試程式碼:

<pre><code>`_weak id reference = nil;

  • (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSArray *arr = [NSArray arrayWithObjects:@"FlyElephant",@"Keso", nil];
    reference = arr;
    }

  • (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"陣列:%@",reference);
    }

  • (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"陣列:%@",reference);
    }`</code></pre>

1048365-812077766795a87a.png
FlyElephant.png

加入autorelease的測試程式碼:
<pre><code>@autoreleasepool { NSArray *arr = [NSArray arrayWithObjects:@"FlyElephant",@"Keso", nil]; reference = arr; }</code></pre>

1048365-3359c7046ac3ca38.png
autoreleasepool.png

autoreleasepool 與 runloop

autoreleasepool與runloop乍一看沒有關係,如果對Runloop有研究,對下面這段文字應該有印象:
App啟動後,蘋果在主執行緒 RunLoop 裡註冊了兩個 Observer,其回撥都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一個 Observer 監視的事件是 Entry(即將進入Loop),其回撥內會呼叫 _objc_autoreleasePoolPush() 建立自動釋放池。其 order 是-2147483647,優先順序最高,保證建立釋放池發生在其他所有回撥之前。

第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時呼叫_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並建立新池;Exit(即將退出Loop) 時呼叫 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先順序最低,保證其釋放池子發生在其他所有回撥之後。

在主執行緒執行的程式碼,通常是寫在諸如事件回撥、Timer回撥內的。這些回撥會被 RunLoop 建立好的 AutoreleasePool 環繞著,所以不會出現記憶體洩漏,開發者也不必顯示建立 Pool 了。

autoreleasepool 原理

ARC下我們使用@autoreleasepool{}來使用一個AutoreleasePool,隨後編譯器將進行編譯:
<pre><code>void *context = objc_autoreleasePoolPush(); // {}中的程式碼 objc_autoreleasePoolPop(context);</code></pre>

autoreleasepool最終底層是由autoreleasepoolpage實現,定義如下:
<pre><code>class AutoreleasePoolPage { magic_t const magic; id *next; pthread_t const thread; AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat; };</code></pre>

1.autoreleasePool並沒有單獨的結構,而是由若干個AutoreleasePoolPage以雙向連結串列的形式組合而成(分別對應結構中的parent指標和child指標)
2.AutoreleasePool是按執行緒一一對應的(結構中的thread指標指向當前執行緒)
3.AutoreleasePoolPage每個物件會開闢4096位元組記憶體(也就是虛擬記憶體一頁的大小
4.next指標作為遊標指向棧頂最新add進來的autorelease物件的下一個位置
5.一個AutoreleasePoolPage的空間被佔滿時,會新建一個AutoreleasePoolPage物件,連線連結串列,後來的autorelease物件在新的page加入.

autoreleasepool 實戰

正常開發中也沒有見過哪個專案中到處都是autoreleasepool的,那麼什麼時候使用autoreleasepool呢?

1.如果你寫的程式不是基於UI框架的,比如說命令列工具.(較少)
2.如果建立一個迴圈,建立了大量的臨時物件,你可以使用自動釋放池處理在下一次迭代前處理這些物件,避免佔用大量記憶體.
iOS中有三種迴圈比遍歷方式for、forin、enumerateObjectsUsingBlcok,實際上enumerateObjectsUsingBlcok內部已經通過@autoreleasepool{}操作進行了物件處理,for和forin的方式需要我們自己手動處理,因此enumerateObjectsUsingBlcok效率最高,記憶體佔用最少.
3.建立次級執行緒,當你建立執行緒的時候,你需要建立釋放器避免記憶體洩漏.

Each thread in a Cocoa application maintains its own stack of autorelease pool blocks. If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.

If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should use autorelease pool blocks (like AppKit and UIKit do on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows. If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.

在Cocoa應用程式中每個系統執行緒都有自己的自動釋放池來維護,如果手動建立建立執行緒,需要手動建立自動釋放池.

如果建立常駐執行緒可能會導致大量的autorelease物件,應該像AppKit和UIkit一樣使用autoreleasepool,如果你不使用cocoa的建立執行緒(比如通過POSIX建立執行緒),那麼不需要使用autoreleasepool.

參考資料
官方文件
黑幕背後的Autorelease
深入理解RunLoop
Objective-C Autorelease Pool 的實現原理

相關文章