Objective-C Autorelease Pool 的實現原理

本文轉自:http://www.jianshu.com/p/d6687291e486

內存管理一直是學習Objectie-C的重點和難點之一,儘管現在已經是ARC時代了,但是瞭解Objective-C的內存管理機制仍然是十分必要的。其中,弄清楚autorelease的原理更是重中之重,只有理解了autorelease的原理,我們纔算是真正瞭解了Objective-C的內存管理機制。

autoreleased對象什麼時候釋放

autoreleased 本質上就是延遲調用release

AutoreleasePoolPage

-[NSAutoreleasePool release]方法最終是通過調用AutoreleasePoolPage:pop(void*)函數來負責對autoreleasepool中的autoreleased對象執行release操作的。

那這裏的AutoreleasePoolPage到底是什麼東西呢?其實,autoreleasepool是沒有單獨的內存結構的,它是通過以AutoreleasePoolPage爲結點的雙向鏈表來實現的。我們打開runtime的源碼工程,在NSObject.mm文件的第438-932行可以找到autoreleasepool的實現源碼。通過閱讀源碼,我們可以知道:


NSObject.mm源碼備註圖

每一個線程的autoreleasepool其實就是一個指針的堆棧

每一個指針代表一個需要release的對象或者POOL_SENTINEL是一個autorelease pool 邊界.(此處翻譯如有問題可留言討論你認爲對的翻譯.)

一個pool token就是這個pool所對應的POOL_SENTINEL的內存地址。當這個pool被popped,所有的內存地址在pool token之後的對象都會被release;

Thread-local storage (線程局部存儲)指向hot page,即最新添加的autoreleased對象所在的那個page.



1.magic_t用來校驗AutoreleasePoolPage的結構是否完整;

2.next指向最新添加的autoreleased對象的下一個位置,初始化時指向begin();

3.thread指向當前線程;

4.parent指向父結點,第一個結點的parent值爲nil;

5.child指向子結點,最後一個結點的child值爲nil;

6.depth代表深度,從0開始,往後遞增1;

7.hiwat代表high water mark;

另外,當next == begin()時,表示AutoreleasePoolPage爲空;當next==end()時,表示AutoreleasePoolPage已滿。

Autorelease Pool Blocks

我們使用 clang-rewrite-objc命令將下面的Objective-C代碼重寫成C++代碼:



將會得到以下輸出結果



不得不說,蘋果對@autoreleasepool {}的實現真的是非常巧妙,真正可以稱得上是代碼的藝術。蘋果通過聲明一個__AtAutoreleasePool類型的局部變量__autoreleasepool來實現@autoreleasepool {}。當聲明__autoreleasepool變量時,構造函數__AtAutoreleasePool()被調用,即執行atautoreleasepoolobj = objc_autoreleasePoolPush();;當出了當前作用域時,析構函數~__AtAutoreleasePool()被調用,即執行objc_autoreleasePoolPop(atautoreleasepoolobj);。也就是說@autoreleasepool {}的實現代碼可以進一步簡化如下:



因此,單個autoreleasepool的運行過程可以簡單地理解爲objc_autoreleasePoolPush()、[對像 autorelease]和objc_autoreleasePoolPop(void*)三個過程。

push操作

上面提到的objc_autoreleasePoolPush()函數本質上就是調用的 AutoreleasePoolPage 的 push 函數。

void*

objc_autoreleasePoolPush(void)

{

       if(UseGC)returnnil;

       returnAutoreleasePoolPage::push();

}

因此,我們接下來看看 AutoreleasePoolPage 的 push 函數的作用和執行過程。一個 push 操作其實就是創建一個新的 autoreleasepool ,對應 AutoreleasePoolPage 的具體實現就是往 AutoreleasePoolPage 中的next位置插入一個 POOL_SENTINEL ,並且返回插入的 POOL_SENTINEL 的內存地址。這個地址也就是我們前面提到的 pool token ,在執行 pop 操作的時候作爲函數的入參。

