內(nèi)存管理之中autorelease部分是相當(dāng)重要的,雖然現(xiàn)在都是ARC的時代了,我們還是要盡量去理解每一個原理,這對于我們理解代碼的實現(xiàn)和原理是有很大的幫助的.MRC中涨共,調(diào)用[obj autorelease]
來延遲內(nèi)存的釋放是一件簡單自然的事纽帖,ARC下,我們甚至可以完全不知道Autorelease就能管理好內(nèi)存举反。那么接下來我們就理解下autorelease的原理.
1. autorelease原理
autorelease原理是什么呢?我們可以通過一行代碼來觀看 :
@autoreleasepool {
Person *p = [[[Person alloc] init] autorelease];
}
這個是很簡單的MRC的代碼,我們可以通過把OC代碼轉(zhuǎn)換成C++代碼:
{
__AtAutoreleasePool __autoreleasepool;
MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
}
上邊的代碼看起來還是有一些繁瑣,我們再把C++代碼簡化:
{
__AtAutoreleasePool __autoreleasepool;
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
最后簡化成的代碼中的__AtAutoreleasePool
是什么? __AtAutoreleasePool是一個結(jié)構(gòu)體
struct __AtAutoreleasePool{
__AtAutoreleasePool{//構(gòu)造函數(shù),在創(chuàng)建結(jié)構(gòu)體變量的時候調(diào)用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
__AtAutoreleasePool{//析構(gòu)函數(shù),在結(jié)構(gòu)體銷毀的時候調(diào)用
objc_atautoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
在這個結(jié)構(gòu)體中第一次函數(shù)是一個C++的構(gòu)造函數(shù),objc_autoreleasePoolPush();
這個方法會在創(chuàng)建結(jié)構(gòu)體變量的時候調(diào)用.
第二個函數(shù)是一個C++的析構(gòu)函數(shù),objc_atautoreleasePoolPop(atautoreleasepoolobj);
這個方法會在結(jié)構(gòu)體銷毀的時候調(diào)用.
所以上面的代碼可以轉(zhuǎn)化為:
{
atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *person = [[[Person alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
一. AutoreleasePoolPage的結(jié)構(gòu)
自動釋放池的主要底層數(shù)據(jù)結(jié)構(gòu)是:__AtAutoreleasePool
懊直、AutoreleasePoolPage
,來看一下objc_autoreleasePoolPush 和 objc_autoreleasePoolPop 的實現(xiàn):
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
上面的方法看上去是對 AutoreleasePoolPage 對應(yīng)靜態(tài)方法 push 和 pop 的封裝.調(diào)用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的.
AutoreleasePoolPage 是一個 C++ 中的類,它在 NSObject.mm 中的定義是這樣的:
class AutoreleasePoolPage
{
magic_t const magic;
id *next;
pthread const thread;
AutoreleasePoolPage *const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
①. magic 用于對當(dāng)前
AutoreleasePoolPage
完整性的校驗.
②. thread 保存了當(dāng)前頁所在的線程.
每個AutoreleasePoolPage對象占用4096字節(jié)內(nèi)存火鼻,除了用來存放它內(nèi)部的成員變量室囊,剩下的空間用來存放autorelease對象的地址.
//源碼中的代碼是這樣的
#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES
③. parent 和child 就是用來構(gòu)造雙向鏈表的指針.
自動釋放池中的 AutoreleasePoolPage
是以雙向鏈表的形式連接起來的:
在自動釋放池中是存在自動釋放池中的棧,被初始化在0x1000~0x2000.其中有56bit用于存儲AutoreleasePoolPage
的成員變量,剩下的 0x1038 ~ 0x2000 都是用來存儲加入到自動釋放池中的對象.begin() 和 end() 這兩個類的實例方法幫助我們快速獲取0x1038 ~ 0x2000 這一范圍的邊界地址.
④. next 指向了下一個為空的內(nèi)存地址瘦陈,如果 next指向的地址加入一個 object波俄,它就會如下圖所示移動到下一個為空的內(nèi)存地址中:
autoreleasePool的雙向鏈表的執(zhí)行步驟是:
- 調(diào)用push方法會將一個
POOL_BOUNDARY
入棧捉貌,并且返回其存放的內(nèi)存地址趁窃,即返回給atautoreleasepoolobj醒陆。 - 調(diào)用pop方法時傳入一個
POOL_BOUNDARY
的內(nèi)存地址刨摩,會從最后一個入棧的對象開始發(fā)送release消息澡刹,直到遇到這個POOL_BOUNDARY
. - id *next指針作為游標(biāo)指向棧頂最新add進(jìn)來的autorelease對象的下一個位置.
- 一個AutoreleasePoolPage的空間被占滿時罢浇,會新建一個AutoreleasePoolPage對象嚷闭,連接鏈表胞锰,后來的autorelease對象在新的page加入.
⑤.
POOL_SENTINEL
(哨兵對象)
POOL_SENTINEL
是nil
的另一個名稱
#define POOL_SENTINEL nil
在每個自動釋放池初始化調(diào)用objc_autoreleasePoolPush
的時候胜蛉,都會把一個POOL_SENTINEL push
到自動釋放池的棧頂,并且返回這個 POOL_SENTINEL
哨兵對象的地址暖璧。
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
上面的autoreleasepoolobj
就是一個POOL_SENTINEL
澎办。
而當(dāng)方法objc_autoreleasePoolPop
調(diào)用時麦锯,就會向自動釋放池中的對象發(fā)送release
消息扶欣,直到第一個POOL_SENTINEL
:
objc_autoreleasePoolPush
的返回值正是這個哨兵對象的地址料祠,objc_autoreleasePoolPop
(哨兵對象)作為入?yún)⒊谑牵?/p>
- 根據(jù)傳入的哨兵對象地址找到哨兵對象所處的page.
- 在當(dāng)前page中括饶,將晚于哨兵對象插入的所有autorelease對象都發(fā)送一次 release消息巷帝,并向回移動next指針到正確位置.
- 補充2:從最新加入的對象一直向前清理楞泼,可以向前跨越若干個page堕阔,直到哨兵所在的page.
A.
objc_autoreleasePoolPush
方法
了解了POOL_SENTINEL
牺弹,我們來重新回顧一下objc_autoreleasePoolPush
方法:
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
它調(diào)用AutoreleasePoolPage
的類方法push张漂,也非常簡單:
static inline void *push() {
return autoreleaseFast(POOL_SENTINEL);
}
在這里會進(jìn)入一個比較關(guān)鍵的方法autoreleaseFast
航攒,并傳入哨兵對象 POOL_SENTINEL
:
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);
}
}
上述方法分三種情況選擇不同的代碼執(zhí)行:
①.有 hotPage (正在使用的AutoreleasePoolPage
)并且當(dāng)前 page 不滿.
????·調(diào)用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage
的棧中.
②.有 hotPage 并且當(dāng)前 page 已滿.
????·調(diào)用 autoreleaseFullPage
初始化一個新的頁.
????·調(diào)用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage
的棧中.
③. 無 hotPage
????·調(diào)用 autoreleaseNoPage 創(chuàng)建一個 hotPage.
????·調(diào)用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage
的棧中.
④. 最后的都會調(diào)用 page->add(obj) 將對象添加到自動釋放池中币他。
a.
page->add
添加對象
id *add(id obj)
將對象添加到自動釋放池頁中:
id *add(id obj) {
id *ret = next;
*next = obj;
next++;
return ret;
}
這個方法其實就是一個壓棧的操作蝴悉,將對象加入 AutoreleasePoolPage 然后移動棧頂?shù)闹羔槨?/p>
b.
autoreleaseFullPage
(當(dāng)前 hotPage 已滿)
autoreleaseFullPage
會在當(dāng)前的hotPage
已滿的時候調(diào)用:
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
它會從傳入的 page 開始遍歷整個雙向鏈表,直到:
1.查找到一個未滿的AutoreleasePoolPage
.
2.使用構(gòu)造器傳入parent
創(chuàng)建一個新的AutoreleasePoolPage
.
在查找到一個可以使用的 AutoreleasePoolPage
之后廉丽,會將該頁面標(biāo)記成 hotPage
,然后調(diào)動上面分析過的page->add
方法添加對象欣福。
c. autoreleaseNoPage(沒有 hotPage)
如果當(dāng)前內(nèi)存中不存在 hotPage
拓劝,就會調(diào)用autoreleaseNoPage
方法初始化一個AutoreleasePoolPage
:
static id *autoreleaseNoPage(id obj) {
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
if (obj != POOL_SENTINEL) {
page->add(POOL_SENTINEL);
}
return page->add(obj);
}
既然當(dāng)前內(nèi)存中不存在AutoreleasePoolPage
郑临,就要從頭開始構(gòu)建這個自動釋放池的雙向鏈表厢洞,也就是說躺翻,新的AutoreleasePoolPage
是沒有 parent 指針的公你。
初始化之后陕靠,將當(dāng)前頁標(biāo)記為hotPage
剪芥,然后會先向這個page
中添加一個 POOL_SENTINEL
對象粗俱,來確保在pop調(diào)用的時候寸认,不會出現(xiàn)異常偏塞。
最后灸叼,將obj
添加到自動釋放池中古今。
B. objc_autoreleasePoolPop 方法
同樣滔以,回顧一下上面提到的 objc_autoreleasePoolPop 方法:
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
static inline void pop(void *token) {
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token;
page->releaseUntil(stop);
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
}
①.使用 pageForPointer獲取當(dāng)前 token所在的 AutoreleasePoolPage.
②.調(diào)用 releaseUntil 方法釋放棧中的對象抵碟,直到 stop.
③.調(diào)用 child 的 kill 方法.
a.
pageForPointer
獲取AutoreleasePoolPage
pageForPointer
方法主要是通過內(nèi)存地址的操作坏匪,獲取當(dāng)前指針?biāo)陧摰氖椎刂罚?/p>
static AutoreleasePoolPage *pageForPointer(const void *p) {
return pageForPointer((uintptr_t)p);
}
static AutoreleasePoolPage *pageForPointer(uintptr_t p) {
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE;
assert(offset >= sizeof(AutoreleasePoolPage));
result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();
return result;
}
將指針與頁面的大小敦迄,也就是 4096 取模颅崩,得到當(dāng)前指針的偏移量蕊苗,因為所有的 AutoreleasePoolPage 在內(nèi)存中都是對齊的
p = 0x100816048
p % SIZE = 0x48
result = 0x100816000
而最后調(diào)用的方法fastCheck()
用來檢查當(dāng)前的result
是不是一個 AutoreleasePoolPage
朽砰。
b. releaseUntil 釋放對象
releaseUntil 方法的實現(xiàn)如下:
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_SENTINEL) {
objc_release(obj);
}
}
setHotPage(this);
}
它的實現(xiàn)還是很容易的瞧柔,用一個while
循環(huán)持續(xù)釋放AutoreleasePoolPage
中的內(nèi)容造锅,直到 next
指向了stop
哥蔚。
使用memset
將內(nèi)存的內(nèi)容設(shè)置成 SCRIBBLE
,然后使用objc_release
釋放對象渤愁。
c. kill() 方法
到這里抖格,沒有分析的方法就只剩下 kill 了雹拄,而它會將當(dāng)前頁面以及子頁面全部刪除:
void kill() {
AutoreleasePoolPage *page = this;
while (page->child) page = page->child;
AutoreleasePoolPage *deathptr;
do {
deathptr = page;
page = page->parent;
if (page) {
page->unprotect();
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}
二. 代碼分析
如果代碼中是嵌套形式的autorelease是什么樣子的?
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool { // r1 = push()
Person *p1 = [[[Person alloc] init] autorelease];
Person *p2 = [[[Person alloc] init] autorelease];
@autoreleasepool { // r2 = push()
for (int i = 0; i < 5; i++) {
Person *p3 = [[[Person alloc] init] autorelease];
}
@autoreleasepool { // r3 = push()
Person *p4 = [[[Person alloc] init] autorelease];
_objc_autoreleasePoolPrint();
} // pop(r3)
} // pop(r2)
} // pop(r1)
return 0;
}
- 因為只打印了一個PAGE坪哄,所以說明他們是在同一個AutoreleasePoolPage损姜,只是每次一個新的autoreleasepool摧阅,都會插入一個POOL_BOUNDARY棒卷。
- 每次釋放對象時比规,都是從后往前釋放拦英,直到遇到POOL_BOUNDARY為止灾常。
那如果對象特別多又是什么樣子的呢?
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool { // r1 = push()
Person *p1 = [[[Person alloc] init] autorelease];
Person *p2 = [[[Person alloc] init] autorelease];
@autoreleasepool { // r2 = push()
for (int i = 0; i < 5; i++) {
Person *p3 = [[[Person alloc] init] autorelease];
}
@autoreleasepool { // r3 = push()
Person *p4 = [[[Person alloc] init] autorelease];
_objc_autoreleasePoolPrint();
} // pop(r3)
} // pop(r2)
} // pop(r1)
return 0;
}
2. autorelease和runloop的結(jié)合使用
MRC的autorelease
對象在什么時機會被調(diào)用release
?
- (void)viewDidLoad {
[super viewDidLoad];
// 這個Person什么時候調(diào)用release,是由RunLoop來控制的
// 它可能是在某次RunLoop循環(huán)中慷荔,RunLoop休眠之前調(diào)用了release
MJPerson *person = [[[MJPerson 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__);
}
iOS在主線程的Runloop中注冊了2個Observer
- 第1個Observer監(jiān)聽了kCFRunLoopEntry事件贷岸,會調(diào)用objc_autoreleasePoolPush().
- 第2個Observer:
- 監(jiān)聽了kCFRunLoopBeforeWaiting事件墓卦,會調(diào)用objc_autoreleasePoolPop()户敬、objc_autoreleasePoolPush()
- 監(jiān)聽了kCFRunLoopBeforeExit事件尿庐,會調(diào)用objc_autoreleasePoolPop().
在ARC中方法里有局部對象抄瑟, 出了方法后會立即釋放嗎?
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
通過打印結(jié)果可知皮假,當(dāng)person對象出了其作用域后就銷毀惹资,即系統(tǒng)會在它出作用域的時候褪测,自動調(diào)用其release方法侮措。
autoreleasePool
在何時被釋放分扎?
App啟動后畏吓,蘋果在主線程 RunLoop 里注冊了兩個 Observer
庵佣,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()
巴粪。
第一個Observer
監(jiān)視的事件是 Entry(即將進(jìn)入Loop)肛根,其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush()
創(chuàng)建自動釋放池派哲。其 order 是 -2147483647,優(yōu)先級最高储矩,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前持隧。
第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting
(準(zhǔn)備進(jìn)入休眠) 時調(diào)用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時調(diào)用_objc_autoreleasePoolPop()
來釋放自動釋放池褥实。這個 Observer 的 order 是 2147483647哥艇,優(yōu)先級最低她奥,保證其釋放池子發(fā)生在其他所有回調(diào)之后哩俭。
在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)凡资、Timer回調(diào)內(nèi)的隙赁。這些回調(diào)會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著,所以不會出現(xiàn)內(nèi)存泄漏厚掷,開發(fā)者也不必顯示創(chuàng)建 Pool 了。
想了解更多iOS學(xué)習(xí)知識請聯(lián)系:QQ(814299221)