iOS內(nèi)存管理-深入解析自動(dòng)釋放池

主要內(nèi)容:

  1. AutoreleasePool簡介
  2. AutoreleasePool底層原理
  3. AutoreleaseNSThread、NSRunLoop的關(guān)系
  4. AutoreleasePool在主線程上的釋放時(shí)機(jī)
  5. AutoreleasePool在子線程上的釋放時(shí)機(jī)
  6. AutoreleasePool需要手動(dòng)添加的情況

一、Autorelease簡介

iOS開發(fā)中的Autorelease機(jī)制是為了延時(shí)釋放對象。自動(dòng)釋放的概念看上去很像ARC,但實(shí)際上這更類似于C語言中自動(dòng)變量的特性。

  • 自動(dòng)變量:在超出變量作用域后將被廢棄;
  • 自動(dòng)釋放池:在超出釋放池生命周期后,向其管理的對象實(shí)例的發(fā)送release消息;
1.1 MRC下使用自動(dòng)釋放池

在MRC環(huán)境中使用自動(dòng)釋放池需要用到NSAutoreleasePool對象,其生命周期就相當(dāng)于C語言變量的作用域。對于所有調(diào)用過autorelease方法的對象,在廢棄NSAutoreleasePool對象時(shí),都將調(diào)用release實(shí)例方法。用源代碼表示如下:

//MRC環(huán)境下的測試:
//第一步:生成并持有釋放池NSAutoreleasePool對象;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

//第二步:調(diào)用對象的autorelease實(shí)例方法;
id obj = [[NSObject alloc] init];
[obj autorelease];

//第三步:廢棄NSAutoreleasePool對象;
[pool drain];   //向pool管理的所有對象發(fā)送消息,相當(dāng)于[obj release]

//obi已經(jīng)釋放,再次調(diào)用會(huì)崩潰(Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT))
NSLog(@"打印obj:%@", obj); 

理解NSAutoreleasePool對象的生命周期,如下圖所示:

NSAutoreleasePool對象的生命周期.png
1.2 ARC下使用自動(dòng)釋放池

ARC環(huán)境不能使用NSAutoreleasePool類也不能調(diào)用autorelease方法,代替它們實(shí)現(xiàn)對象自動(dòng)釋放的是@autoreleasepool塊和__autoreleasing修飾符。比較兩種環(huán)境下的代碼差異如下圖:

對比MRC與ARC的自動(dòng)釋放池使用.png

如圖所示,@autoreleasepool塊替換了NSAutoreleasePoool類對象的生成、持有及廢棄這一過程。而附有__autoreleasing修飾符的變量替代了autorelease方法,將對象注冊到了Autoreleasepool;由于ARC的優(yōu)化,__autorelease是可以被省略的,所以簡化后的ARC代碼如下:

//ARC環(huán)境下的測試:
@autoreleasepool {
    id obj = [[NSObject alloc] init];
    NSLog(@"打印obj:%@", obj); 
}

顯式使用__autoreleasing修飾符的情況非常少見,這是因?yàn)锳RC的很多情況下,即使是不顯式的使用__autoreleasing,也能實(shí)現(xiàn)對象被注冊到釋放池中。主要包括以下幾種情況:

  1. 編譯器會(huì)進(jìn)行優(yōu)化,檢查方法名是否以alloc/new/copy/mutableCopy開始,如果不是則自動(dòng)將返回對象注冊到Autoreleasepool;
  2. 訪問附有__weak修飾符的變量時(shí),實(shí)際上必定要訪問注冊到Autoreleasepool的對象,即會(huì)自動(dòng)加入Autoreleasepool;
  3. id的指針或?qū)ο蟮闹羔?id*,NSError **),在沒有顯式地指定修飾符時(shí)候,會(huì)被默認(rèn)附加上__autoreleasing修飾符,加入Autoreleasepool

注意:如果編譯器版本為LLVM.3.0以上,即使ARC無效@autoreleasepool塊也能夠使用;如下源碼所示:

//MRC環(huán)境下的測試:
@autoreleasepool{
    id obj = [[NSObject alloc] init];
    [obj autorelease];
}

二、AutoRelease原理

2.1 使用@autoreleasepool{}

我們在main函數(shù)中寫入自動(dòng)釋放池相關(guān)的測試代碼如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
    return 0;
}

