內存管理剖析(二)——定時器問題markdown
經歷過MRC時代的開發者,確定都用過autorelease
方法,用於把對象交給AutoreleasePool
管理,在合適的時候,自動釋放對象。其實所謂的自動釋放對象,就是對所管理的對象調用release
方法。要想知道autorelease
方法的原理,首先就須要弄清楚AutoreleasePool
是個什麼東東。iphone
下面來看一個段MRC環境下的代碼,爲何要在MRC下討論這個問題呢?由於ARC會爲咱們在合適的地方自動加上autorelease
代碼,而且不容許咱們手動調用該方法了,爲了方便研究autorelease
原理,咱們仍是得回到MRC。函數
****************** main.m *****************
#import <Foundation/Foundation.h>
#import "CLPerson.h"
int main(int argc, const char * argv[]) {
NSLog(@"pool--start");
@autoreleasepool {
CLPerson *p = [[[CLPerson alloc] init] autorelease];
}
NSLog(@"pool--end");
return 0;
}
************** CLPerson.m **************
#import "CLPerson.h"
@implementation CLPerson
- (void)dealloc
{
NSLog(@"%s", __func__);
[super dealloc];
}
@end
****************** 打印結果 *******************
2019-08-27 16:37:15.141523+0800 Interview16-autorelease[11602:772121] pool--start
2019-08-27 16:37:15.141763+0800 Interview16-autorelease[11602:772121] -[CLPerson dealloc]
2019-08-27 16:37:15.141775+0800 Interview16-autorelease[11602:772121] pool--end
複製代碼
歸納一下看到的表面現象:CLPerson
實例對象p
是在@autoreleasepool {}
大括號結束的時候被釋放的。 那麼@autoreleasepool {}
到底作了什麼呢?咱們在命令行窗口裏對main.m
文件執行以下命令oop
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
佈局
在生成的中間代碼main.cpp
中,找到main
函數的底層實現以下post
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __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"));
}
return 0;
}
複製代碼
其實若是你熟悉消息機制,上述的代碼能夠轉化成以下形式ui
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;
CLPerson *p = [[[CLPerson alloc] init] autorelease];
}
return 0;
}
複製代碼
咱們觀察可發現@autoreleasepool {}
通過編譯以後發生了以下轉變
這裏多了個__AtAutoreleasePool
,它實際上是個c++的結構體,能夠在main.cpp
裏搜索到它的定義以下
struct __AtAutoreleasePool {
//構造函數-->能夠類比成OC的init方法,在建立時調用
__AtAutoreleasePool()
{
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
//析構函數-->能夠類比成OC的dealloc方法,在銷燬時調用
~__AtAutoreleasePool()
{
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
複製代碼
若是你還不瞭解C++語法也無妨,它跟OC的類類似,能夠有函數(方法),上面的這個結構體__AtAutoreleasePool
裏面有已經有兩個函數,
__AtAutoreleasePool()
--> atautoreleasepoolobj = objc_autoreleasePoolPush();
,結構體被建立時調用,用於結構體的初始化~__AtAutoreleasePool()
--> objc_autoreleasePoolPop(atautoreleasepoolobj);
,結構體被銷燬時調用再回到咱們的main
函數,其實它本質上就是下面這個形式上面是單層@autoreleasepool {}
的狀況,那麼若是有多層@autoreleasepool {}
嵌套在一塊兒,就能夠按照一樣的規則來拆解
接下來咱們就來探究一下這兩個函數的實現邏輯。在objc4源碼的NSObject.mm
文件裏能夠找到它們的實現
*************** NSObject.mm (objc4) ******************
void * objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
複製代碼
能夠看到,它們分別調用了C++類 AutoreleasePoolPage
的push()
和pop()
函數。要想繼續深刻後續函數的實現邏輯,咱們須要先來看一看這個AutoreleasePoolPage
的內部結構,它的內容很多,有大量函數,可是咱們首先須要理清楚它的成員變量,這些是可變化的,可操控的,因此去掉函數和一些靜態常量,能夠將AutoreleasePoolPage
結構簡化以下
class AutoreleasePoolPage {
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
複製代碼
根據其命名,中文釋義成自動釋放池頁,有個頁的概念。咱們知道自動釋放池,是用來存放對象的,這個「頁」就說明釋放池的結構體應該有頁面篇幅限制(內存空間大小)。具體多大呢?來看一下AutoreleasePoolPage
的兩個函數
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
複製代碼
begin()
函數返回一個指針,指向自身最後一個成員變量以後的內存地址(至關於越過了自身所佔用的內存空間) end()
裏面有一個SIZE
,咱們看看它的定義
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
********************************************
#define PAGE_MAX_SIZE PAGE_SIZE
********************************************
#define PAGE_SIZE I386_PGBYTES
********************************************
#define I386_PGBYTES 4096 /* bytes per 80386 page */
複製代碼
能夠看到,SIZE
其實是4096
。這就是說end()
函數,獲得的是一個指針,指向AutoreleasePoolPage
對象地址以後的第4096
個字節的內存地址。
經過以上掌握的信息,咱們先拋出結論,而後再繼續經過源碼加深理解。
每一個
AutoreleasePoolPage
對象佔4096
個字節,其中成員變量共佔用8字節
*7
=56個字節
。剩餘的4040
個字節的空間就是用來存儲自動釋放對象的。由於一個
AutoreleasePoolPage
對象的內存是有限的,程序裏面可能有不少對象會被加入自動釋放池,所以可能會出現多個AutoreleasePoolPage
對象來共同存放自動釋放對象。全部的AutoreleasePoolPage
對象是以雙向鏈表的形式(數據結構)鏈接在一塊兒的。
AutoreleasePoolPage
對象的各成員變量含義以下
magic_t const magic;
id *next;
指向AutoreleasePoolPage
內下一個能夠用來存放自動釋放對象的內存地址pthread_t const thread;
自動釋放池所屬的線程,說明它不能跟多個線程關聯。AutoreleasePoolPage * const parent;
指向上一頁釋放池的指針AutoreleasePoolPage *child;
指向下一頁釋放池的指針uint32_t const depth;
uint32_t hiwat;
接下來,咱們就正式開始研究AutoreleasePoolPage::push();
。假設咱們如今是處在項目的main函數的第一個@autoreleasepool {}
開始的地方,也就是整個程序將會第一次去調用push()
函數:
# define POOL_BOUNDARY nil
static inline void *push() {
id *dest;
if (DebugPoolAllocation) {//Debug模式下,每一個autorelease pool都會建立新頁
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {//標準狀況下,調用autoreleaseFast()函數
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
複製代碼
其中POOL_BOUNDARY
就是nil
的宏定義,忽略Debug模式,咱們只看正常模式,那麼push()
將會調用autoreleaseFast(POOL_BOUNDARY)
獲得一個id *dest
並將其返回給上層函數。查看一下這個autoreleaseFast()
,看看它到底能給咱們返回什麼
static inline id *autoreleaseFast(id obj) {
//拿到當前可用的AutoreleasePoolPage對象page
AutoreleasePoolPage *page = hotPage();
//(1)若是page存在&&page未滿,則直接增長obj
if (page && !page->full()) {
return page->add(obj);
} else if (page) {//(2)若是滿了,則調用autoreleaseFullPage(obj, page);
return autoreleaseFullPage(obj, page);
} else {//(3)若是沒有頁面,則調用autoreleaseNoPage(obj);
return autoreleaseNoPage(obj);
}
}
複製代碼
由於是整個程序第一次push操做,所以page對象還不存在,因此會按照狀況(3)走,也就是autoreleaseNoPage(obj);
,實現以下
static __attribute__((noinline))
id *autoreleaseNoPage(id obj) {
/*--"No page" 1.能夠表示當前尚未任何pool被建立(pushed) 2.也能夠表示已經建立了一個empty placeholder pool(空釋放池佔位符),只是還沒添加任何內容 */
assert(!hotPage());
//標籤-->是否須要增長額外的POOL_BOUNDARY
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
/* 若是存在EmptyPoolPlaceholder(空佔位符pool),就修改標籤爲true, 後面就須要依據此標籤增長額外的POOL_BOUNDARY */
pushExtraBoundary = true;
}
/* 若是傳入的obj不等於POOL_BOUNDARY(nil)而且找不到當前pool(丟失了),返回nil */
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
pthread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
/* ♥️♥️♥️♥️若是傳入的是POOL_BOUNDARY,而且不在Debug模式, 會調用setEmptyPoolPlaceholder()設置一個EmptyPoolPlaceholder */
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
return setEmptyPoolPlaceholder();
}
// 初始化第一個AutoreleasePoolPage
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
//將其設置成當前頁(hot)
setHotPage(page);
// 根據pushExtraBoundary標籤決定是否多入棧一個POOL_BOUNDARY
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// 將傳入的obj入棧,經過 add()函數
return page->add(obj);
}
複製代碼
由於此時尚未建立過AutoreleasePoolPage
,而且也沒有設置過EmptyPoolPlaceholder
,所以程序會命中代碼中♥️♥️♥️♥️標記出的代碼,調用setEmptyPoolPlaceholder();
,該函數實現以下
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
********************************************
static inline id* setEmptyPoolPlaceholder() {
assert(tls_get_direct(key) == nil);
tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
return EMPTY_POOL_PLACEHOLDER;
}
複製代碼
能夠看到實際上就是將key
與(id*)1
綁定起來,這個key
是一個靜態常量,最後將這個(id*)1
做爲一個空釋放池池佔位符返回,這樣整個程序的第一個push()
函數結束,結果是生成了一個EMPTY_POOL_PLACEHOLDER (也就是(id*)1)
做爲釋放池佔位符。
接着上面的過程,咱們在push()
後,第一次對某個對象執行autorelease
方法時,看一下autorelease
的內部作了什麼,先找到其源碼以下
- (id)autorelease {
return ((id)self)->rootAutorelease();//🈯️從這裏往下走
}
************************************************
inline id objc_object::rootAutorelease() {
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();//🈯️從這裏往下走
}
************************************************
__attribute__((noinline,used))
id objc_object::rootAutorelease2() {
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);//🈯️從這裏往下走
}
************************************************
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;
}
複製代碼
經過逐層遞進,咱們看到autorelease
方法最終又來到了autoreleaseFast()
函數
static inline id *autoreleaseFast(id obj) {
//拿到當前可用的AutoreleasePoolPage對象page
AutoreleasePoolPage *page = hotPage();
//(1)若是page存在&&page未滿,則直接增長obj
if (page && !page->full()) {
return page->add(obj);
} else if (page) {//(2)若是滿了,則調用autoreleaseFullPage(obj, page);
return autoreleaseFullPage(obj, page);
} else {//(3)若是沒有頁面,則調用autoreleaseNoPage(obj);
return autoreleaseNoPage(obj);
}
}
複製代碼
那麼這一次,咱們看看第一句代碼裏面hotPage();
獲得的是什麼
static inline AutoreleasePoolPage *hotPage() {
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
//若是檢查到key有綁定EMPTY_POOL_PLACEHOLDER,返回nil
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;//將當前頁對象返回
}
複製代碼
由於咱們一開始將key
與EMPTY_POOL_PLACEHOLDER
綁定過,所以這裏返回空,代表當前頁空,還未被建立,所以咱們返回到autoreleaseFast
方法裏面,將會調用autoreleaseNoPage(obj)
函數,根據咱們上面對這個函數步驟的註釋,這一次程序應該會走到函數的最後一部分 主要作了下面幾件事:
AutoreleasePoolPage
EMPTY_POOL_PLACEHOLDER
會使pushExtraBoundary
置爲true
,所以這裏須要爲第一個AutoreleasePoolPage
先入棧一個POOL_BOUNDARY
add(obj)
將傳入的自動釋放對象obj
入棧上面add()
函數的具體功能,其實就是將obj
的值賦值給當前AutoreleasePoolPage
的next
指針指向的內存空間,而後next
再進行++
操做,移向下一段可用內存空間,方便下一次存放自動釋放對象的時候使用。以下
id *add(id obj) {
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;//先賦值,再++
protect();
return ret;
}
複製代碼
另外須要注意一下這裏的setHotPage(page)
函數,實現以下
static inline void setHotPage(AutoreleasePoolPage *page) {
if (page) page->fastcheck();
tls_set_direct(key, (void *)page);
}
複製代碼
它的做用就是把當前新建立的AutoreleasePoolPage
與key
綁定起來,往後hotPage()
函數就能夠經過key
直接拿到當前頁。
若是咱們繼續對新的對象執行autorelease
操做,一樣會來到函數,但因爲AutoreleasePoolPage
對象已經存在了,若是當前page
未滿,會走以下函數 也就是直接經過add(obj)
函數將obj
對象入棧
咱們以前說過,一個AutoreleasePoolPage
對象能存放的自動釋放對象數量是有限的,一個自動釋放對象就是一個指針,佔8字節,而AutoreleasePoolPage
對象可用的空間是4040個字節,也就是能夠存放505個對象(指針),因此一頁AutoreleasePoolPage
是有可能滿頁的,這個時候,autoreleaseFast
就會調用autoreleaseFullPage(obj, page);
函數,它的實現以下
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
assert(page == hotPage());
assert(page->full() || DebugPoolAllocation);
do {//經過child指針拿到下一個沒有滿的page對象
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);//先將上面獲取的page設置爲當前頁(hot)
return page->add(obj);//經過add函數將obj存入該page
}
複製代碼
其實上面就是經過AutoreleasePoolPage
對象的child
指針去尋找下一個未滿的page
。AutoreleasePoolPage
對象之間是經過child
和parent
指針造成的雙向鏈表結構,就是爲了在這個時候使用的。一樣,在清空釋放池對象的時候,若是當前釋放池徹底空了,則會經過parent
指針去尋找上層的釋放池。
除了系統在main
函數里加上的最初的一層@autoreleasepool {}
以外,有時候咱們本身的代碼裏面可能會也會使用@autoreleasepool {}
,方便對一些對象進行更爲靈活的內存管理。那麼咱們手動加的@autoreleasepool {}
確定是嵌套在main函數@autoreleasepool {}
內部的,至關於
int main(int argc, const char * argv[]) {
@autoreleasepool {//這是系統加的第一層
@autoreleasepool {}//這是咱們可能會添加的內層嵌套
}
}
複製代碼
如今咱們再次來看一下這一次AutoreleasePoolPage::push();
會如何執行。一樣程序會執行到autoreleaseFast(POOL_BOUNDARY);
POOL_BOUNDARY
會被傳入autoreleaseFast
函數,而且也會經過add()
或者autoreleaseFullPage()
被添加到AutoreleasePoolPage
對象的頁空間上。其實就是和普通的[obj autorelease]
的流程同樣,只不過此次是obj
= POOL_BOUNDARY
,顯然這是爲了一個新的@autoreleasepool{}
作準備。
POOL_BOUNDARY
究竟是拿來幹嗎的呢?一會你就知道了。
分析完了源碼,如今經過圖例來展現一下
@autoreleasepool
的實現原理。 【假設】爲方便展現每頁AutoreleasePoolPage
只能存放3個釋放對象,以下
這個問題就要搞清楚@autoreleasepool{}
的另外一半AutoreleasePoolPage::pop(atautoreleasepoolobj);
作了什麼。一塊兒來看一看其中的核心函數即是releaseUntile(stop)
,這裏的stop
實際上傳入的就是POOL_BOUNDARY
,進入該函數
void releaseUntil(id *stop) {
while (this->next != stop) {//🥝若是next指向POOL_BOUNDARY,跳出循環🥝
//🥝拿到當前頁
AutoreleasePoolPage *page = hotPage();
//🥝🥝當前頁若是爲空,經過parent拿到上一個AutoreleasePoolPage對象做爲當前頁
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
//🥝🥝🥝經過 --next 拿到當前頁棧頂的對象
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
//🥝🥝🥝🥝若是obj不是POOL_BOUNDARY,就進行[obj release]
objc_release(obj);
}
}
setHotPage(this);
}
複製代碼
pop()
核心步驟已經在上面函數裏的註釋體現出來。也就是說,當最內層的@autoreleasepool{}
做用域結束調用其對應的pop()
函數時,會從AutoreleasePoolPage
鏈表的當前頁裏面找到棧頂的對象,逐個開始釋放,直到遇到POOL_BOUNDARY
就停下來,這樣,就表明這一層的@autorelease{}
內所包含的全部對象都完成了release
方法調用。
當程序走到上一層的@autoreleasepool{}
做用域結束的地方,又回執行上面的流程,對其包含的對象一次調用release
方法。能夠經過下圖的示例來體會一下。
經過上面的研究,咱們知道@autoreleasepool{}
的做用,實際上就是在做用域的頭和尾分別調用了objc_autoreleasePoolPush();
和objc_autoreleasePoolPop()
函數,可是在iOS項目當中,@autoreleasepool{}
的做用域是何時開始,何時結束呢?這就須要瞭解咱們以前研究過的另外一個知識點RunLoop。咱們知道,除非咱們手動啓動子線程的RunLoop,不然程序裏面只有主線程有RunLoop,這是系統默認開啓的。下面咱們來看一下主線程的RunLoop肚子裏都有什麼寶貝。
咱們能夠隨便新建一個iOS項目,在ViewController
的viewDidLoad
方法裏能夠直接打印當前RunLoop對象(即主線程的RunLoop對象)
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@",[NSRunLoop currentRunLoop]);
}
@end
複製代碼
打印結果是洋洋灑灑的一大堆,若是你還不熟悉RunLoop的結構,能夠參考個人Runloop的內部結構與運行原理,裏面應該說的比較清楚了。咱們能夠在打印結果的common mode items
部分,找到兩個跟autorelease
相關的observer
,以下圖所示 具體以下
<CFRunLoopObserver 0x600003f3c640 [0x10a2fdae8]>
{
valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647,
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e17ac9d),
context =
<CFArray 0x6000000353b0 [0x10a2fdae8]>
{
type = mutable-small, count = 1, values = (0 : <0x7f91ff802058>)
}
}
<CFRunLoopObserver 0x600003f3c500 [0x10a2fdae8]>
{
valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647,
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e17ac9d),
context =
<CFArray 0x6000000353b0 [0x10a2fdae8]>
{
type = mutable-small, count = 1, values = (0 : <0x7f91ff802058> )
}
}
複製代碼
咱們能夠看到,這兩個監聽器分監聽的狀態分別是
activities = 0xa0
(對應十進制的160
)activities = 0x1
(對應十進制的1
)這兩個狀態怎麼解讀呢?咱們能夠在CF框架的RunLoop源碼裏面找到對應的定義
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),************十進制1---(進入loop)
kCFRunLoopBeforeTimers = (1UL << 1),****十進制2
kCFRunLoopBeforeSources = (1UL << 2),**十進制4
kCFRunLoopBeforeWaiting = (1UL << 5),***十進制32----(loop即將休眠)
kCFRunLoopAfterWaiting = (1UL << 6),*****十進制64
kCFRunLoopExit = (1UL << 7),**************十進制128----(退出loop)
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼
根據RunLoop狀態的枚舉值能夠看出,160 = 128 + 32
,也就是說
activities = 0xa0
=(kCFRunLoopExit | kCFRunLoopBeforeWaiting)activities = 0x1
=(kCFRunLoopEntry)所以這三個狀態被監聽到的時候,就會調用_wrapRunLoopWithAutoreleasePoolHandler
函數。這個函數其實是按照下圖的示意運做
objc_autoreleasePoolPush();
objc_autoreleasePoolPop()
,而後調用objc_autoreleasePoolPush();
objc_autoreleasePoolPop()
根據上面的分析,咱們能夠總結,除了程序啓動(對應kCFRunLoopEntry)和程序退出(對應kCFRunLoopExit)會調用一次objc_autoreleasePoolPush();
和objc_autoreleasePoolPop()
外,程序的運行過程當中,每當RunLoop即將休眠,被observer
監聽到kCFRunLoopBeforeWaiting狀態時,會先調用一次objc_autoreleasePoolPop()
,這樣就將當前的autoreleasepool
裏面的對象逐個調用release
方法,至關於清空釋放池子;緊接着再調用一次objc_autoreleasePoolPush();
,至關於開啓一個新的釋放池,等待RunLoop醒來後的下一次循環使用。
自動釋放池的對象何時會被調用release方法呢?
RunLoop的每一圈循環過程當中,調用過autorelease
方法的對象(也就是被加入AutoreleasePoolPage
的對象),會在當次循環即將進入休眠狀態的時候,被調用release
方法,也能夠說是被釋放了。
好了,AutoreleasePool的原理以及它和RunLoop的關係就分析到這裏。