[iOS] AutoreleasePool實(shí)現(xiàn)原理

AutoreleasePool自動釋放池,是 OC 中的一種內(nèi)存自動回收機(jī)制,可以將加入自動釋放池中的對象的release 時(shí)機(jī)延遲。當(dāng)自動釋放池作用域結(jié)束時(shí),將池中的對象統(tǒng)一發(fā)送一次 release 消息,當(dāng)對象的引用計(jì)數(shù)為零時(shí),對象就會被釋放。

我們這次來探究一下AutoreleasePool自動釋放池的實(shí)現(xiàn)原理!

1. AutoreleasePool底層結(jié)構(gòu)

在 main.m 編寫如下函數(shù):

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}

通過clang -rewrite-objc main.m 指令,將 main.m 轉(zhuǎn)化為main.cpp文件,自動釋放池部分代碼如下:

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

struct __AtAutoreleasePool {
  // 構(gòu)造函數(shù)
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  // 析構(gòu)函數(shù)
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  // 成員變量
  void * atautoreleasepoolobj;
};

#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

    }
    return 0;
}

上面的代碼很清晰,自動釋放池構(gòu)造時(shí),會調(diào)用objc_autoreleasePoolPush()函數(shù),銷毀時(shí)會調(diào)用objc_autoreleasePoolPop()函數(shù)。

對應(yīng) objc 中的源碼實(shí)現(xiàn)為:

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

NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}


void *
_objc_autoreleasePoolPush(void)
{
    return objc_autoreleasePoolPush();
}

void
_objc_autoreleasePoolPop(void *ctxt)
{
    objc_autoreleasePoolPop(ctxt);
}

可以看到,自動釋放池構(gòu)造時(shí)調(diào)用AutoreleasePoolPage::push()函數(shù),析構(gòu)時(shí)調(diào)用 AutoreleasePoolPage::pop(ctxt);函數(shù)。下面也會從這兩個(gè)函數(shù)為切入點(diǎn)進(jìn)行分析。

AutoreleasePoolPage類實(shí)現(xiàn)的上方有一段注釋,翻譯如下:

  • 一個(gè)線程的自動釋放池就是一個(gè)存放對象指針的棧(AutoreleasePool的結(jié)構(gòu)是由AutoreleasePoolPage作為節(jié)點(diǎn)構(gòu)成的雙向鏈表,而每個(gè)AutoreleasePoolPage里面有一個(gè)存放對象指針的)。
  • 棧里面的每個(gè)指針要么是等待autorelease的對象,要么是POOL_BOUNDARY自動釋放池邊界(實(shí)際為#define POOL_BOUNDARY nil,同時(shí)也是 next 的指向)。一個(gè)pool token是指向 POOL_BOUNDARY 的指針。
    -AutoreleasePoolPage 會根據(jù)需要進(jìn)行動態(tài)添加和刪除。
  • hotPage 保存在當(dāng)前線程中,當(dāng)有新的autorelease 對象添加進(jìn)自動釋放池時(shí)會被添加到hotPage

先大概理解一下,下面繼續(xù)看下AutoreleasePoolPage類:

//************宏定義************
#define PAGE_MIN_SIZE           PAGE_SIZE
#define PAGE_SIZE               I386_PGBYTES
#define I386_PGBYTES            4096            /* bytes per 80386 page */

//************類定義************
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
    friend struct thread_data_t;

public:
    //頁的大小
    static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MIN_SIZE;  // size and alignment, power of 2
#endif
... 方法實(shí)現(xiàn)先省略
}

從源碼中可以看出AutoreleasePoolPage 是私有繼承自AutoreleasePoolPageData的類,并且頁的大小為 4096字節(jié),保存的 autorelease 對象的指針,每個(gè)指針占 8 個(gè)字節(jié)(后面會進(jìn)行驗(yàn)證)。

我們繼續(xù)看下AutoreleasePoolPageData,是一個(gè)結(jié)構(gòu)體:

class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
    magic_t const magic; //用來校驗(yàn)AutoreleasePoolPage的結(jié)構(gòu)是否完整
    __unsafe_unretained id *next; // 作為游標(biāo)指向棧頂最新 add 進(jìn)來的 autorelease 對象的下一個(gè)位置
    pthread_t const thread; // AutoreleasePool是按線程一一對應(yīng)的,thread 是自動釋放池所處的線程
    AutoreleasePoolPage * const parent; // 指向前面的節(jié)點(diǎn)
    AutoreleasePoolPage *child;  // 指向后面的節(jié)點(diǎn)
    uint32_t const depth; // 標(biāo)記每個(gè)結(jié)點(diǎn)在鏈表中的深度,第一個(gè)節(jié)點(diǎn)為 0,后面依次遞增
    uint32_t hiwat;

      // 構(gòu)造函數(shù)
    AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }
};

AutoreleasePoolPageData結(jié)構(gòu)體中可以看出,其包含構(gòu)成雙向鏈表的兩個(gè)指針parentchild,所以說明自動釋放池確實(shí)是一個(gè)雙向鏈表結(jié)構(gòu)。

AutoreleasePoolPageData結(jié)構(gòu)體占用的內(nèi)存大小為56個(gè)字節(jié)(這個(gè)點(diǎn)在下面會體現(xiàn)到):
-magic的類型是 magic_t結(jié)構(gòu)體,成員變量只有uint32_t m[4];,占用 4 * 4 = 16字節(jié)

  • next、thread 、parent、child均占8字節(jié),即4 * 8 = 32字節(jié)
  • 屬性depth、hiwat類型為uint32_t,實(shí)際類型是unsigned int類型,均占4字節(jié)(即2 * 4 = 8字節(jié))

大概是下圖這樣的結(jié)構(gòu):

image.png

其中的56個(gè)字節(jié)存儲的AutoreleasePoolPage的成員變量,其他的區(qū)域存儲加載到自動釋放池的對象。
當(dāng)next==begin()時(shí)表示AutoreleasePoolPage為空,當(dāng)next==end()的時(shí)表示AutoreleasePoolPage已滿。

大概了解了AutoreleasePool的結(jié)構(gòu)之后,我們就從objc_autoreleasePoolPush方法開始進(jìn)行分析。

2. objc_autoreleasePoolPush源碼分析

objc_autoreleasePoolPush方法,內(nèi)部是調(diào)用了AutoreleasePoolPage::push()方法,源碼如下:

//入棧
static inline void *push() 
{
    id *dest;
    // OPTION( DebugPoolAllocation, OBJC_DEBUG_POOL_ALLOCATION,
    // "halt when autorelease pools are popped out of order, 
    // and allow heap debuggers to track autorelease pools")
    // 當(dāng)自動釋放池彈出順序時(shí)停止,并允許堆調(diào)試器跟蹤自動釋放池

    if (slowpath(DebugPoolAllocation)) {
        // Each autorelease pool starts on a new pool page.自動釋放池從新池頁面開始
        // 創(chuàng)建一個(gè)新的自動釋放池
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        // 壓棧一個(gè)POOL_BOUNDARY,即壓棧哨兵
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    return dest;
}

從源碼中可以看出,如果自動釋放池不存在,則構(gòu)建一個(gè)新的page。push 函數(shù)的作用可以理解為,調(diào)用AutoreleasePoolPage::push在當(dāng)前線程的存儲空間保存一個(gè) EMPTY_POOL_PLACEHOLDER。

autoreleaseFast 函數(shù)比autoreleaseNewPage多了一個(gè)判斷page還沒有滿時(shí)就直接添加 objpage中的邏輯,剩下的調(diào)用autoreleaseFullPageautoreleaseNoPage 是一樣的。

2.1 autoreleaseNewPage函數(shù)

autoreleaseNewPage函數(shù)源碼如下:

static __attribute__((noinline))
    id *autoreleaseNewPage(id obj)
    {
        // 獲取當(dāng)前操作的page
        AutoreleasePoolPage *page = hotPage();
        // 如果存在則壓棧對象
        if (page) return autoreleaseFullPage(obj, page);
        // 如果不存在則創(chuàng)建頁
        else return autoreleaseNoPage(obj);
    }
2.2 hotPage

獲取當(dāng)前的頁,hotPage是在線程中存儲的,代碼如下:

static inline AutoreleasePoolPage *hotPage() 
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
        // 如果是一個(gè)空池,則返回nil,否則,返回當(dāng)前線程的自動釋放池
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }
2.3 autoreleaseFullPage