為了探究釋放池的底層實(shí)現(xiàn),我們在終端使用clang -rewrite-objc + 文件名命令將上述OC代碼轉(zhuǎn)化為C++源碼:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */
    {
        __AtAutoreleasePool __autoreleasepool;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_d37e0d_mi_0);
     }//大括號對應(yīng)釋放池的作用域
     
     return 0;
}

在經(jīng)過編譯器clang命令轉(zhuǎn)化后,我們看到的所謂的@autoreleasePool塊,其實(shí)對應(yīng)著__AtAutoreleasePool的結(jié)構(gòu)體。

2.2 分析結(jié)構(gòu)體__AtAutoreleasePool的具體實(shí)現(xiàn)

在源碼中找到__AtAutoreleasePool結(jié)構(gòu)體的實(shí)現(xiàn)代碼,具體如下:

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;
};

__AtAutoreleasePool結(jié)構(gòu)體包含了:構(gòu)造函數(shù)、析構(gòu)函數(shù)和一個(gè)邊界對象;

  • 構(gòu)造函數(shù)內(nèi)部調(diào)用:objc_autoreleasePoolPush()方法,返回邊界對象atautoreleasepoolobj
  • 析構(gòu)函數(shù)內(nèi)部調(diào)用:objc_autoreleasePoolPop()方法,傳入邊界對象atautoreleasepoolobj

分析main函數(shù)中__autoreleasepool結(jié)構(gòu)體實(shí)例的生命周期是這樣的:
__autoreleasepool是一個(gè)自動(dòng)變量,其構(gòu)造函數(shù)是在程序執(zhí)行到聲明這個(gè)對象的位置時(shí)調(diào)用的,而其析構(gòu)函數(shù)則是在程序執(zhí)行到離開這個(gè)對象的作用域時(shí)調(diào)用。所以,我們可以將上面main函數(shù)的代碼簡化如下:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}
2.3 objc_autoreleasePoolPush與objc_autoreleasePoolPop

進(jìn)一步觀察自動(dòng)釋放池構(gòu)造函數(shù)與析構(gòu)函數(shù)的實(shí)現(xiàn),其實(shí)它們都只是對AutoreleasePoolPage對應(yīng)靜態(tài)方法pushpop的封裝

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}
2.4 理解AutoreleasePoolPage

AutoreleasePoolPage是一個(gè)C++中的類,打開Runtime的源碼工程,在NSObject.mm文件中可以找到它的定義,摘取其中的關(guān)鍵代碼如下:

//大致在641行代碼開始
class AutoreleasePoolPage {
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)  //空池占位
#   define POOL_BOUNDARY 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;                  //校驗(yàn)AutoreleasePagePoolPage結(jié)構(gòu)是否完整
    id *next;                             //指向新加入的autorelease對象的下一個(gè)位置,初始化時(shí)指向begin()
    pthread_t const thread;               //當(dāng)前所在線程,AutoreleasePool是和線程一一對應(yīng)的
    AutoreleasePoolPage * const parent;   //指向父節(jié)點(diǎn)page,第一個(gè)結(jié)點(diǎn)的parent值為nil
    AutoreleasePoolPage *child;           //指向子節(jié)點(diǎn)page,最后一個(gè)結(jié)點(diǎn)的child值為nil
    uint32_t const depth;                 //鏈表深度,節(jié)點(diǎn)個(gè)數(shù)
    uint32_t hiwat;                       //數(shù)據(jù)容納的一個(gè)上限
    //......
};

其實(shí),每個(gè)自動(dòng)釋放池都是是由若干個(gè)AutoreleasePoolPage組成的雙向鏈表結(jié)構(gòu),如下圖所示:

AutoreleasePool.png

AutoreleasePoolPage中擁有parentchild指針,分別指向上一個(gè)和下一個(gè)page;當(dāng)前一個(gè)page的空間被占滿(每個(gè)AutorelePoolPage的大小為4096字節(jié))時(shí),就會(huì)新建一個(gè)AutorelePoolPage對象并連接到鏈表中,后來的 Autorelease對象也會(huì)添加到新的page中;

另外,當(dāng)next== begin()時(shí),表示AutoreleasePoolPage為空;當(dāng)next == end(),表示AutoreleasePoolPage已滿。

