當你需要延遲調用release方法的時候會使用autorelease。如:
- (NSString *)fullName {
NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@",
self.firstName, self.lastName] autorelease];
return string;
}
在上面代碼中,通過alloc生成并持有對象,根據iOS記憶體管理規則,在失去對象的引用之前,我們必須要放棄該對象的所有權。如果使用release,那麼對象在傳回之前就會失去所有權,導緻傳回一個無用的對象。使用autorelease,會保證調用放在使用該傳回值之前,該對象不會釋放,也就是說,autorelease可以保證對象在跨越“方法調用邊界”後存活。
這時你可能會有一個疑問,使用autorelease的對象到底在什麼時候釋放呢?接下來,我們将會圍繞這個問題逐漸進行研究。
AutoreleasePool是如何工作的?
Coco架構會幫我們維護一個autorelease pool用來将對象臨時儲存在記憶體中,以便稍後釋放。
iOS應用程式是在event loop(事件循環)中運作的。當應用程式加載時,程式就會進入事件循環并等待使用者的互動事件。當一個點選事件發生時,Cocoa架構會檢測到該事件,然後建立事件對象,并生成一個autorelease pool對象。整個流程如下表所示:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38FdsYkRGZkRG9lcvx2bjxiNx8VZ6l2csIzaE10drRVT2R2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL2MDN4UTOxMTM2IjNwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
當事件處理方法完成執行時,控制權會傳回給Cocoa架構。Cocoa架構會銷毀autorelease Pool,它會向池子中的每一個對象都發送一條release消息。
autorelease實作
通過檢視Objc源碼,來确認autorlease的實作:
- (id)autorelease {
return ((id)self)->rootAutorelease();
}
inline id objc_object::rootAutorelease()
{
assert(!UseGC);
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
id objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
通過源碼可以發現,當調用對象的autorelease方法時,最終調用的是AutoreleasePoolPage的autorelease方法。
接下來我們看一下AutoreleasePoolPage的實作,
AutoreleasePoolPage是使用C++實作的一個類,截取源碼中的主要部分,其實作如下:
class AutoreleasePoolPage
{
#define POOL_SENTINEL nil
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
public:
static inline id autorelease(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_SENTINEL);
} else {
dest = autoreleaseFast(POOL_SENTINEL);
}
assert(*dest == POOL_SENTINEL);
return dest;
}
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
page = pageForPointer(token);
stop = (id *)token;
if (DebugPoolAllocation && *stop != POOL_SENTINEL) {
// This check is not valid with DebugPoolAllocation off
// after an autorelease with a pool page but no pool in place.
_objc_fatal("invalid or prematurely-freed autorelease pool %p; ",
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();
}
}
}
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next;
*next++ = obj;
protect();
return ret;
}
void releaseAll()
{
releaseUntil(begin());
}
}
通過分析上面的源碼,我們可以了解到對象調用autorelease方法的過程如下:
- 擷取正在使用的AutoreleasePoolPage對象。
- 擷取目前AutoreleasePoolPage對象的next指針,将autorelease對象位址複制給next指針,next指針向上移動一個位置。
- 若一個AutoreleasePoolPage空間被占滿時,會建立一個AutoreleasePoolPage對象,連結連結清單,後來的autorelease對象會加入到新的page中。
AutoreleasePool的生命周期
在ARC下,我們使用@autoreleasepool{}來使用AutoreleasePool:
@autoreleasepool {
}
使用clang(clang -rewrite-objc)指令将其轉化成C++代碼,關鍵代碼如下:
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
__AtAutoreleasePool()
{
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool()
{
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
是以我們可以得出結論:Autorelease Pool的生命周期是由 void * objc_autoreleasePoolPush(void)和void objc_autoreleasePoolPop(void *)這兩個方法實作的。
通過檢視Objc源碼,确定這兩個方法的實作:
void *objc_autoreleasePoolPush(void)
{
if (UseGC) return nil;
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
if (UseGC) return;
AutoreleasePoolPage::pop(ctxt);
}
通過源碼,我們可以确定,void * objc_autoreleasePoolPush(void)和 void objc_autoreleasePoolPop(void *)這兩個方法是對AutoreleasePoolPage::push()和AutoreleasePoolPage::pop(ctxt)方法的封裝。
通過分析AutoreleasePoolPage源碼中push()和pop()方法的實作,我們可以知道AutoreleasePool對象的生命周期
push()過程:
- 擷取目前正在使用的AutoreleasePoolPage對象。若目前對象為空,則生成一個新的page對象。若目前page存在且已滿,則建立一個新的page,通過連結清單的形式将兩個page連結起來。
- 向目前page對象中next位置追加一個哨兵對象(POOL_SENTINEL)并傳回該位址,這個位址作為pop操作的入參。
注意:每調用一次push()操作都會建立一個新的autoreleasepool對象,即往AutoreleasePoolPage中插入一個POOL_SENTINEL,并且傳回插入的 POOL_SENTINEL 的記憶體位址。
pop()過程:
- 根據傳入的哨兵對象位址找到其所在的page。
- 在目前page中,将晚于哨兵對象插入的所有autorelease對象都發送一次release消息,并将next指針移回正确位置。
- 從最新加入的對象一直向前清理,可以跨越若幹個page,直到哨兵對象所在的位置。
iOS記憶體管理之autorelease
autorelease對象在什麼時候釋放呢?
通過前面的分析,我們可以知道,autorelease對象是在AutoReleasePool的生命周期結束時釋放的。在沒有顯示生成AutoReleasePool對象的情況下,autorelease對象在目前runloop疊代結束時釋放的,Cocoa架構會為每個runloop添加AutoreleasePool的push和pop操作。
ARC下autorelease優化政策
先看一下,ARC下,調用NSMutableArray類的array方法時,編譯器為我們做了哪些操作
+ (id)array {
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
id obj = objc_retainAutoreleasedReturnValue([NSMutableArray array]);
objc_autoreleaseReturnValue函數會檢查使用該函數的方法或函數調用方的執行指令清單,如果方法或函數的調用方在調用了方法或函數後緊接着調用objc_retainAutoreleasedReturnValue()函數,那麼就不将傳回的對象注冊到autoreleasepool中,而是直接傳遞到方法或函數的調用方。
拾遺
使用容器的block版本的枚舉器時,内部會自動添加一個AutoreleasePool
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 這裡被一個局部@autoreleasepool包圍着
}];
在普通for循環和for in循環中沒有。for循環和for in循環中周遊産生大量autorelease變量時,就需要手加局部 AutoreleasePool啦。
掃一掃關注iOS學習社群公号,了解更多iOS知識以及提升工作技能