當(dāng)page 滿了的時(shí)候,就會調(diào)用autoreleaseFullPage方法,主要是構(gòu)建新的AutoreleasePoolPage,添加進(jìn)雙向鏈表中,并把obj添加進(jìn)去,代碼如下:

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.
    // 如果 hotpage 滿了,轉(zhuǎn)到下一個(gè)未滿的 page,如果不存在的話添加一個(gè)新的 page。  
    // 然后把 object 添加到新 page 里。
    
    // page 必須是 hotPage
    ASSERT(page == hotPage());
    // page 滿了,或者自動釋放池按順序彈出時(shí)暫停,并允許堆調(diào)試器跟蹤自動釋放池
    
    // OPTION( DebugPoolAllocation,
    //         OBJC_DEBUG_POOL_ALLOCATION,
    //         "halt when autorelease pools are popped out of order,
    //          and allow heap debuggers to track autorelease pools")
    // 自動釋放池按順序彈出時(shí)暫停,并允許堆調(diào)試器跟蹤自動釋放池
    
    ASSERT(page->full()  ||  DebugPoolAllocation);

    // do while 循環(huán)里面分為兩種情況
    // 1. 沿著 child 往后走,如果能找到一個(gè)非滿的 page,則可以把 obj 放進(jìn)去
    // 2. 如果 child 不存在或者所有的 child 都滿了,
    //    則構(gòu)建一個(gè)新的 AutoreleasePoolPage 并拼接在 AutoreleasePool 的雙向鏈表中,
    //    并把 obj 添加進(jìn)新 page 里面
    do {
        if (page->child) page = page->child;
        // 構(gòu)造方法時(shí)傳進(jìn)的page的child會指向新構(gòu)建的page,也就是添加新構(gòu)建的page到雙向鏈表中
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    // 設(shè)置 page 為 hotPage
    setHotPage(page);
    
    // 把 obj 添加進(jìn) page 里面,返回值是 next 之前指向的位置 (objc_object **)
    return page->add(obj);
}

其中 page->add(obj)是將obj 壓入棧中:

id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }
2.4 autoreleaseNoPage