2.5 理解哨兵對象/邊界對象(POOL_BOUNDARY)的作用

AutoreleasePoolPage的源碼中,我們很容易找到邊界對象(哨兵對象)的定義:

#define POOL_BOUNDARY nil

邊界對象其實(shí)就是nil的別名,而它的作用事實(shí)上也就是為了起到一個(gè)標(biāo)識的作用。

每當(dāng)自動(dòng)釋放池初始化調(diào)用objc_autoreleasePoolPush方法時(shí),總會(huì)通過AutoreleasePoolPagepush方法,將POOL_BOUNDARY放到當(dāng)前page的棧頂,并且返回這個(gè)邊界對象;

而在自動(dòng)釋放池釋放調(diào)用objc_autoreleasePoolPop方法時(shí),又會(huì)將邊界對象以參數(shù)傳入,這樣自動(dòng)釋放池就會(huì)向釋放池中對象發(fā)送release消息,直至找到第一個(gè)邊界對象為止。

2.6 理解objc_autoreleasePoolPush方法

經(jīng)過前面的分析,objc_autoreleasePoolPush最終調(diào)用的是 AutoreleasePoolPagepush方法,該方法的具體實(shí)現(xiàn)如下:

static inline void *push() {
   return autoreleaseFast(POOL_BOUNDARY);
}

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 {
1.        return autoreleaseNoPage(obj);
   }
}

//壓棧操作:將對象加入AutoreleaseNoPage并移動(dòng)棧頂?shù)闹羔?id *add(id obj) {
    id *ret = next;
    *next = obj;
    next++;
    return ret;
}

//當(dāng)前hotPage已滿時(shí)調(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);
}

//當(dāng)前hotpage不存在時(shí)調(diào)用
static id *autoreleaseNoPage(id obj) {
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }

    return page->add(obj);
}

觀察上述代碼,每次調(diào)用push其實(shí)就是創(chuàng)建一個(gè)新的AutoreleasePool,在對應(yīng)的AutoreleasePoolPage中插入一個(gè)POOL_BOUNDARY ,并且返回插入的POOL_BOUNDARY 的內(nèi)存地址。push方法內(nèi)部調(diào)用的是autoreleaseFast方法,并傳入邊界對象(POOL_BOUNDARY)。hotPage可以理解為當(dāng)前正在使用的AutoreleasePoolPage。

自動(dòng)釋放池最終都會(huì)通過page->add(obj)方法將邊界對象添加到釋放池中,而這一過程在autoreleaseFast方法中被分為三種情況:

  1. 當(dāng)前page存在且不滿,調(diào)用page->add(obj)方法將對象添加至page的棧中,即next指向的位置
  2. 當(dāng)前page存在但是已滿,調(diào)用autoreleaseFullPage初始化一個(gè)新的page,調(diào)用page->add(obj)方法將對象添加至page的棧中
  3. 當(dāng)前page不存在時(shí),調(diào)用autoreleaseNoPage創(chuàng)建一個(gè)hotPage,再調(diào)用page->add(obj) 方法將對象添加至page的棧中
2.7 objc_autoreleasePoolPop方法

AutoreleasePool的釋放調(diào)用的是objc_autoreleasePoolPop方法,此時(shí)需要傳入邊界對象作為參數(shù)。這個(gè)邊界對象正是每次執(zhí)行objc_autoreleasePoolPush方法返回的對象atautoreleasepoolobj;

同理,我們找到objc_autoreleasePoolPop最終調(diào)用的方法,即AutoreleasePoolPagepop方法,該方法的具體實(shí)現(xiàn)如下:

static inline void pop(void *token)   //POOL_BOUNDARY的地址
{
    AutoreleasePoolPage *page;
    id *stop;

    page = pageForPointer(token);   //通過POOL_BOUNDARY找到對應(yīng)的page
    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();   // 記錄最高水位標(biāo)記

    page->releaseUntil(stop);   //向棧中的對象發(fā)送release消息,直到遇到第一個(gè)哨兵對象

    // memory: delete empty children
    // 刪除空掉的節(jié)點(diǎn)
    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();
        }
    }
}

上述代碼中,首先根據(jù)傳入的邊界對象地址找到邊界對象所處的page;然后選擇當(dāng)前page中最新加入的對象一直向前清理,可以向前跨越若干個(gè)page,直到邊界所在的位置;清理的方式是向這些對象發(fā)送一次release消息,使其引用計(jì)數(shù)減一;

