iOS底層探索 --- AutoReleasePool

AutoReleasePool 自動釋放池

AutoReleasePool是OC的一種內(nèi)存自動回收機制,它可以將加入AutoReleasePool變量的release時機 --- 延遲环础。
當我們創(chuàng)建一個對象的時候溜腐,正常情況下,變量會在超出其作用域的時候立即release。如果將對象加入到自動釋放池中,這個對象不會立即釋放,而是等到runloop休眠 或者 超出autoreleasepool作用域{}之后才會被釋放展融。

自動釋放池

  • 1、從程序啟動到加載完成豫柬,主線程對應的RunLoop會處于休眠狀態(tài)告希,等待用戶交互來喚醒RunLoop
  • 2烧给、用戶的每一次交互都會啟動一次RunLoop燕偶,用于處理用戶的所有點擊,觸摸事件等础嫡。
  • 3指么、RunLoop在監(jiān)聽到交互事件之后,就會創(chuàng)建自動釋放池榴鼎,并將所有延遲釋放的對象添加到自動釋放池伯诬。
  • 4、在一次完整的RunLoop結束之前巫财,會向自動釋放池中所有的對象發(fā)送release消息盗似,然后\color{red}{銷毀}自動釋放池。

在大致了解自動釋放池的工作流程之后翁涤,我們一起來探索一下自動釋放池桥言。
在日常的開發(fā)中萌踱,我們見到的最多的自動釋放池就是main函數(shù)里面的自動釋放池葵礼。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
*******
在這個@autoreleasepool Block 中号阿,只包含了一行代碼,這行代碼將所有的事件鸳粉、消息全部交給了`UIApplication`來處理扔涧。
?????? 注意:整個 iOS 的應用都是包含在一個自動釋放池 Block 中的。

下面我們?yōu)榱藴p少干擾代碼届谈,將自動釋放池中的代碼刪除枯夜,只保留自動釋放池。然后將main.m文件轉(zhuǎn)cpp來探索一下艰山,就像我們探索Block一樣(Block 底層原理(一)

int main(int argc, char * argv[]) {
    @autoreleasepool {
    }
}

**************
$ clang -rewrite-objc main.m

main.cpp文件中我們可以看到:

struct __AtAutoreleasePool {
    ///構造函數(shù)
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
    ///析構函數(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; 
    }
}

我們會發(fā)現(xiàn)湖雹,main.m里面的@autoreleasepool {}變成了__AtAutoreleasePool __autoreleasepool。而__AtAutoreleasePool又是一個結構體曙搬;所以自動釋放池是一個\color{orange}{結構體對象}摔吏。
仔細觀察__AtAutoreleasePool這個結構體,會發(fā)現(xiàn)結構體兩個函數(shù):
1纵装、構造函數(shù) objc_autoreleasePoolPush() 征讲,會在結構體初始化的時候調(diào)用;
2橡娄、析構函數(shù) objc_autoreleasePoolPop()诗箍,還在結構體析構的時候調(diào)用(也就是說在出了作用域后,會自動調(diào)用析構)挽唉。
看到兩個函數(shù)滤祖,不知道大家有什么想法,給我的感覺就是瓶籽,這個一定跟\color{orange}{棧}有關匠童,大家仔細品一品這兩個函數(shù)名。

到這里棘劣,我們好像已經(jīng)將自動釋放池的面紗揭開了一點點俏让,下面我們順著這個思路繼續(xù)探索。


源碼探索

上面我們看到了連個函數(shù)茬暇,我們在源碼中找這個連個函數(shù)是下面的樣子:

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

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

我們會發(fā)現(xiàn)首昔,兩個函數(shù)中,都用到了AutoreleasePoolPage糙俗,這兩個函數(shù)就是對AutoreleasePoolPage對應的靜態(tài)方法push&pop的封裝勒奇。
那么我們就再去尋找一下AutoreleasePoolPage

class AutoreleasePoolPage : private AutoreleasePoolPageData
{.......}
///繼續(xù)跟進
????
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
    magic_t const magic;
    __unsafe_unretained id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;

    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)
    {
    }
};
  • magic 用來校驗 AutoreleasePoolPage 的結構是否完整巧骚;
  • next 指向最新添加的autoreleased對象的下一個位置赊颠,初始化時指向begin()格二;
  • thread 指向當前線程;
  • parent 指向父結點竣蹦,第一個結點的parent值為nil;
  • child 指向子結點顶猜,最后一個結點的child值為nil;
  • depth 代表深度,從 0 開始痘括,往后遞增 1长窄;
  • hiwat 代表 high water mark 最大入棧數(shù)量標記。(這個地方驗證上面的猜想纲菌,自動釋放池一定跟棧有關挠日。)