autoreleaseNoPage創(chuàng)建新頁的方法,代碼如下:

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    // "No page" could mean no pool has been pushed or an empty
    // placeholder pool has been pushed and has no contents yet
    // "No page" 可能意味著沒有構(gòu)建任何池,或者只有一個(gè) EMPTY_POOL_PLACEHOLDER 占位
    
    // hotPage 不存在,否則執(zhí)行斷言
    ASSERT(!hotPage());

    bool pushExtraBoundary = false;
    if (haveEmptyPoolPlaceholder()) {
        // 如果線程里面存儲的是 EMPTY_POOL_PLACEHOLDER
        
        // We are pushing a second pool over the empty placeholder pool
        // or pushing the first object into the empty placeholder pool.
        // 我們正在將第二個(gè)池推入空的占位符池,或者將第一個(gè)對象推入空的占位符池。
        // Before doing that, push a pool boundary on behalf of the
        // pool that is currently represented by the empty placeholder.
        // 在此之前,代表當(dāng)前由空占位符表示的池來推動池邊界
        
        pushExtraBoundary = true;
    }
    else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
        // OPTION( DebugMissingPools, OBJC_DEBUG_MISSING_POOLS,
        // "warn about autorelease with no pool in place, which may be a leak")
        // 警告在沒有自動釋放池的情況下進(jìn)行 autorelease,
        // 這可能導(dǎo)致內(nèi)存泄漏(可能是因?yàn)闆]有釋放池,然后對象缺少一次 objc_release 執(zhí)行,導(dǎo)致內(nèi)存泄漏)
        // 如果 obj 不為 nil 并且 DebugMissingPools。
        
        // We are pushing an object with no pool in place,
        // and no-pool debugging was requested by environment.
        // 我們正在沒有自動釋放池的情況下把一個(gè)對象往池里推,
        // 并且打開了 environment 的 no-pool debugging,此時(shí)會在控制臺給一個(gè)提示信息。
        
        // 線程內(nèi)連 EMPTY_POOL_PLACEHOLDER 都沒有存儲,并且如果 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", 
                     objc_thread_self(), (void*)obj, object_getClassName(obj));
                     
        // obj 不為 nil,并且線程內(nèi)連 EMPTY_POOL_PLACEHOLDER 都沒有存儲
        // 執(zhí)行 objc_autoreleaseNoPool,且它是個(gè) hook 函數(shù)             
        objc_autoreleaseNoPool(obj);
        
        // 返回 nil
        return nil;
    }
    else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        // OPTION( DebugPoolAllocation, OBJC_DEBUG_POOL_ALLOCATION, 
        //         "halt when autorelease pools are popped out of order, 
        // and allow heap debuggers to track autorelease pools")
        // 當(dāng)自動釋放池順序彈出時(shí)暫停,并允許堆調(diào)試器跟蹤自動釋放池
        // 如果 obj 為空,并且沒有打開 DebugPoolAllocation
        
        // We are pushing a pool with no pool in place,
        // and alloc-per-pool debugging was not requested.
        // 在沒有池的情況下,我們設(shè)置一個(gè)空池占位,并且不要求為池分配空間和調(diào)試。(空池占位只是一個(gè) ((id*)1))
        
        // Install and return the empty pool placeholder.
        
        // 根據(jù) key 在當(dāng)前線程的存儲空間內(nèi)保存 EMPTY_POOL_PLACEHOLDER 占位
        return setEmptyPoolPlaceholder();
    }

    // We are pushing an object or a non-placeholder'd pool.
    // 構(gòu)建非占位的池

    // Install the first page.
    // 構(gòu)建自動釋放池的第一個(gè)真正意義的 page
    
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    
    // 設(shè)置為 hotPage
    setHotPage(page);
    
    // Push a boundary on behalf of the previously-placeholder'd pool.
    // 代表先前占位符的池推邊界。
    
    // 如果之前有一個(gè) EMPTY_POOL_PLACEHOLDER 在當(dāng)前線程的存儲空間里面占位的話
    if (pushExtraBoundary) {
        // 池邊界前進(jìn)一步
        
        // 可以理解為把 next 指針往前推進(jìn)了一步,并在 next 之前的指向下放了一個(gè) nil 
        page->add(POOL_BOUNDARY);
    }
    
    // Push the requested object or pool.
    // 把 objc 放進(jìn)自動釋放池
    return page->add(obj);
}

這個(gè)方法主要是在線程中創(chuàng)建一個(gè)EMPTY_POOL_PLACEHOLDER占位,然后創(chuàng)建自動釋放池的第一頁,并且將哨兵對象壓進(jìn)棧中。

其中AutoreleasePoolPage的創(chuàng)建是通過其構(gòu)造方法實(shí)現(xiàn)的,它的構(gòu)造方法其實(shí)是AutoreleasePoolPageData的構(gòu)造方法:

//**********AutoreleasePoolPage構(gòu)造方法**********
    AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),//開始存儲的位置
                                objc_thread_self(),//傳的是當(dāng)前線程,當(dāng)前線程時(shí)通過tls獲取的
                                newParent,
                                newParent ? 1+newParent->depth : 0,//如果是第一頁深度為0,往后是前一個(gè)的深度+1
                                newParent ? newParent->hiwat : 0)
{ 
    if (parent) {
        parent->check();
        ASSERT(!parent->child);
        parent->unprotect();
        //this 表示 新建頁面,將當(dāng)前頁面的子節(jié)點(diǎn) 賦值為新建頁面
        parent->child = this;
        parent->protect();
    }
    protect();
}