另外,清空page對象還會(huì)遵循一些原則:

  1. 如果當(dāng)前的page中存放的對象少于一半,則子page全部刪除;
  2. 如果當(dāng)前當(dāng)前的page存放的多余一半(意味著馬上將要滿),則保留一個(gè)子page,節(jié)省創(chuàng)建新page的開銷;
2.8 autorelease方法

上述是對自動(dòng)釋放池整個(gè)生命周期的分析,現(xiàn)在我們來理解延時(shí)釋放對象autorelease方法的實(shí)現(xiàn),首先查看該方法的調(diào)用棧:

- [NSObject autorelease]
└── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
        └── static id AutoreleasePoolPage::autorelease(id obj)
            └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
                ├── id *add(id obj)
                ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
                │   ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                │   └── id *add(id obj)
                └── static id *autoreleaseNoPage(id obj)
                    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                    └── id *add(id obj)

如上所示,autorelease方法最終也會(huì)調(diào)用上面提到的 autoreleaseFast方法,將當(dāng)前對象加到AutoreleasePoolPage中。關(guān)于autoreleaseFast的分析這里不再累述,我們主要來考慮一下兩次調(diào)用的區(qū)別:

autorelease函數(shù)和push函數(shù)一樣,關(guān)鍵代碼都是調(diào)用autoreleaseFast函數(shù)向自動(dòng)釋放池的鏈表?xiàng)V刑砑右粋€(gè)對象,不過push函數(shù)入棧的是一個(gè)邊界對象,而autorelease函數(shù)入棧的是一個(gè)具體的Autorelease的對象。

三、AutoreleasePool與NSThread、NSRunLoop的關(guān)系

由于AppKitUIKit框架的優(yōu)化,我們很少需要顯式的創(chuàng)建一個(gè)自動(dòng)釋放池塊。這其中就涉及到AutoreleasePoolNSThread、NSRunLoop的關(guān)系。

3.1 RunLoop和NSThread的關(guān)系

RunLoop是用于控制線程生命周期并接收事件進(jìn)行處理的機(jī)制,其實(shí)質(zhì)是一個(gè)do-While循環(huán)。在蘋果文檔找到關(guān)于NSRunLoop的介紹如下:

Your application neither creates or explicitly manages NSRunLoop objects. Each NSThread object—including the application’s main thread—has an NSRunLoop object automatically created for it as needed. If you need to access the current thread’s run loop, you do so with the class method currentRunLoop.

總結(jié)RunLoopNSThread(線程)之間的關(guān)系如下:

  1. RunLoop與線程是一一對應(yīng)關(guān)系,每個(gè)線程(包括主線程)都有一個(gè)對應(yīng)的RunLoop對象;其對應(yīng)關(guān)系保存在一個(gè)全局的Dictionary里;
  2. 主線程的RunLoop默認(rèn)由系統(tǒng)自動(dòng)創(chuàng)建并啟動(dòng);而其他線程在創(chuàng)建時(shí)并沒有RunLoop,若該線程一直不主動(dòng)獲取,就一直不會(huì)有RunLoop;
  3. 蘋果不提供直接創(chuàng)建RunLoop的方法;所謂其他線程Runloop的創(chuàng)建其實(shí)是發(fā)生在第一次獲取的時(shí)候,系統(tǒng)判斷當(dāng)前線程沒有RunLoop就會(huì)自動(dòng)創(chuàng)建;
  4. 當(dāng)前線程結(jié)束時(shí),其對應(yīng)的Runloop也被銷毀;
3.2 RunLoop和AutoreleasePool的關(guān)系

蘋果文檔中找到兩者關(guān)系的介紹如下:

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

如上所述,主線程的NSRunLoop在監(jiān)測到事件響應(yīng)開啟每一次event loop之前,會(huì)自動(dòng)創(chuàng)建一個(gè)autorelease pool,并且會(huì)在event loop結(jié)束的時(shí)候執(zhí)行drain操作,釋放其中的對象。

3.3 Thread和AutoreleasePool的關(guān)系

蘋果文檔中找到兩者關(guān)系的介紹如下:

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.