每一個自動釋放池都是由一系列AutoreleasePoolPage組成的,并且每一個AutoreleasePoolPage的大小都是4096字節(jié)(16進制0x1000)

#define I386_PGBYTES            4096            /* bytes per 80386 page */
#define PAGE_SIZE               I386_PGBYTES
  • 雙向鏈表
    自動釋放池中的AutoreleasePoolPage是以雙向鏈表的形式連接起來的翰舌,這一點我們通過AutoreleasePoolPage里面的參數(shù)parent & child就可以知道嚣潜。
    • AutoreleasePoolPageData結構體的內(nèi)存大小為56字節(jié):
      • 屬性magic的類型是magic_t結構體,所占內(nèi)存大小為m[4] --- 4*4 = 16字節(jié)椅贱;
      • 屬性next(指針)懂算、thread(對象)parent(對象)夜涕、child(對象)均占8字節(jié)犯犁,所以8*4 = 32字節(jié)
      • 屬性depth女器、hiwat類型為uint32_t酸役,實際類型為unsigned int類型,均占4字節(jié)驾胆,所以4*2 = 8字節(jié)涣澡。

自動釋放池中的棧
  • POOL_BOUNDARY(哨兵對象)
    在我們接著源碼分析之前,先來了解一個感念:POOL_BOUNDARY(哨兵對象)
    哨兵對象只是nil的別名:
#   define POOL_BOUNDARY nil

在每個自動釋放池初始化調(diào)用objc_autoreleasePoolPush的時候丧诺,都會把一個POOL_BOUNDARY Push到自動釋放池的棧頂入桂,并且返回這個POOL_BOUNDARY(哨兵對象)
而當方法objc_autoreleasePoolPop被調(diào)用的時候驳阎,就會像自動釋放池中的對象發(fā)送release消息抗愁,直到第一個POOL_BOUNDARY


objc_autoreleasePoolPush

通過上面的分析呵晚,我們已經(jīng)直到objc_autoreleasePoolPush實際調(diào)用的就是push方法蜘腌,那么我們就進入push方法里面去一探究竟。

static inline void *push() 
    {
        id *dest;
        ///判斷是否有pool
        if (slowpath(DebugPoolAllocation)) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            // 壓棧一個POOL_BOUNDARY饵隙,哨兵壓棧
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }
  • autoreleaseFast
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);
        }
    }

上面分成了三種不同的情況:

  • 1:有hotPage撮珠,并且當前page不滿
    • 調(diào)用page->add(obj)方法,將對象添加至AutoreleasePoolPage的棧中金矛。
  • 2:有hotPage并且page已滿
    • 調(diào)用autoreleaseFullOPage初始化一個新的頁芯急;
    • 調(diào)用page->add(obj)方法勺届,將對象添加至AutoreleasePoolPage的棧中(這個在autoreleaseFullOPage函數(shù)里面有)。
  • 3:無hotPage
    • 調(diào)用autoreleaseNoPage創(chuàng)建一個hotPage娶耍;
    • 調(diào)用page->add(obj)方法免姿,將對象添加到AutoreleasePoolPage的棧中。

通過上面可以看到伺绽,最后都會調(diào)用page->add(obj)方法养泡,將對象添加到自動釋放池中嗜湃。hotPage可以理解為當前正在使用的AutoreleasePoolPage奈应。

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

這個方法其實就是一個壓棧的操作,將對象加入AutoreleasePoolPage购披,飯后移動棧頂指針杖挣。

  • autoreleaseFullOPage(當前的hotPage已滿)
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 {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }

它會從傳入的page開始遍歷整個雙向鏈表,直到
1:查找到一個未滿的AutoreleasePoolPage刚陡。
2:使用構造器傳入parent創(chuàng)建一個新的AutoreleasePoolPage
在查找到一個可以使用的AutoreleasePoolPage之后惩妇,會將該頁面標記成houPage,然后調(diào)用page->add(obj)方法筐乳,添加對象歌殃。

  • autoreleaseNoPage(沒有hotPage)
    如果當前內(nèi)存中不存在hotPage的時候,就會調(diào)用autoreleaseNoPage方法初始化一個AutoreleasePoolPage
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
        ASSERT(!hotPage());

        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
            // We are pushing a second pool over the empty placeholder pool
            // or pushing the first object into the empty placeholder pool.
            // Before doing that, push a pool boundary on behalf of the pool 
            // that is currently represented by the empty placeholder.
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            // We are pushing an object with no pool in place, 
            // and no-pool debugging was requested by environment.
            _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));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            // We are pushing a pool with no pool in place,
            // and alloc-per-pool debugging was not requested.
            // Install and return the empty pool placeholder.
            return setEmptyPoolPlaceholder();
        }

        // We are pushing an object or a non-placeholder'd pool.

        // Install the first page.
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
        return page->add(obj);
    }

