@autoreleasepool {
// Code benefitting from a local autorelease pool.
}
1、原理分析
1.1攀例、__AtAutoreleasePool
下面我們先通過(guò)macOS工程來(lái)分析@autoreleasepool
的底層原理沼本。 macOS工程中的main()函數(shù)什么都沒(méi)做,只是放了一個(gè)@autoreleasepool
笑窜。
int main(int argc, const char * argv[]) {
@autoreleasepool {}
return 0;
}
通過(guò) Clang clang -rewrite-objc main.m
將以上代碼轉(zhuǎn)換為 C++ 代碼。
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 構(gòu)造函數(shù)瓤檐,在創(chuàng)建結(jié)構(gòu)體的時(shí)候調(diào)用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析構(gòu)函數(shù)赂韵,在結(jié)構(gòu)體銷毀的時(shí)候調(diào)用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool; }
return 0;
}
可以看到:
@autoreleasepool
底層是創(chuàng)建了一個(gè)__AtAutoreleasePool
結(jié)構(gòu)體對(duì)象;在創(chuàng)建
__AtAutoreleasePool
結(jié)構(gòu)體時(shí)會(huì)在構(gòu)造函數(shù)中調(diào)用objc_autoreleasePoolPush()
函數(shù)挠蛉,并返回一個(gè)atautoreleasepoolobj
(POOL_BOUNDARY存放的內(nèi)存地址祭示,下面會(huì)講到);在釋放
__AtAutoreleasePool
結(jié)構(gòu)體時(shí)會(huì)在析構(gòu)函數(shù)中調(diào)用objc_autoreleasePoolPop()
函數(shù)谴古,并將atautoreleasepoolobj
傳入绍移。
1.2、AutoreleasePoolPage
下面我們進(jìn)入Runtime objc4源碼查看以上提到的兩個(gè)函數(shù)的實(shí)現(xiàn)讥电。
// NSObject.mm
void * objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
可以得知蹂窖,
objc_autoreleasePoolPush()
和objc_autoreleasePoolPop()
兩個(gè)函數(shù)其實(shí)是調(diào)用了AutoreleasePoolPage
類的兩個(gè)類方法push()
和pop()
。所以@autoreleasepool
底層就是使用AutoreleasePoolPage
類來(lái)實(shí)現(xiàn)的恩敌。
自動(dòng)釋放池的數(shù)據(jù)結(jié)構(gòu)
- 自動(dòng)釋放池的主要數(shù)據(jù)結(jié)構(gòu)是:
__AtAutoreleasePool
瞬测、AutoreleasePoolPage
; - 調(diào)用了 autorelease的對(duì)象最終都是通過(guò)
AutoreleasePoolPage
對(duì)象來(lái)管理的纠炮;
下面我們來(lái)看一下AutoreleasePoolPage
類的定義:
class AutoreleasePoolPage
{
# define EMPTY_POOL_PLACEHOLDER ((id*)1) // EMPTY_POOL_PLACEHOLDER:表示一個(gè)空自動(dòng)釋放池的占位符
# define POOL_BOUNDARY nil // POOL_BOUNDARY:哨兵對(duì)象
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 用來(lái)標(biāo)記已釋放的對(duì)象
static size_t const SIZE = // 每個(gè) Page 對(duì)象占用 4096 個(gè)字節(jié)內(nèi)存
#if PROTECT_AUTORELEASEPOOL // PAGE_MAX_SIZE = 4096
PAGE_MAX_SIZE; // must be muliple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id); // Page 的個(gè)數(shù)
magic_t const magic; // 用來(lái)校驗(yàn) Page 的結(jié)構(gòu)是否完整
id *next; // 指向下一個(gè)可存放 autorelease 對(duì)象地址的位置月趟,初始化指向 begin()
pthread_t const thread; // 指向當(dāng)前線程
AutoreleasePoolPage * const parent; // 指向父結(jié)點(diǎn),首結(jié)點(diǎn)的 parent 為 nil
AutoreleasePoolPage *child; // 指向子結(jié)點(diǎn)恢口,尾結(jié)點(diǎn)的 child 為 nil
uint32_t const depth; // Page 的深度孝宗,從 0 開(kāi)始遞增
uint32_t hiwat;
......
}
整個(gè)程序運(yùn)行過(guò)程中,可能會(huì)有多個(gè)AutoreleasePoolPage
對(duì)象耕肩。從它的定義可以得知:
自動(dòng)釋放池(即所有的
AutoreleasePoolPage
對(duì)象)是以棧為結(jié)點(diǎn)通過(guò)雙向鏈表的形式組合而成因妇;自動(dòng)釋放池與線程一一對(duì)應(yīng);
每個(gè)
AutoreleasePoolPage
對(duì)象占用4096
字節(jié)內(nèi)存猿诸,其中56
個(gè)字節(jié)用來(lái)存放它內(nèi)部的成員變量婚被,剩下的空間(4040
個(gè)字節(jié))用來(lái)存放autorelease對(duì)象的地址
。
其內(nèi)存分布圖如下:
1.2.1梳虽、POOL_BOUNDARY
在分析這些方法之前址芯,先介紹一下POOL_BOUNDARY
。
-
POOL_BOUNDARY
的前世叫做POOL_SENTINEL
窜觉,稱為哨兵對(duì)象或者邊界對(duì)象谷炸; -
POOL_BOUNDARY
用來(lái)區(qū)分不同的自動(dòng)釋放池,以解決自動(dòng)釋放池嵌套的問(wèn)題禀挫; - 每當(dāng)創(chuàng)建一個(gè)自動(dòng)釋放池旬陡,就會(huì)調(diào)用
push()
方法將一個(gè)POOL_BOUNDARY
入棧,并返回其存放的內(nèi)存地址特咆; - 當(dāng)往自動(dòng)釋放池中添加
autorelease對(duì)象
時(shí)季惩,將autorelease對(duì)象的內(nèi)存地址
入棧录粱,它們前面至少有一個(gè)POOL_BOUNDARY
腻格; - 當(dāng)銷毀一個(gè)自動(dòng)釋放池時(shí)画拾,會(huì)調(diào)用
pop()
方法并傳入一個(gè)POOL_BOUNDARY
,會(huì)從自動(dòng)釋放池中最后一個(gè)對(duì)象開(kāi)始菜职,依次給它們發(fā)送release消息
青抛,直到遇到這個(gè)POOL_BOUNDARY
。
1.2.2酬核、push
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) { // 出錯(cuò)時(shí)進(jìn)入調(diào)試狀態(tài)
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY); // 傳入 POOL_BOUNDARY 哨兵對(duì)象
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
當(dāng)創(chuàng)建一個(gè)自動(dòng)釋放池時(shí)蜜另,會(huì)調(diào)用push()
方法。push()
方法中調(diào)用了autoreleaseFast()
方法并傳入了POOL_BOUNDARY
哨兵對(duì)象嫡意。
下面我們來(lái)看一下autoreleaseFast()
方法的實(shí)現(xiàn):
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage(); // 雙向鏈表中的最后一個(gè) Page
if (page && !page->full()) { // 如果當(dāng)前 Page 存在且未滿
return page->add(obj); // 將 autorelease 對(duì)象入棧举瑰,即添加到當(dāng)前 Page 中;
} else if (page) { // 如果當(dāng)前 Page 存在但已滿
return autoreleaseFullPage(obj, page); // 創(chuàng)建一個(gè)新的 Page蔬螟,并將 autorelease 對(duì)象添加進(jìn)去
} else { // 如果當(dāng)前 Page 不存在此迅,即還沒(méi)創(chuàng)建過(guò) Page
return autoreleaseNoPage(obj); // 創(chuàng)建第一個(gè) Page,并將 autorelease 對(duì)象添加進(jìn)去
}
}
autoreleaseFast()
中先是調(diào)用了hotPage()
方法獲得未滿的Page
旧巾,從AutoreleasePoolPage
類的定義可知耸序,每個(gè)Page
的內(nèi)存大小為 4096
個(gè)字節(jié),每當(dāng)Page
滿了的時(shí)候鲁猩,就會(huì)創(chuàng)建一個(gè)新的Page
坎怪。hotPage()
方法就是用來(lái)獲得這個(gè)新創(chuàng)建的未滿的Page
。
autoreleaseFast()
在執(zhí)行過(guò)程中有三種情況:
- ① 當(dāng)前
Page
存在且未滿時(shí)廓握,通過(guò)page->add(obj)
將autorelease對(duì)象入棧搅窿,即添加到當(dāng)前Page
中;
② 當(dāng)前Page
存在但已滿時(shí)隙券,通過(guò)autoreleaseFullPage(obj, page)
創(chuàng)建一個(gè)新的Page
戈钢,并將autorelease對(duì)象添加進(jìn)去;
③ 當(dāng)前Page
不存在是尔,即還沒(méi)創(chuàng)建過(guò)Page
殉了,通過(guò)autoreleaseNoPage(obj)
創(chuàng)建第一個(gè)Page
,并將autorelease對(duì)象添加進(jìn)去拟枚。
1.2.3薪铜、pop
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
if (hotPage()) {
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
pop(coldPage()->begin());
} else {
// Pool was never used. Clear the placeholder.
setHotPage(nil);
}
return;
}
page = pageForPointer(token);
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);
// memory: delete empty children
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
}
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
pop()
方法的傳參token
即為POOL_BOUNDARY
對(duì)應(yīng)在Page
中的地址。當(dāng)銷毀自動(dòng)釋放池時(shí)恩溅,會(huì)調(diào)用pop()
方法將自動(dòng)釋放池中的autorelease對(duì)象
全部釋放(實(shí)際上是從自動(dòng)釋放池的中的最后一個(gè)入棧的autorelease對(duì)象開(kāi)始隔箍,依次給它們發(fā)送一條release消息,直到遇到這個(gè)POOL_BOUNDARY
)脚乡。pop()
方法的執(zhí)行過(guò)程如下:
- ① 判斷
token
是不是EMPTY_POOL_PLACEHOLDER
蜒滩,是的話就清空這個(gè)自動(dòng)釋放池滨达; - ② 如果不是的話,就通過(guò)
pageForPointer(token)
拿到token
所在的Page
(自動(dòng)釋放池的首個(gè)Page)俯艰; - ③ 通過(guò)
page->releaseUntil(stop)
將自動(dòng)釋放池中的autorelease對(duì)象
全部釋放捡遍,傳參stop
即為POOL_BOUNDARY
的地址; - ④ 判斷當(dāng)前
Page
是否有子Page
竹握,有的話就銷毀画株。
1.2.4、begin啦辐、end谓传、empty、full
下面再來(lái)看一下begin
芹关、end
续挟、empty
、full
這些方法的實(shí)現(xiàn)侥衬。
-
begin
的地址為:Page自己的地址+Page對(duì)象的大小56個(gè)字節(jié)诗祸; -
end
的地址為:Page自己的地址+4096個(gè)字節(jié); -
empty
判斷Page是否為空的條件是next地址是不是等于begin浇冰; -
full
判斷Page是否已滿的條件是next地址是不是等于end(棧頂)贬媒。
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
bool empty() {
return next == begin();
}
bool full() {
return next == end();
}
2、查看自動(dòng)釋放池的情況
可以通過(guò)以下私有函數(shù)來(lái)查看自動(dòng)釋放池的情況:
extern void _objc_autoreleasePoolPrint(void);
3肘习、iOS 工程示例分析
在iOS工程中际乘,方法里的autorelease對(duì)象
是什么時(shí)候釋放的呢?
有系統(tǒng)干預(yù)釋放和手動(dòng)干預(yù)釋放兩種情況漂佩。
- 系統(tǒng)干預(yù)釋放是不指定
@autoreleasepool
脖含,所有autorelease對(duì)象
都由主線程的RunLoop創(chuàng)建的@autoreleasepool
來(lái)管理。 - 手動(dòng)干預(yù)釋放就是將
autorelease對(duì)象
添加進(jìn)我們手動(dòng)創(chuàng)建的@autoreleasepool
中投蝉。
下面還是在MRC環(huán)境下進(jìn)行分析养葵。
3.1、系統(tǒng)干預(yù)釋放
我們先來(lái)看以下 Xcode 11 版本的iOS程序中的main()函數(shù)瘩缆,和舊版本的差異关拒。
// Xcode 11
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
// Xcode 舊版本
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
新版本 Xcode 11 中的 main 函數(shù)發(fā)生了哪些變化?
舊版本是將整個(gè)應(yīng)用程序運(yùn)行放在@autoreleasepool內(nèi)庸娱,由于RunLoop的存在着绊,要return即程序結(jié)束后@autoreleasepool作用域才會(huì)結(jié)束,這意味著程序結(jié)束后main函數(shù)中的@autoreleasepool中的autorelease對(duì)象才會(huì)釋放熟尉。
而在 Xcode 11中归露,觸發(fā)主線程RunLoop的UIApplicationMain函數(shù)放在了@autoreleasepool外面,這可以保證@autoreleasepool中的autorelease對(duì)象在程序啟動(dòng)后立即釋放斤儿。正如新版本的@autoreleasepool中的注釋所寫 “Setup code that might create autoreleased objects goes here.”(如上代碼)剧包,可以將autorelease對(duì)象放在此處恐锦。
接著我們來(lái)看 “系統(tǒng)干預(yù)釋放” 情況的示例:
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[[Person alloc] init] autorelease];
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[Person dealloc]
// -[ViewController viewDidAppear:]
可以看到,調(diào)用了autorelease
方法的person對(duì)象
不是在viewDidLoad方法
結(jié)束后釋放疆液,而是在viewWillAppear方法
結(jié)束后釋放一铅,說(shuō)明在viewWillAppear方法
結(jié)束的時(shí)候,調(diào)用了pop()
方法釋放了person對(duì)象
枚粘。
其實(shí)這是由RunLoop
控制的馅闽,下面來(lái)講解一下RunLoop
和@autoreleasepool
的關(guān)系飘蚯。
3.2馍迄、RunLoop 與 @autoreleasepool
iOS在主線程的RunLoop
中注冊(cè)了兩個(gè)Observer
。
第1個(gè)Observer
- 監(jiān)聽(tīng)了
kCFRunLoopEntry
事件局骤,會(huì)調(diào)用objc_autoreleasePoolPush()
攀圈;
第2個(gè)Observer
- ① 監(jiān)聽(tīng)了
kCFRunLoopBeforeWaiting
事件,會(huì)調(diào)用objc_autoreleasePoolPop()
峦甩、objc_autoreleasePoolPush()
赘来; - ② 監(jiān)聽(tīng)了
kCFRunLoopBeforeExit
事件,會(huì)調(diào)用objc_autoreleasePoolPop()
凯傲。
所以犬辰,在iOS工程中系統(tǒng)干預(yù)釋放的autorelease對(duì)象
的釋放時(shí)機(jī)是由RunLoop
控制的,會(huì)在當(dāng)前RunLoop
每次循環(huán)結(jié)束時(shí)釋放冰单。以上person對(duì)象在viewWillAppear方法
結(jié)束后釋放幌缝,說(shuō)明viewDidLoad
和viewWillAppear
方法在同一次循環(huán)里。
-
kCFRunLoopEntry
:在即將進(jìn)入RunLoop
時(shí)诫欠,會(huì)自動(dòng)創(chuàng)建一個(gè)__AtAutoreleasePool
結(jié)構(gòu)體對(duì)象涵卵,并調(diào)用objc_autoreleasePoolPush()
函數(shù)。
-kCFRunLoopBeforeWaiting
:在RunLoop
即將休眠時(shí)荒叼,會(huì)自動(dòng)銷毀一個(gè)__AtAutoreleasePool
對(duì)象,調(diào)用objc_autoreleasePoolPop()
。然后創(chuàng)建一個(gè)新的__AtAutoreleasePool
對(duì)象潮针,并調(diào)用objc_autoreleasePoolPush()
谅海。 -
kCFRunLoopBeforeExit
,在即將退出RunLoop
時(shí)嫁乘,會(huì)自動(dòng)銷毀最后一個(gè)創(chuàng)建的__AtAutoreleasePool
對(duì)象昆婿,并調(diào)用objc_autoreleasePoolPop()
。
3.3亦渗、手動(dòng)干預(yù)釋放
我們?cè)賮?lái)看一下手動(dòng)干預(yù)釋放的情況挖诸。
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
Person *person = [[[Person alloc] init] autorelease];
}
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
// -[Person dealloc]
// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[ViewController viewDidAppear:]
可以看到,添加進(jìn)手動(dòng)指定的@autoreleasepool
中的autorelease對(duì)象
法精,在@autoreleasepool
大括號(hào)結(jié)束時(shí)就會(huì)釋放多律,不受RunLoop
控制痴突。