staticinlinevoid*push()

{

       id*dest=autoreleaseFast(POOL_SENTINEL);

        assert(*dest==POOL_SENTINEL);

        return dest;

}

push 函數通過調用autoreleaseFast函數來執行具體的插入操作。

staticinlineid*autoreleaseFast(idobj)

{

AutoreleasePoolPage*page=hotPage();

if(page&&!page->full()) {

returnpage->add(obj);

}elseif(page) {

returnautoreleaseFullPage(obj,page);

}else{

returnautoreleaseNoPage(obj);

}

}

autoreleaseFast函數在執行一個具體的插入操作時,分別對三種情況進行了不同的處理:

1.當前 page 存在且沒有滿時,直接將對象添加到當前 page 中,即next指向的位置;

2.當前 page 存在且已滿時,創建一個新的 page ,並將對象添加到新創建的 page 中;

3.當前 page 不存在時,即還沒有 page 時,創建第一個 page ,並將對象添加到新創建的 page 中。

每調用一次 push 操作就會創建一個新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一個 POOL_SENTINEL ,並且返回插入的 POOL_SENTINEL 的內存地址。

autorelease 操作

通過NSObject.mm源文件,我們可以找到-autorelease方法的實現

- (id)autorelease{

      return((id)self)->rootAutorelease();

}

通過查看((id)self)->rootAutorelease()的方法調用,我們發現最終調用的就是 AutoreleasePoolPage 的autorelease函數。

__attribute__((noinline,used))

id

objc_object::rootAutorelease2()

{

assert(!isTaggedPointer());

returnAutoreleasePoolPage::autorelease((id)this);

}

AutoreleasePoolPage 的autorelease函數的實現對我們來說就比較容量理解了,它跟 push 操作的實現非常相似。只不過 push 操作插入的是一個 POOL_SENTINEL ,而 autorelease 操作插入的是一個具體的 autoreleased 對象。

staticinlineidautorelease(idobj)

{

assert(obj);

assert(!obj->isTaggedPointer());

id*dest__unused=autoreleaseFast(obj);

assert(!dest||*dest==obj);

returnobj;

}

pop 操作

同理,前面提到的objc_autoreleasePoolPop(void *)函數本質上也是調用的 AutoreleasePoolPage 的pop函數。

void

objc_autoreleasePoolPop(void*ctxt)

{

if(UseGC)return;

// fixme rdar://9167170

if(!ctxt)return;

AutoreleasePoolPage::pop(ctxt);

}

pop 函數的入參就是 push 函數的返回值,也就是 POOL_SENTINEL 的內存地址,即 pool token 。當執行 pop 操作時,內存地址在 pool token 之後的所有 autoreleased 對象都會被 release 。直到 pool token 所在 page 的next指向 pool token 爲止。

下面是某個線程的 autoreleasepool 堆棧的內存結構圖,在這個 autoreleasepool 堆棧中總共有兩個 POOL_SENTINEL ,即有兩個 autoreleasepool 。該堆棧由三個 AutoreleasePoolPage 結點組成,第一個 AutoreleasePoolPage 結點爲coldPage(),最後一個 AutoreleasePoolPage 結點爲hotPage()。其中,前兩個結點已經滿了,最後一個結點中保存了最新添加的 autoreleased 對象objr3的內存地址。



此時,如果執行pop(token1)操作,那麼該 autoreleasepool 堆棧的內存結構將會變成如下圖所示:






NSThread、NSRunLoop和NSAutoreleasePool

根據蘋果官方文檔中對NSRunLoop的描述,我們可以知道每一個線程,包括主線程,都會擁有一個專屬的NSRunLoop對象,並且會在有需要的時候自動創建。(注:NSRunLoop的本質是一個消息機制的處理模式)

蘋果官方文檔中對NSRunLoop的描述