由于是從新構建的自動釋放池的雙向鏈表蝙云,所以新的AutoreleasePoolPage沒有parent指針氓皱。
初始化之后,將當前頁標記為hotPage勃刨,然后先向這個page中添加一個POOL_BOUNDARY(哨兵對象)波材,確保在pop的時候不會報錯。
最后依然是page->add(obj)身隐,將對象添加到自動釋放池廷区。


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

可以看到objc_autoreleasePoolPop方法的調(diào)用,是有一個參數(shù)傳遞進來的贾铝。那么這個參數(shù)是什么呢隙轻?不知道大家還記不記得我們上面clang出來的main.cpp文件。我們再來看一下里面的代碼:

struct __AtAutoreleasePool {
    ///構造函數(shù)
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
    ///析構函數(shù)
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

大家發(fā)現(xiàn)沒有垢揩,傳入的參數(shù)就是push壓棧后返回的哨兵對象atautoreleasepoolobj玖绿。
通過上面我們已經(jīng)知道,objc_autoreleasePoolPop最終調(diào)用的是pop方法水孩,那么我們就來看一下pop方法:

static inline void
    pop(void *token)
    {
        AutoreleasePoolPage *page;
        id *stop;
        // 判斷token是否是空占位符
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            // 獲取當前頁
            page = hotPage();
            if (!page) {
                // Pool was never used. Clear the placeholder.
                // 如果當前頁不存在镰矿,則清楚空占位符
                return setHotPage(nil);
            }
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            // 如果當前頁存在,則將當前頁設置為coldPage俘种,token設置為coldPage的開始位置
            page = coldPage();
            token = page->begin();
        } else {
            // 獲取token所在的頁
            page = pageForPointer(token);
        }

        stop = (id *)token;
        // 判斷最后一個位置秤标,是否是哨兵
        if (*stop != POOL_BOUNDARY) {
            // 進入 if 說明最后一個位置不是哨兵绝淡,也就是說最后一個位置是一個對象
            
            
            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 {
                // 出現(xiàn)混亂
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }

        // 出棧頁
        return popPage<false>(token, page, stop);
    }
  • 我們在pop方法中看到牢酵,正常情況下,最后出棧調(diào)用的是popPage方法衙猪;那么我們再來追蹤popPage方法馍乙。
template<bool allowDebug>
    static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
        if (allowDebug && PrintPoolHiwat) printHiwat();

        // 出棧當前操作頁面對象
        page->releaseUntil(stop);

        // memory: delete empty children
        // 刪除空子項
        if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            // 特殊情況:debug期間,刪除所有的池
            
            // 獲取當前頁的父結點
            AutoreleasePoolPage *parent = page->parent;
            // 將當前頁kill
            page->kill();
            // 設置操作頁為父結點頁
            setHotPage(parent);
        } else if (allowDebug && 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();
            }
        }
    }

可以看到popPage中垫释,會通過releaseUntil出棧當前頁stop位置之前的所有對象丝格,即向棧中的對象發(fā)送release消息,直到遇到傳入的哨兵對象棵譬。還有就是killchild頁显蝌,這一步操作可能有什么其他的考慮,暫時不是很清楚订咸。但是既然是出棧曼尊,那重點就是releaseUntil;延續(xù)我們之前的思路脏嚷,繼續(xù)追蹤releaseUntil

    // 釋放 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
        // 判斷下一個對象是否是stop骆撇,如果不是繼續(xù)循環(huán)
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            // 獲取當前操作頁,即hot頁
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            // 如果當前頁為空
            while (page->empty()) {
                // 將page的父結點頁賦值個page
                page = page->parent;
                // 設置當前頁為父結點頁
                setHotPage(page);
            }

            page->unprotect();
            // page->next減減父叙,出棧
            id obj = *--page->next;
            // 將page->next位置的索引神郊,設置為SCRIBBLE,表示已經(jīng)被釋放
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
                // 對象不是哨兵高每,就釋放對象
                objc_release(obj);
            }
        }

        setHotPage(this);

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            ASSERT(page->empty());
        }