如上所述, 包括主線程在內(nèi)的所有線程都維護(hù)有它自己的自動(dòng)釋放池的堆棧結(jié)構(gòu)。新的自動(dòng)釋放池被創(chuàng)建的時(shí)候,它們會(huì)被添加到棧的頂部,而當(dāng)池子銷毀的時(shí)候,會(huì)從棧移除。對于當(dāng)前線程來說,Autoreleased對象會(huì)被放到棧頂?shù)淖詣?dòng)釋放池中。當(dāng)一個(gè)線程線程停止,它會(huì)自動(dòng)釋放掉與其關(guān)聯(lián)的所有自動(dòng)釋放池。

四、AutoreleasePool在主線程上的釋放時(shí)機(jī)

4.1 理解主線程上的自動(dòng)釋放過程

分析主線程RunLoop管理自動(dòng)釋放池并釋放對象的詳細(xì)過程,我們在如下Demo中的主線程中設(shè)置斷點(diǎn),并執(zhí)行l(wèi)ldb命令:po [NSRunLoop currentRunLoop],具體效果如下:

autoreleasepool在主線程的釋放時(shí)機(jī).png

我們看到主線程RunLoop中有兩個(gè)與自動(dòng)釋放池相關(guān)的Observer,它們的 activities分別為0x10xa0這兩個(gè)十六進(jìn)制的數(shù),轉(zhuǎn)為二進(jìn)制分別為110100000,對應(yīng)CFRunLoopActivity的類型如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),          //0x1,啟動(dòng)Runloop循環(huán)
    kCFRunLoopBeforeTimers = (1UL << 1),            
    kCFRunLoopBeforeSources = (1UL << 2),        
    kCFRunLoopBeforeWaiting = (1UL << 5),  //0xa0,即將進(jìn)入休眠     
    kCFRunLoopAfterWaiting = (1UL << 6),   
    kCFRunLoopExit = (1UL << 7),           //0xa0,退出RunLoop循環(huán)  
    kCFRunLoopAllActivities = 0x0FFFFFFFU
    };