//**********AutoreleasePoolPageData初始化方法**********
AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }

上面 begin()表示壓棧的位置,即下一個(gè)要釋放對象的壓棧地址,其實(shí)就是頁的首地址 + 56,這個(gè)56其實(shí)就是結(jié)構(gòu)體AutoreleasePoolPageData本身所占用的內(nèi)存大小,在上面我們已經(jīng)計(jì)算過了。

2.2 autoreleaseFast

代碼實(shí)現(xiàn)如下:

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

主要是判斷當(dāng)前頁是否滿了,如果沒滿,則直接添加,如果滿了則開辟新頁,如果當(dāng)前頁都沒有,再進(jìn)入autoreleaseNoPage的流程。

2.3 自動釋放池內(nèi)存結(jié)構(gòu)

在分析了objc_autoreleasePoolPush之后,我們對于自動釋放池有了個(gè)大概的認(rèn)識,現(xiàn)在我們通過代碼去驗(yàn)證下他的結(jié)構(gòu):
由于在ARC模式下,是無法手動調(diào)用autorelease,所以將Demo切換至MRC模式(Build Settings -> Objectice-C Automatic Reference Counting設(shè)置為NO):

image.png

定義如下代碼:

//************打印自動釋放池結(jié)構(gòu)************
extern void _objc_autoreleasePoolPrint(void);

//************運(yùn)行代碼************
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //循環(huán)創(chuàng)建對象,并加入自動釋放池
        for (int i = 0; i < 5; i++) {
             NSObject *objc = [[NSObject alloc] sutorelease];
        }
        //調(diào)用
        _objc_autoreleasePoolPrint();
    }
}

運(yùn)行結(jié)果如下,發(fā)現(xiàn)是 6 個(gè),但是我們實(shí)際上創(chuàng)建了 5個(gè)對象啊,其中的POOL表示哨兵,即邊界,其目的是為了防止越界,也就是我們在上面autoreleaseNoPage方法中添加的POOL_BOUNDARY:

image.jpeg

從上圖中我們看到,page 的首地址是0x100817000 和 哨兵對象的地址0x100817038相差0x38 ,轉(zhuǎn)換成十進(jìn)制剛好是56,也就是AutoreleasePoolPage本身所占用的大小,想想上面的 begin()方法,也是從首地址 + 56 開始存儲的。

在一開始我們還提到 page的大小為 4096 字節(jié),自身占用了56個(gè)字節(jié),哨兵對象占用了8個(gè)字節(jié),所以除去哨兵對象還能存儲 4032 / 8 = 504個(gè)對象,我們將上述的測試代碼的循環(huán)次數(shù)數(shù)據(jù)改為 505,其內(nèi)存結(jié)構(gòu)如下,發(fā)現(xiàn)第一頁滿了,存儲了504個(gè)要釋放的對象,第二頁只存儲了一個(gè) ,說明是對的:

image.png

我們將數(shù)據(jù)改為505 + 506,來驗(yàn)證第二頁是否也是存儲 505個(gè)對象:

image.jpeg

可以發(fā)現(xiàn),第一頁存儲了504 個(gè),第二頁存儲了 505個(gè),第三頁存儲了2 個(gè)。

所以通過上述測試,可以得出以下結(jié)論:

  • 第一頁可以存放 504 個(gè)對象,且只有第一頁有哨兵對象,當(dāng)?shù)谝豁摑M了,就會開辟新的頁
  • 第二頁開始,最多可以存放505個(gè)對象
  • 一頁的大小 = 505 * 8 + 56 = 4096 字節(jié)

自動釋放池結(jié)構(gòu)如下圖所示:


image.png
2.4. autorelease底層分析

在 demo 中,我們通過 autorelease方法,在MRC的模式下,將對象壓棧到自動釋放池,會調(diào)用objc_autorelease方法,源碼如下:

__attribute__((aligned(16), flatten, noinline))
id
objc_autorelease(id obj)
{
    //如果不是對象,則直接返回
    if (!obj) return obj;
    //如果是小對象,也直接返回
    if (obj->isTaggedPointer()) return obj;
    return obj->autorelease();
}

進(jìn)入對象的autorelease實(shí)現(xiàn):

??
inline id 
objc_object::autorelease()
{
    ASSERT(!isTaggedPointer());
    //判斷是否是自定義類
    if (fastpath(!ISA()->hasCustomRR())) {
        return rootAutorelease();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(autorelease));
}
??
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());
    //autoreleaseFast 壓棧操作
    id *dest __unused = autoreleaseFast(obj);
    ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    return obj;
}

從這里看出,無論是壓棧哨兵對象,還是普通對象,都會來到autoreleaseFast方法,只是區(qū)別標(biāo)識不同。

2.5 總結(jié)

autoreleaseobjc_autoreleasePush的整體流程如下圖所示:

image.png

3. objc_autoreleasePoolPop 源碼分析

objc_autoreleasePoolPop方法中有個(gè)參數(shù),在clang分析時(shí),發(fā)現(xiàn)傳入的參數(shù)是push壓棧后返回的對象,即ctxt,其目的是避免出棧混亂,防止將別的對象出棧:

static inline void
pop(void *token)
{
    AutoreleasePoolPage *page;
    id *stop;

    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        // Popping the top-level placeholder pool.
        // 彈出頂級 EMPTY_POOL_PLACEHOLDER 占位符池
        
        // 取出 hotPage
        page = hotPage();
        if (!page) {
            // 如果 hotPage 不存在,則表示目前就一 EMPTY_POOL_PLACEHOLDER,說明池還沒有使用過
            // Pool was never used. Clear the placeholder.
            // Pool 從未使用過。清除占位符。
            return setHotPage(nil);
        }
        // Pool was used. Pop its contents normally.
        // Pool 是使用過了。正常彈出其內(nèi)容。
        // Pool pages remain allocated for re-use as usual.
        // Pool pages 保持分配以照常使用.
        
        // 第一個(gè) page
        page = coldPage();
        // 把第一個(gè) page 的 begin 賦值給 token
        token = page->begin();
    } else {
        // token 轉(zhuǎn)為 page 
        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);
        }
    }
    
    // allowDebug 為 true
    if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
        return popPageDebug(token, page, stop);
    }
    
    // 釋放對象刪除 page
    return popPage<false>(token, page, stop);
}


進(jìn)入popPage源碼,其中傳入的allowDebugfalse,則通過releaseUntil出棧當(dāng)前頁stop位置之前的所有對象,即向棧中的對象發(fā)送release消息,直到遇到傳入的哨兵對象:

//出棧頁面
template<bool allowDebug>
    static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    if (allowDebug && PrintPoolHiwat) printHiwat();
    //出棧當(dāng)前操作頁面對象
    page->releaseUntil(stop);

    // memory: delete empty children 刪除空子項(xiàng)
    if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        //調(diào)試期間刪除每個(gè)特殊情況下的所有池
        //獲取當(dāng)前頁面的父節(jié)點(diǎn)
        AutoreleasePoolPage *parent = page->parent;
        //將當(dāng)前頁面殺掉
        page->kill();
        //設(shè)置操作頁面為父節(jié)點(diǎn)頁面
        setHotPage(parent);
    }
    else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        // special case: delete everything for pop(top)
        // when debugging missing autorelease pools
        //特殊情況:調(diào)試丟失的自動釋放池時(shí)刪除pop(top)的所有內(nèi)容
        page->kill();
        setHotPage(nil);
    }
    else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full 如果頁面已滿一半以上,則保留一個(gè)空子級
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