#endif
    }

通過源碼我們開看到屿岂,raleaseUntil主要是通過循環(huán)遍歷,判斷當前對象是否是stop鲸匿,其目的是釋放stop之前的所有對象爷怀。
i:首先通過pagenext獲得對象,對next進行減減操作带欢,并且對索引進行更改运授;
ii:判斷獲得的對象是否為哨兵對象,如果不是乔煞,就釋放對象吁朦。

  • 在上面我們還提到了kill,同過字面意思也能理解這個方法是做什么的渡贾。
    它會將當前頁面以及子頁面全部銷毀逗宜。
    不過我們還是再來看一期其內(nèi)部實現(xiàn):
    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;
        // 一直循環(huán)到最后一頁
        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);
    }

探索了這么多,我們對于自動釋放池本質(zhì)壓棧纺讲、出棧都有了一定的了解擂仍。但是還有一個知識點我們還沒探索到,那就是autorelease熬甚。

autorelease
  • 如果不是對象逢渔,或者是小對象,直接返回乡括;
  • 如果是對象肃廓,則調(diào)用對象的autorelease方法,進行釋放诲泌。
__attribute__((aligned(16), flatten, noinline))
id
objc_autorelease(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->autorelease();
}

跟進對象的autorelease方法:

// objc_object::autorelease()inline id 
objc_object::autorelease()
{
    // 判斷是否是 `Tagged Pointer`,這個函數(shù)并不希望處理的對象是`Tagged Pointer`
    ASSERT(!isTaggedPointer());
    // 通過 `hasCustomRR`,檢查 類(包括其父類)中是否含有默認的方法
    if (fastpath(!ISA()->hasCustomRR())) {
        // 如果沒有盲赊,調(diào)用`rootAutorelease`函數(shù)
        return rootAutorelease();
    }
    // 如果有,則調(diào)用自定義方法
    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(autorelease));
}
??????
// objc_object::rootAutorelease()
inline id 
objc_object::rootAutorelease()
{
    // 如果是小對象档礁,直接返回
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}
??????
// objc_object::rootAutorelease2()
__attribute__((noinline,used))
id 
objc_object::rootAutorelease2()
{
    ASSERT(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}
??????
// 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;
    }
??????
// AutoreleasePoolPage::autoreleaseFast((id)this)
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);
        }
    }

所以autorelease的函數(shù)調(diào)用棧是這個樣子的:


看到autorelease的函數(shù)調(diào)用棧之后角钩,不知道大家有沒有感到熟悉;沒錯呻澜,有很多方法,我們在上面探索objc_autoreleasePoolPush的時候也見到過惨险。那么我們將autorelease和我們的壓棧結合起來羹幸,把整個流程串起來:

自動釋放池出棧流程圖:


總結:
1、自動釋放池是由AutoreleasePoolPage雙向鏈表的形式實現(xiàn)的辫愉。
2栅受、當對象調(diào)用autorelease方法的時候,會將對象加入AutoreleasePoolPage的棧中
3恭朗、調(diào)用AutoreleasePoolPage::pop方法會向棧中的對象發(fā)送release消息屏镊。也就是我們所說的出棧,主要通過page-next遞減操作來完成痰腮,當出棧對象不是哨兵的時候而芥,釋放對象。


  • Tips
    • assert斷言
      我們在源碼中發(fā)現(xiàn)有大量的斷言使用膀值,比如:ASSERT(!hotPage());
      在源碼中棍丐,我們看到它是一個宏,我們跟進去看看這個宏是什么樣的:
// An assert that's disabled for release builds but still ensures the expression compiles.
#ifdef NDEBUG
#define ASSERT(x) (void)sizeof(!(x))
#else
#define ASSERT(x) assert(x)
#endif

*****************
我們看到官方注釋的很清楚沧踏,這個斷言不能用于發(fā)布模式歌逢。

assert的作用是:校驗傳入的參數(shù)是否為;如果為翘狱,則向stderr打印一條出錯信息秘案,然后通過abort來終止應用程序。
assert的缺點是:頻繁的調(diào)用會極大的影響程序的性能,增加額外的開銷阱高。

用法和注意事項:
1师骗、在函數(shù)開始處,檢驗傳入?yún)?shù)的合法性

int resetBufferSize(int newSize) {
 assert(newSize >= 0);
 assert(newSize <= MaxSize);
}