結(jié)合RunLoop監(jiān)聽的事件類型,分析主線程上自動(dòng)釋放池的使用過程如下:
1.App啟動(dòng)后,蘋果在主線程RunLoop里注冊了兩個(gè)Observer,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler();

  1. 第一個(gè)Observer監(jiān)視的事件是Entry(即將進(jìn)入Loop),其回調(diào)內(nèi)會(huì)調(diào)用 _objc_autoreleasePoolPush()創(chuàng)建自動(dòng)釋放池。order = -2147483647(即 32`位整數(shù)最小值)表示其優(yōu)先級最高,可以保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前;
  2. 第二個(gè)Observer監(jiān)視了兩個(gè)事件BeforeWaiting(準(zhǔn)備進(jìn)入休眠)時(shí)調(diào)用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時(shí)調(diào)用 _objc_autoreleasePoolPop()來釋放自動(dòng)釋放池。order = 2147483647(即32位整數(shù)的最大值)表示其優(yōu)先級最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后;
  3. 在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)、Timer回調(diào)內(nèi)的。這些回調(diào)會(huì)被 RunLoop創(chuàng)建好的AutoreleasePool環(huán)繞著,所以不會(huì)出現(xiàn)內(nèi)存泄漏,開發(fā)者也不必顯示創(chuàng)建AutoreleasePool了;

最后,也可以結(jié)合圖示理解主線程上自動(dòng)釋放對象的具體流程:

自動(dòng)釋放池系統(tǒng)釋放原理圖.png
  1. 程序啟動(dòng)到加載完成后,主線程對應(yīng)的RunLoop會(huì)停下來等待用戶交互
  2. 用戶的每一次交互都會(huì)啟動(dòng)一次運(yùn)行循環(huán),來處理用戶所有的點(diǎn)擊事件、觸摸事件。
  3. RunLoop檢測到事件后,就會(huì)創(chuàng)建自動(dòng)釋放池;
  4. 所有的延遲釋放對象都會(huì)被添加到這個(gè)池子中;
  5. 在一次完整的運(yùn)行循環(huán)結(jié)束之前,會(huì)向池中所有對象發(fā)送release消息,然后自動(dòng)釋放池被銷毀;
4.2 測試主線程上的對象自動(dòng)釋放過程

下面的代碼創(chuàng)建了一個(gè)Autorelease對象string,并且通過weakString進(jìn)行弱引用(不增加引用計(jì)數(shù),所以不會(huì)影響對象的生命周期),具體如下:

@interface TestMemoryVC ()
@property (nonatomic,weak)NSString *weakString;
@end

@implementation TestMemoryVC
- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *string = [NSString stringWithFormat:@"%@",@"WUYUBEICHEN"];
    self.weakString = string;
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear:%@", self.weakString);
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"viewDidAppear:%@", self.weakString);
}

@end

//打印結(jié)果:
//viewWillAppear:WUYUBEICHEN
//viewDidAppear:(null)

代碼分析:自動(dòng)變量的string在離開viewDidLoad的作用域后,會(huì)依靠當(dāng)前主線程上的RunLoop迭代自動(dòng)釋放。最終string對象在viewDidAppear方法執(zhí)行前被釋放(RunLoop完成此次迭代)。

五、AutoreleasePool子線程上的釋放時(shí)機(jī)

子線程默認(rèn)不開啟RunLoop,那么其中的延時(shí)對象該如何釋放呢?其實(shí)這依然要從ThreadAutoreleasePool的關(guān)系來考慮:

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.

也就是說,每一個(gè)線程都會(huì)維護(hù)自己的 Autoreleasepool棧,所以子線程雖然默認(rèn)沒有開啟RunLoop,但是依然存在AutoreleasePool,在子線程退出的時(shí)候會(huì)去釋放autorelease對象。

前面講到過,ARC會(huì)根據(jù)一些情況進(jìn)行優(yōu)化,添加__autoreleasing修飾符,其實(shí)這就相當(dāng)于對需要延時(shí)釋放的對象調(diào)用了autorelease方法。從源碼分析的角度來看,如果子線程中沒有創(chuàng)建AutoreleasePool ,而一旦產(chǎn)生了Autorelease對象,就會(huì)調(diào)用autoreleaseNoPage方法自動(dòng)創(chuàng)建hotpage,并將對象加入到其棧中。所以,一般情況下,子線程中即使我們不手動(dòng)添加自動(dòng)釋放池,也不會(huì)產(chǎn)生內(nèi)存泄漏。

六、AutoreleasePool需要手動(dòng)添加的情況

盡管ARC已經(jīng)做了諸多優(yōu)化,但是有些情況我們必須手動(dòng)創(chuàng)建AutoreleasePool,而其中的延時(shí)對象將在當(dāng)前釋放池的作用域結(jié)束時(shí)釋放。蘋果文檔中說明了三種情況,我們可能會(huì)需要手動(dòng)添加自動(dòng)釋放池:

  1. 編寫的不是基于UI框架的程序,例如命令行工具;
  2. 通過循環(huán)方式創(chuàng)建大量臨時(shí)對象;
  3. 使用非Cocoa程序創(chuàng)建的子線程;

而在ARC環(huán)境下的實(shí)際開發(fā)中,我們最常遇到的也是第二種情況,以下面的代碼為例:

- (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 1000000; i++) {
        NSObject *obj = [[NSObject alloc] init];
        NSLog(@"打印obj:%@", obj);
    }
 }

上述代碼中,obj因?yàn)殡x開作用域所以會(huì)被加入最近一次創(chuàng)建的自動(dòng)釋放池中,而這個(gè)釋放池就是主線程上的RunLoop管理的;因?yàn)?code>for循環(huán)在當(dāng)前線程沒有執(zhí)行完畢,Runloop也就沒有完成當(dāng)前這一次的迭代,所以導(dǎo)致大量對象被延時(shí)釋放。釋放池中的對象將會(huì)在viewDidAppear方法執(zhí)行前就被銷毀。在此情況下,我們就有必要通過手動(dòng)干預(yù)的方式及時(shí)釋放不需要的對象,減少內(nèi)存消耗;優(yōu)化的代碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 1000000; i++) {
        @autoreleasepool{
             NSObject *obj = [[NSObject alloc] init];
             NSLog(@"打印obj:%@", obj);
        }
    }
 }

參考鏈接

  1. 蘋果文檔NSAutoreleasePool
  2. 蘋果文檔NSRunLoop
  3. 蘋果文檔Using Autorelease Pool Blocks
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容