The NSRunLoop class declares the programmatic interface to objects that manage input sources. An NSRunLoop object processes input for sources such as mouse and keyboard events from the window system, NSPort objects, and NSConnection objects. An NSRunLoop object also processes NSTimer events.

NSRunLoop類聲明的編程接口對象管理輸入源.一個NSRunLoop對象進程的輸入源像一個窗口系統輸入鼠標和鍵盤,NSPort 對象,和NSConnection對象。NSTimre事件發生時伴隨着一個NSRunLoop對象被創建。

Your application cannot either create or explicitly manage NSRunLoop objects. Each NSThread object, including the application’s main thread, has an NSRunLoop object automatically created for it as needed. If you need to access the current thread’s run loop, you do so with the class method currentRunLoop.

您的應用不能創建或顯示管理NSRunLoop對象。每一個線程,包括主線程,都會擁有一個專屬的NSRunLoop對象,並且會在有需要的時候自動創建。


WARNING

警告⚠️

The NSRunLoop class is generally not considered to be thread-safe and its methods should only be called within the context of the current thread. You should never try to call the methods of an NSRunLoop object running in a different thread, as doing so might cause unexpected results.

NSRunLoop類一般不被認爲是線程安全的,其方法應該只被爲當前線程的上下文中。你不應該試圖去訪問NSRunLoop對象在不同的線程中,這樣做可能會導致意想不到的結果.


同樣的,根據蘋果官方文檔中對NSAutoreleasePool的描述,我們可知,在主線程的NSRunLoop對象(在系統級別的其它線程中應該也是如此,比如通過dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)) 的每個event loop開始前,系統會自動創建一個autoreleasepool,並在event loop結束時drain。

 蘋果官方文檔中對NSAutoreleasePool的描述

The NSAutoreleasePool class is used to support Cocoa is reference-counted memory management system.An autorelease pool stores objects that are sent a release message when the pool itself is drained.

NSAutoreleasePool 類是用於支持Cocoa採用引用計數的內存管理系統. 發送一個release時 autorelease pool池將會被排幹.

另外,NSAutoreleasepool 中提到,每一個線程都會維護自己的autoreleasepool堆棧。換句話說autoreleasepool是與線程緊密相關的,每一個autoreleasepool只對應一個線程。

Each thread(including the main thread) maintains its own stack of NSAutoreleasePool objects.

每個線程都只對應它自己的NSAutoreleasepool對象(包括主線程)

弄清楚NSThread、NSRunLoop和NSAutoreleasePool三者的關係可以幫助我們從整體上了解Objective-C的內存管理機制,清楚系統在背後到底爲我們做了什麼,理解整個運行機制等。

總結

我們到這裏,相信應該對Object-C的內存管理機制有了更進一步的認識。通常情況下,我們不需要手動添加autoreleasepool 的,使用線程自動維護的autoreleasepool就好了。根據蘋果官方文檔中對Using Autorelease Pool Blocks的描述,我們知道在下面三種情況下是需要我們手動添加autoreleasepool的:

1.If you are writing a program that is not based on a UI framework, such as a command-line tool.

如果你編寫的程序不是基於UI框架的,比如說命令行工具.

2.If you write a loop that creates many temporary objects.

You may use an autorelease pool block inside the loop to dispose of those objects before the next iteration. Using an autorelease pool block in the loop helps to reduce the maximum memory footprint of the application.

如果你編寫的循環中創建了大量的臨時對象

3.If you spawn a secondary thread.

You must create your own autorelease pool block as soon as the thread begins executing; otherwise, your application will leak objects. (See Autorelease Pool Blocks and Threads for details.)

如果你創建了一個輔助線程.

(有問題可留言.沒事多看下英文文檔 一手的資料總是比較全面的,別人的文檔都是自己吸收後寫出來的對於自己是有幫助的,但也不可能面面具到.)



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章