iOS -- Autorelease & AutoreleasePool
前言
記憶體管理一直是Objective-C
的重點,在MRC
環境下,通過呼叫[obj autorelease]
來延遲記憶體的釋放,在現在ARC環境下,我們都知道編譯器會在合適的地方插入release/autorelease
記憶體釋放語句,我們甚至可以不需要知道Autorelease
就能很好的管理記憶體。雖然現在已經幾乎用不到MRC
,但是瞭解 Objective-C
的記憶體管理機制仍然是十分必要的,看看編譯器幫助我們怎麼來管理記憶體。本文僅僅是記錄自己的學習筆記。
AutoreleasePool簡介
1.什麼是AutoreleasePool
AutoreleasePool
:自動釋放池是 Objective-C
release
、autorelease
等記憶體釋放操作。當物件呼叫 autorelease
方法後會被放到自動釋放池中延遲釋放時機,當快取池需要清除dealloc
時,會向這些 Autoreleased
物件做 release
釋放操作。
2.物件什麼時候釋放(ARC規則)
一般的說法是物件會在當前作用域大括號結束時釋放,
有這樣一個ARC
環境下簡單的例子?:首先建立一個ZHPerson
類:
//// ZHPerson.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ZHPerson : NSObject
+(instancetype)object;
@end
////ZHPerson.m
#import "ZHPerson.h"
@implementation ZHPerson
-(void)dealloc
{
NSLog(@"ZHPerson dealloc");
}
+(instancetype)object
{
return [[ZHPerson alloc] init];
}
@end
複製程式碼
然後在ViewController.m
匯入標頭檔案ZHPerson.h
,然後在寫一段這樣的程式碼:
__weak id temp = nil;
{
ZHPerson *person = [[ZHPerson alloc] init];
temp = person;
}
NSLog(@"temp = %@" ,temp);
複製程式碼
解釋一下這個程式碼:先聲明瞭一個 __weak
變數temp
,因為 __weak
變數有一個特性就是它不會影響所指向物件的生命週期,然後讓變數temp
指向建立的person
物件,輸出如下:
person
的作用域,它就被釋放了,看來是正常的。
把上面的建立物件的方法,變一變寫法:
__weak id temp = nil;
{
ZHPerson *person = [ZHPerson object];
temp = person;
}
NSLog(@"temp = %@",temp);
複製程式碼
輸出如下:
這裡person
物件超出了其作用域還是存在的,被延遲釋放了,也就是說其內部呼叫了autorelease
方法。
小總結:
查詢得知:以 alloc
,copy
,mutableCopy
和new
這些方法會被預設標記為 __attribute((ns_returns_retained))
,以這些方法建立的物件,編譯器在會在呼叫方法外圍要加上記憶體管理程式碼retain/release
,所以其在作用域結束的時候就會釋放,而不以這些關鍵字開頭的方法,會被預設標記為__attribute((ns_returns_not_retained))
,編譯器會在方法內部自動加上autorelease
方法,這時建立的物件就會被註冊到自動釋放池中,同時其釋放會延遲,等到自動釋放池銷燬的時候才釋放。
3.AutoreleasePool的顯示建立
1.MRC下的建立
//1.生成一個NSAutoreleasePool物件
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//2.建立物件
id object = [[NSObject alloc] init];
//3.物件呼叫autorelease方法
[object autorelease];
//4.廢棄NSAutoreleasePool物件,會對釋放池中的object傳送release訊息
[pool drain];
複製程式碼
2.ARC下的建立
@autoreleasepool {
//LLVM會在內部插入autorelease方法
id object = [[NSObject alloc] init];
}
複製程式碼
AutoreleasePool
的作用前面有提到過,每當一個物件呼叫 autorelease
方法時,實際上是將該物件放入當前 AutoreleasePool
中,當前AutoreleasePool
釋放時,會對新增進該 AutoreleasePool
中的物件逐一呼叫 release
方法。在ARC
環境下,並不需要特別的去關注Autoreleasepool
的使用,因為系統已經做了處理。
AutoreleasePool探索學習
為了看一下AutoreleasePool
到底做了什麼,先來建立一個main.m
檔案(Xcode
-> File
-> New Project
-> macOS
-> Command Line Tool
-> main.m
);
#import <Foundation/Foundation.h>
int main(int argc,const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello,World!");
}
return 0;
}
複製程式碼
然後,使用編譯器clang
編譯main.m
轉化成main.cpp
檔案(在終端使用命令:clang -rewrite-objc main.m
),滑到main.cpp
檔案的最後,有這樣一段程式碼:
@autoreleasePool
轉換成一個__AtAutoreleasePool
型別的區域性私有變數__AtAutoreleasePool __autoreleasepool
;
接著在 main.cpp
檔案中查詢__AtAutoreleasePool
,來看一下它具體的實現:
__AtAutoreleasePool
是結構體型別,並且實現了兩個函式:建構函式__AtAutoreleasePool()
和解構函式~__AtAutoreleasePool()
。
也就是說在宣告 __autoreleasepool
變數時,建構函式 __AtAutoreleasePool()
被呼叫,即執行 atautoreleasepoolobj = objc_autoreleasePoolPush();
;當出了當前作用域時,解構函式 ~__AtAutoreleasePool()
被呼叫,即執行 objc_autoreleasePoolPop(atautoreleasepoolobj);
那麼上面的main.m
中的程式碼可以用這種形式代替:
#import <Foundation/Foundation.h>
int main(int argc,const char * argv[]) {
// @autoreleasepool
{
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// insert code here...
NSLog(@"Hello,World!");
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
複製程式碼
接下來看一下解構函式和建構函式分別實現了什麼內容?這裡需要一份objc_runtime
的原始碼(原始碼地址),這裡使用的是objc4-756.2.tar.gz
:
AutoreleasePoolPage
的push
方法和pop
方法(這裡::是C++
呼叫方法的形式,類似於點語法)。
1.AutoreleasePoolPage
AutoreleasePoolPage
是一個C++
實現的類,它的具體實現程式碼是:
class AutoreleasePoolPage
{
# define POOL_BOUNDARY nil //哨兵物件(可以看做是一個邊界)
static size_t const COUNT = SIZE / sizeof(id); // 物件數量
magic_t const magic; //用來校驗 `AutoreleasePoolPage`的結構是否完整;
id *next; //指向最新新增的 `autoreleased` 物件的下一個位置,初始化時指向 `begin()` ;
pthread_t const thread; //指向當前執行緒;
AutoreleasePoolPage * const parent; //指向父結點,第一個結點的 `parent` 值為 `nil` ;
AutoreleasePoolPage *child; //指向子結點,最後一個結點的 `child` 值為 `nil` ;
uint32_t const depth; //代表深度,從 `0` 開始,往後遞增 `1`;
uint32_t hiwat; //代表 `high water mark` ;
//剩下程式碼省略......
}
複製程式碼
通過原始碼可以知道這是一個典型的雙向列表結構,所以AutoreleasePool
是由若干個AutoreleasePoolPage
以雙向連結串列的形式組合而成。
AutoreleasePoolPage
每個物件會開闢4096位元組記憶體(虛擬記憶體一頁的大小),除了上面的例項變數所佔空間,剩下的空間全部用來儲存autorelease
物件的地址,AutoreleasepoolPage
通過壓棧的方式來儲存每個autorelease
的物件(從低地址到高地址)。其中next
指標作為遊標指向棧頂最新add
進來的autorelease
物件的下一個位置,當 next
指標指向begin
時,表示 AutoreleasePoolPage
為空;當 next
指標指向end
時,表示 AutoreleasePoolPage
已滿,此時會新建一個AutoreleasePoolPage
物件,連線連結串列,後來的autorelease
物件在新的AutoreleasePoolPage
插入,同樣的新AutoreleasePoolPage
的next
指標被初始化在棧底(指向begin
的位置)。
2.AutoreleasePoolPage::push()
既然已經知道了autorelease
的物件會通過壓棧的方式插入到AutoreleasePoolPage
當中,那麼顯然AutoreleasePoolPage
的push
方法就承包了AutoreleasePoolPage
的建立和插入。
接著看下push
方法的原始碼:
static inline void *push()
{
id *dest;
//判斷是否已經初始化AutoreleasePoolPage
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
複製程式碼
這裡的POOL_BOUNDARY
可以理解為哨兵物件,或者理解為一種邊界標識,而且這個POOL_BOUNDARY
值為0,是個nil
。
接下來,先來看一下autoreleaseFast
這個方法,
static inline id *autoreleaseFast(id obj)
{
//獲取到當前page,這個hotPage是從當前執行緒的區域性私有空間取出來的
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj,page);
} else {
return autoreleaseNoPage(obj);
}
}
複製程式碼
我們知道連結串列是有空間的,所以上面?的原始碼可以理解為:
(1). 當前page
存在且沒有滿時,直接將物件新增到當前page
中,即next
指向的位置;
(2). 當前page
存在並且已滿時,建立一個新的page
,並將物件新增到新建立的page
中,然後將這兩個連結串列節點進行連結。
(3). 當前page
不存在時,建立第一個page
,並將物件新增到新建立的page
中。
這裡重點看一下page->add(obj)
這個方法,
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
複製程式碼
可以看到這裡返回的ret
其實next
指標指向的地址,由上面的push
方法的原始碼可知,這裡page->add(obj)
傳入的obj
其實就是POOL_BOUNDARY
,也就是說每一次呼叫push
方法,都會插入一個POOL_BOUNDARY
,所以objc_autoreleasePoolPush
的返回值就是這個哨兵物件的地址。
3.AutoreleasePoolPage::pop(ctxt)
通過上面對建構函式objc_autoreleasePoolPush
的學習,已經知道objc_autoreleasePoolPush
返回的是哨兵物件的地址,那麼在呼叫解構函式objc_autoreleasePoolPop
的時候傳入的也就是這個哨兵物件的地址。隨著方法的一步步呼叫,緊接著來看下AutoreleasePoolPage
的pop
方法的實現:
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
if (hotPage()) {
pop(coldPage()->begin());
} else {
setHotPage(nil);
}
return;
}
page = pageForPointer(token); //根據傳入的哨兵物件的地址,獲取到page中的哨兵物件之後的地址空間
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
} else {
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop); //對當前連結串列當中的物件進行release操作
if (DebugPoolAllocation && page->empty()) {
//釋放 `Autoreleased` 物件後,銷燬多餘的 page
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
page->kill();
setHotPage(nil);
}
else if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
複製程式碼
這裡重點看一下page->releaseUntil(stop)
方法:
void releaseUntil(id *stop)
{
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next;
memset((void*)page->next,SCRIBBLE,sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
}
複製程式碼
這裡的stop
同樣是POOL_BOUNDARY
的地址,這裡分析一下這個方法:
(1). 外部迴圈挨個遍歷autoreleased
物件,直到遍歷到哨兵物件POOL_BOUNDARY
。
(2). 如果當前page
沒有 POOL_BOUNDARY
,並且為空,則將hotPage
設定為當前page
的父節點。
(3). 給當前autoreleased
物件傳送release
訊息。
(4). 最後再次配置hotPage
。
4.autorelease
通過上面的分析已經知道了構造方法objc_autoreleasePoolPush
會建立AutoreleasePoolPage
,並插入哨兵物件POOL_BOUNDARY
,析構方法objc_autoreleasePoolPop
會對哨兵物件之後插入的物件傳送release
訊息,那麼在這兩個方法之間,物件通過呼叫autorelease
是怎麼插入到AutoreleasePoolPage
的呢?下面來看下autorelease
的原始碼實現:
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
複製程式碼
這裡的重點還是autoreleaseFast(obj)
;由於這裡插入物件的方法和AutoreleasePoolPage
呼叫push
方法的實現是一樣的,只不過push
操作插入的是一個 POOL_BOUNDARY
,而autorelease
操作插入的是一個具體的autoreleased
物件,在此處就不做多餘分析。
通過上面?的這些分析,已經大概知道AutoreleasePool
是怎樣的一個構造,以及內如是如何實現自動釋放的。
5.AutoreleasePool的巢狀
對於巢狀的AutoreleasePool
也是同樣的原理,在pop
的時候總會釋放物件到上次push
的位置為止,也就是哨兵位置,多層的pool
就是插入多個哨兵物件而已,然後根據哨兵物件來進行釋放,就像剝洋蔥一樣一層一層的,互不影響。
那麼這裡有個疑問,如果在AutoreleasePool
多層巢狀中是同一個物件呢,那麼會怎麼釋放?下面通過一個小例子?來看一下:
@autoreleasepool {
ZHPerson *person = [ZHPerson object];
NSLog(@"current count %d",_objc_rootRetainCount(person));
@autoreleasepool {
ZHPerson *person1 = person;
NSLog(@"current count %d",_objc_rootRetainCount(person));
@autoreleasepool {
ZHPerson *person2 = person;
NSLog(@"current count %d",_objc_rootRetainCount(person));
}
}
}
複製程式碼
列印結果如下:
這裡dealloc
方法只調用了一次,由上面的程式碼可知:當前person1
和person2
是對person
的引用,如果系統會為每一次引用都自動插入一個autorelease
,那麼物件在執行第一個autorelease
的時候,會呼叫objc_release(obj)
來釋放當前的物件,那麼當呼叫rootRelease()
的時候就會報錯,因為當前物件已經被釋放了,那麼也就是說對於引用的物件只會被釋放一次。(同一個物件不能夠反覆的autorelease
)
NSthread、NSRunLoop、AutoReleasePool
1.NSthread和AutoReleasePool
先來看個簡單的例子:
在temp
的位置設定一個斷點,然後在控制檯輸入watchpoint set variable temp
,
等到這個執行緒執行結束之後,來看一下左側邊欄的內容:
當執行到NSLog(@"thread end");
這句程式碼,表示執行緒執行結束,這裡,其實執行緒會先呼叫[NSthread exit]
,然後執行_pthread_tsd_cleanup
,清除當前執行緒的有關資源,然後呼叫tls_dealloc
,也就是把當前執行緒關聯的AutoReleasePool
釋放掉,最後呼叫weak_clear_no_lock
清除指標。
那麼這一系列過程就說明了:在NSThread
退出了之後,與NSThread
對應的AutoReleasePool
也會被自動清空,所以當一個執行緒結束的時候,就會回收♻️AutoReleasePool
中自動釋放的物件。
結論:
每一個執行緒都會維護自己的
AutoReleasePool
,而每一個AutoReleasePool
都會對應唯一一個執行緒,但是執行緒可以對應多個AutoReleasePool
。
2.NSRunLoop和AutoReleasePool
對於NSThread
只是一個簡單的執行緒,如果把它換成一個常駐執行緒呢?
NSTimer
,並將其常駐。利用同樣的方式,watchpoint set variable temp
,:
可以看到這裡NStimer
是被加入到子執行緒當中的,但是在子執行緒中,我們並沒有去寫關於AutoReleasePool
的內容,我們只知道test
做了autorelease
操作。下面回到原始碼中來看:
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj,page);
} else {
return autoreleaseNoPage(obj);
}
}
id *autoreleaseNoPage(id obj)
{
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
}
//這裡省略了部分程式碼
複製程式碼
所以從上面的原始碼我們可以得出結論:子執行緒在使用autorelease
物件的時候,會懶加載出來一個AutoreleasePoolPage
,然後將物件插入進去。
那麼問題又來了,autorelease
物件在什麼時候釋放的呢?也就說AutoreleasePoolPage
在什麼時候呼叫了pop
方法?
其實在上面建立一個NSThread
的時候,在呼叫[NSthread exit]
的時候,會釋放當前資源,也就是把當前執行緒關聯的autoReleasePool
釋放掉,而在這裡當RunLoop
執行完成退出的時候,也會執行pop
方法,這就說明了為什麼在子執行緒當中,我們沒有顯示的呼叫pop
,它也能釋放當前AutoreleasePool
的資源的原因。
3.主執行緒的NSRunLoop和AutoReleasePool
那麼在主執行緒的RunLoop
到底什麼時候把物件進行釋放回收的呢?
簡單粗暴點,直接在控制檯通過po [NSRunloop currentRunloop]
列印主執行緒的RunLoop
:
這裡,系統在主執行緒的RunLoop
裡註冊了兩個Observer
,回撥都是_wrapRunLoopWithAutoreleasePoolHandler
,第一個Observer
的狀態是activities = 0x1
,第二個Observer
的狀態是activities = 0xa0
,這兩種狀態代表什麼意思呢?
先在這裡插入一點RunLoop
的內容(RunLoop
的狀態列舉):
typedef CF_OPTIONS(CFOptionFlags,CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),// 1
kCFRunLoopBeforeTimers = (1UL << 1),// 2
kCFRunLoopBeforeSources = (1UL << 2),// 4
kCFRunLoopBeforeWaiting = (1UL << 5),// 32
kCFRunLoopAfterWaiting = (1UL << 6),// 64
kCFRunLoopExit = (1UL << 7),// 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製程式碼
0x1
代表的是kCFRunLoopEntry
,也就是說第一個 Observer
監視的事件是Entry
(即將進入Loop
時),其回撥內會呼叫_objc_autoreleasePoolPush()
建立一個自動釋放池。其order
優先順序是-2147483647,優先順序最高,保證建立自動釋放池發生在其他所有回撥之前。
0xa0
對應的是kCFRunLoopBeforeWaiting
和kCFRunLoopExit
,也就是說第二個Observer
監視了兩個事件:kCFRunLoopBeforeWaiting
準備進入休眠,kCFRunLoopExit
即將退出RunLoop
。在kCFRunLoopBeforeWaiting
事件時呼叫 _objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
釋放舊的自動釋放池並建立新的自動釋放池;在kCFRunLoopExit
事件時呼叫_objc_autoreleasePoolPop()
來釋放自動釋放池,同時這個Observer
的order
優先順序是 2147483647,優先順序最低,保證其釋放自動釋放池的操作發生在其他所有回撥之後。
所以在沒有手動增加AutoreleasePool
的情況下,Autorelease
物件都是在當前的runloop
迭代結束時釋放的,而它能夠釋放的原因是系統在每個runloop
迭代中都加入了自動釋放池push
和pop
操作。
總結:
對於不同執行緒,應當建立自己的
AutoReleasePool
。如果應用長期存在,應該定期drain
和建立新的AutoReleasePool
,AutoReleasePool
與RunLoop
與執行緒是一一對應的關係,AutoReleasePool
在RunLoop
在開始迭代時做push
操作,在RunLoop
休眠或者迭代結束時做pop
操作。
AutoreleasePool的應用場景
通常情況下我們是不需要手動建立AutoreleasePool
,但是也有一些特殊的:
-
編寫的程式不基於
UI
框架,如命令列程式。 -
在迴圈中建立大量臨時物件時用以降低記憶體佔用峰值。
-
在主執行緒之外建立新的執行緒,在新執行緒開始執行處,建立自己的
AutoreleasePool
,否則將導致記憶體洩漏。
下面就來簡單看下第二種情況,直接來個for
迴圈:
for (int i = 0; i < 100000000; i ++) {
NSString * str = [NSString stringWithFormat:@"noAutoReleasePool"];
NSString *tempstr = str;
}
}
複製程式碼
來看一下Memory
的使用情況:
相反的,如果加上AutoreleasePool
,來看一下:
for (int i = 0; i < 100000000; i ++) {
@autoreleasepool {
NSString * str = [NSString stringWithFormat:@"AutoReleasePool"];
NSString *tempstr = str;
}
}
複製程式碼
來看一下這種情況下的Memory
的使用情況:
這個做個備註:在主函式main.m
檔案中的@autoreleasepool
,如果在這裡做個測試,使用for
迴圈建立大量的臨時物件,是否加上這個@autoreleasepool
對Memory
的使用情況沒有特別大的影響。
總結
寫到這裡,對於AutoReleasePool
學習內容就暫告一段了,正常情況下,我們不需要去關心AutoReleasePool
的建立和釋放,但是學習理解了AutoReleasePool
能夠使我們更加理解ARC
模式下系統是怎樣來管理記憶體的。
文中內容如有不當之處,還請指出,謝謝您!