進(jìn)入releaseUntil實(shí)現(xiàn),主要是通過循環(huán)遍歷,判斷對象是否等于stop,其目的是釋放stop之前的所有的對象:

    void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        // 循環(huán)從 next 開始,一直后退,直到 next 到達(dá) stop
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            
            // 取得當(dāng)前的 AutoreleasePoolPage
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            // fixme 我認(rèn)為 “while” 可以是 “if”,但我無法證明
            // 我覺得也是可以用 if 代替 while
            // 一個(gè) page 滿了會生成一個(gè)新的 page 并鏈接為下一個(gè) page,
            // 所以從第一個(gè) page 開始到 hotPage 的前一個(gè)page,應(yīng)該都是滿的
            
            // 如果當(dāng)前 page 已經(jīng)空了,則往后退一步,把前一個(gè) AutoreleasePoolPage 作為 hotPage
            while (page->empty()) {
                // 當(dāng)前 page 已經(jīng)空了,還沒到 stop,
                // 往后走 
                page = page->parent;
                // 把 page 作為 hotPage
                setHotPage(page);
            }
            
            // 可讀可寫
            page->unprotect();
            
            // next 后移一步,并用解引用符取出 objc_object * 賦值給 obj
            id obj = *--page->next;
            
            // 把 page->next 開始的 sizeof(*page->next) 個(gè)字節(jié)置為 SCRIBBLE
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            
            // 只可讀
            page->protect();
            
            // 如果 obj 不為 nil,則執(zhí)行 objc_release 操作
            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }

        // 這里還是把 this 作為 hotPage,
        // 可能從 stop 所在的 page 開始到 hotPage 這些 page 本來存放自動釋放對象的位置都放的是 SCRIBBLE
        setHotPage(this);

#if DEBUG
        // we expect any children to be completely empty
        // 保證從當(dāng)前 page 的 child 開始,向后都是空 page
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            ASSERT(page->empty());
        }
#endif
    }


從最前面的page開始一直向后移動直到到達(dá)stop 所在的 page,并把經(jīng)過的page里保存的對象都執(zhí)行一次 objc_release操作,把之前每個(gè)存放 objc_object ** 的空間都置為 SCRIBBLE,每個(gè)pagenext 都指向了該 pagebegin。

release做的事情是遍歷釋放保存的自動釋放對象,而kill 做的事情是遍歷對 AutoreleasePoolPage 執(zhí)行 delete操作:

void kill() 
{
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
    
    AutoreleasePoolPage *page = this;
    // 從當(dāng)前 page 開始一直沿著 child 鏈往前走,直到 AutoreleasePool 的雙向鏈表的最后一個(gè) page
    while (page->child) page = page->child;

    // 臨時(shí)變量(死亡指針)
    AutoreleasePoolPage *deathptr;
    
    // 是 do while 循環(huán),所以會至少進(jìn)行一次 delete,
    // 即當(dāng)前 page 也會被執(zhí)行 delete(不同與上面的 release 操作,入?yún)?stop 并不會執(zhí)行 objc_release 操作)
    do {
        // 要執(zhí)行 delete 的 page
        deathptr = page;
        
        // 記錄前一個(gè) page
        page = page->parent;
        
        // 如果當(dāng)前 page 的 parent 存在的話,要把這個(gè) parent 的 child 置為 nil
        // 這個(gè)是鏈表算法題的經(jīng)典操作
        if (page) {
            // 可讀可寫
            page->unprotect();
            
            // child 置為 nil
            page->child = nil;
            
            // 可寫
            page->protect();
        }
        
        // delete page
        delete deathptr;
    } while (deathptr != this);
}


從當(dāng)前的page 開始,一直根據(jù) child 鏈向前走直到 child為空,把經(jīng)過的page全部執(zhí)行delete 操作(包括當(dāng)前page)。

當(dāng)pthread_exit線程退出時(shí),觸發(fā)了_pthread_tsd_cleanup,觸發(fā)AutoreleasePoolPagetls_dealloc(void*),然后回收autorelease對象:

static void tls_dealloc(void *p) 
{
    // # define EMPTY_POOL_PLACEHOLDER ((id*)1)
    // 如果 p 是空占位池則 return
    if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
        // No objects or pool pages to clean up here.
        // 這里沒有 objects 或者 pages 需要清理
        return;
    }

    // reinstate TLS value while we work
    // 這里直接把 p 保存在 TLS 中作為 hotPage
    setHotPage((AutoreleasePoolPage *)p);

    if (AutoreleasePoolPage *page = coldPage()) {
        // 如果 coldPage 存在(雙向鏈表中的第一個(gè) page)
        
        // 這個(gè)調(diào)用的函數(shù)鏈超級長,最終實(shí)現(xiàn)的是把自動釋放池里的所有自動釋放對象都執(zhí)行
        // objc_release 然后所有的 page 執(zhí)行 delete 
        if (!page->empty()) objc_autoreleasePoolPop(page->begin());  // pop all of the pools
        
        // OPTION( DebugMissingPools, 
        //         OBJC_DEBUG_MISSING_POOLS,
        //         "warn about autorelease with no pool in place, which may be a leak")
        // 警告沒有池的自動釋放,這可能是泄漏
        
        // OPTION( DebugPoolAllocation,
        //         OBJC_DEBUG_POOL_ALLOCATION,
        //         "halt when autorelease pools are popped out of order,
        //          and allow heap debuggers to track autorelease pools")
        // 當(dāng)自動釋放池順序彈出時(shí)暫停,并允許堆調(diào)試器跟蹤自動釋放池
        
        if (slowpath(DebugMissingPools || DebugPoolAllocation)) {
            // pop() killed the pages already
        } else {
            // 從 page 開始一直沿著 child 向前把所有的 page 執(zhí)行 delete
            // kill 只處理 page,不處理 autorelease 對象
            page->kill();  // free all of the pages
        }
    }
    
    // clear TLS value so TLS destruction doesn't loop
    // 清除 TLS 值,以便 TLS 銷毀不會循環(huán)
    // 把 hotPage 置為 nil
    // static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    // tls_set_direct(key, (void *)page);
    // 把 key 置為 nil
    setHotPage(nil);
}

綜上所述,objc_autoreleasePoolPop出棧的流程如下所示:

image.png

4. AutoreleasePool和RunLoop

一般很少會將自動釋放池RunLoop聯(lián)系起來,但是如果打印[NSRunLoop currentRunLoop]結(jié)果中會發(fā)現(xiàn)和自動釋放池相關(guān)的回調(diào)。

<CFRunLoopObserver 0x6000024246e0 [0x7fff8062ce20]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
<CFRunLoopObserver 0x600002424640 [0x7fff8062ce20]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}

即App啟動后,蘋果會給RunLoop注冊很多個(gè)observers,其中有兩個(gè)是跟自動釋放池相關(guān)的,其回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()。

第一個(gè)observer監(jiān)聽的是activities=0x1(kCFRunLoopEntry),也就是在即將進(jìn)入loop時(shí),其回調(diào)會調(diào)用_objc_autoreleasePoolPush()創(chuàng)建自動釋放池;
第二個(gè)observer監(jiān)聽的是activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit),
即監(jiān)聽的是準(zhǔn)備進(jìn)入睡眠和即將退出loop兩個(gè)事件。在準(zhǔn)備進(jìn)入睡眠之前,因?yàn)樗呖赡軙r(shí)間很長,所以為了不占用資源先調(diào)用_objc_autoreleasePoolPop()釋放舊的釋放池,并調(diào)用_objc_autoreleasePoolPush()創(chuàng)建新建一個(gè)新的,用來裝載被喚醒后要處理的事件對象;在最后即將退出loop時(shí)則會_objc_autoreleasePoolPop()釋放池子。

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

相關(guān)閱讀更多精彩內(nèi)容

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