2讨惩、每個assert只校驗一個條件癞尚,因為同時校驗多個條件時,如果斷言失敗遗契,無法直觀的判斷是哪個條件失敗蛋欣。

// 錯誤示范
assert(a > 0 && b > 0);
// 正確示范
assert(a>0);
assert(b>0);

3、不能使用改變環(huán)境的語句处面。

// 錯誤示范 如果在執(zhí)行之前 `i==100` , 那么這條語句就不會執(zhí)行厂置,那么`i++`這條命令就沒有執(zhí)行。
assert(i++ < 100);
// 正確示范
assert(i < 100);
i++;

4魂角、assert和后面的語句應空一行昵济,以形成邏輯個視覺上的一致感。

  • Tagged Pointer
    Tagged Pointer是一個特別的指針野揪,它分為兩個部分:
    i:一部分直接保存數(shù)據(jù)访忿;
    ii:一部分作為特殊標記,表示這個是一個特別的指針斯稳,不指向任何一個地方海铆。
    因此Tagged Pointer也被叫做偽指針
    • Tagged Pointer被設計的目的是用來存儲較小的對象,例如NSNumber挣惰、NSDate卧斟、NSString等等。
    • Tagged Pointer的值不再表示地址憎茂,而是真正的值珍语。
    • Tagged Pointer在內(nèi)存讀取上有這3倍的效率,創(chuàng)建的時候比以前快106倍竖幔。

常見面試題

  • 面試題1:臨時變量什么時候釋放板乙?

    • 如果在正常情況下,一般是超出作用域就會立即釋放赏枚。
    • 如果將臨時變量加入了自動釋放池亡驰,會延遲釋放,即在RunLoop休眠AutoreleasePool作用域之后釋放饿幅。
  • 面試題2:AutoreleasePool原理

    • 自動釋放池的本質(zhì)是一個AutoreleasePoolPage結構體對象凡辱,是一個棧結構存儲的頁,每一個AutoreleasePoolPage都是以雙向鏈表的形式連接的栗恩。
    • 自動釋放池的壓棧出棧主要是通過結構體的構造函數(shù)析構函數(shù)調(diào)用底層的objc_autoreleasePoolPushobjc_autoreleasePoolPop透乾,進而調(diào)用AutoreleasePoolPagepushpop兩個方法。
    • 每次調(diào)用push操作,其實就是創(chuàng)建一個新的AutoreleasePoolPage乳乌,而AutoreleasePoolPage的具體操作就是插入一個POOL_BOUNDARY(哨兵對象)捧韵,并返回插入POOL_BOUNDARY的內(nèi)存地址。而push內(nèi)部調(diào)用autoreleaseFast方法處理汉操,主要有以下三種情況:
      • page存在再来,且不滿的時候,調(diào)用add方法將對象添加至pagenext指針處磷瘤,并將next遞增芒篷。
      • page存在,且已滿的時候采缚,調(diào)用autoreleaseFullPage初始化一個新的page针炉,然后調(diào)用add方法將對象添加至page棧中。
      • page不存在的時候扳抽,調(diào)用autoreleaseNoPage創(chuàng)建一個hotPage篡帕,然后調(diào)用add方法,將對象添加到page棧中贸呢。
  • 當執(zhí)行pop操作的時候镰烧,會傳入一個值,這個值就是push操作的返回值贮尉,即POOL_BOUNDARY的內(nèi)存地址token拌滋。所以pop內(nèi)部的實現(xiàn)就是根據(jù)token找到哨兵對象所處的page(頁),然后使用objc_release釋放token之前的所有對象猜谚,并把next指針指向正確的位置。

  • 面試題3:AutoreleasePool能否嵌套使用赌渣?

    • 可以嵌套使用魏铅,其目的是控制應用程序的內(nèi)存峰值,使其不要太高坚芜。
    • 可以嵌套使用的原因是因為:自動釋放池是以為結點览芳,通過雙向鏈表的形式連接的,且是和線程一一對應的鸿竖。
    • 自動釋放池的多層嵌套其實就是不停的push哨兵對象沧竟,在pop時,會先釋放里面的缚忧,再釋放外面的悟泵。
  • 面試題4:哪些對象可以加入AutoreleasePool?alloc創(chuàng)建的可以嗎闪水?

    • 使用new / alloc / copy關鍵字生成的對象 和 retain了的對象需要手動釋放糕非,不會被添加到自動釋放池中。
    • 設置為autorelease的對象不需要手動釋放,會直接進入自動釋放池朽肥。
    • 所有autorelease的對象禁筏,在出了作用域之后,會被自動添加到最新創(chuàng)建的自動釋放池之中衡招。
  • 面試題5:AutoreleasePool的釋放時機是什么時候篱昔?

    • APP啟動之后,系統(tǒng)在主線程RunLoop里面注冊了兩個Observer始腾,其回調(diào)都是_wrapRunLoopWithAutoreleasePoolHander()州刽。
    • 第一個Observer監(jiān)聽的事件是Entry(即將進入RunLoop),其回調(diào)內(nèi)會調(diào)用_objc_autoreleasePoolPush()創(chuàng)建自動釋放池窘茁。其order-2147183647(優(yōu)先級最高)怀伦,保證創(chuàng)建自動釋放池發(fā)生在其他所有回調(diào)之前。
    • 第二個Observer監(jiān)聽兩個事件:
      1山林、BeforWaiting(準備進入休眠)房待,這個時候調(diào)用_objc_autoreleasePoolPop() & _objc_autoreleasePoolPush(),釋放舊池并創(chuàng)建新池
      2驼抹、Exit(即將推出Loop)桑孩,這個時候調(diào)用_objc_autoreleasePoolPop()釋放自動釋放池。這個Observerorder2147483647(優(yōu)先級最低)框冀,保證其釋放池子的操作發(fā)生在其他所有回調(diào)之后流椒。
  • 面試題6:thread 和 AutoreleasePool的關系

    • 每個線程(包括主線程在內(nèi)),都維護了自己的自動釋放池堆棧結構明也。
    • 新的自動釋放池在被創(chuàng)建的時候宣虾,會被添加到棧頂;當自動釋放池銷毀的時候温数,會從中移除绣硝。
    • 對于當前線程來說,會將自動釋放池對象放入自動釋放池的棧頂撑刺;在線程停止的時候鹉胖,會自動釋放掉與該線程關聯(lián)的所有自動釋放池

每個線程都有與之關聯(lián)的自動釋放池堆棧結構够傍,新的pool在創(chuàng)建的時候回被壓棧到棧頂甫菠;pool銷毀的時候,會被出棧冕屯。
對于當前線程來說寂诱,釋放對象唄壓棧到棧頂,線程停止時愕撰,會自動釋放與之關聯(lián)的自動釋放池刹衫。

  • 面試題7:RunLoop 和 AutoreleasePool的關系
    • 主程序的RunLoop在每次事件循環(huán)之前醋寝,會自動創(chuàng)建一個autoreleasepool
    • 事件循環(huán)結束的時候带迟,執(zhí)行drain操作音羞,釋放其中的對象。

參考資料
AutoReleasePool & NSRunLoop 底層分析
自動釋放池的前世今生 ---- 深入解析 autoreleasepool
斷言(assert)的用法
聊聊偽指針 Tagged Pointer

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末仓犬,一起剝皮案震驚了整個濱河市嗅绰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌搀继,老刑警劉巖窘面,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異叽躯,居然都是意外死亡财边,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門点骑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酣难,“玉大人,你說我怎么就攤上這事黑滴『┠迹” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵袁辈,是天一觀的道長菜谣。 經(jīng)常有香客問我,道長晚缩,這世上最難降的妖魔是什么尾膊? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮荞彼,結果婚禮上眯停,老公的妹妹穿的比我還像新娘。我一直安慰自己卿泽,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布滋觉。 她就那樣靜靜地躺著签夭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪椎侠。 梳的紋絲不亂的頭發(fā)上第租,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音我纪,去河邊找鬼慎宾。 笑死丐吓,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的趟据。 我是一名探鬼主播券犁,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼汹碱!你這毒婦竟也來了粘衬?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤咳促,失蹤者是張志新(化名)和其女友劉穎稚新,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體跪腹,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡褂删,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了冲茸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屯阀。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖噪裕,靈堂內(nèi)的尸體忽然破棺而出蹲盘,到底是詐尸還是另有隱情,我是刑警寧澤膳音,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布召衔,位于F島的核電站,受9級特大地震影響祭陷,放射性物質(zhì)發(fā)生泄漏苍凛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一兵志、第九天 我趴在偏房一處隱蔽的房頂上張望醇蝴。 院中可真熱鬧,春花似錦想罕、人聲如沸悠栓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽惭适。三九已至,卻和暖如春楼镐,著一層夾襖步出監(jiān)牢的瞬間癞志,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工框产, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留凄杯,地道東北人错洁。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像戒突,于是被迫代替她去往敵國和親屯碴。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容