iOS 內存管理 部分四

主要講解Autoreleasepool的原理;

本文中的測試代碼主要為 MRC 環(huán)境;
文中使用的 runtime源碼是objc4-781版本;

iOS 內存管理 部分一
iOS 內存管理 部分二
iOS 內存管理 部分三
iOS 內存管理 部分四


1. 什么是 Autoreleasepool?

從字面意思即可得知是自動釋放池, 其作用就是在@autoreleasepool結束的時候會將其中的對象進行回收,釋放內存空間;
它有如下特點;

    1. Autoreleasepool的底層也是一個結構體, 其存儲結構類似棧,先入的autorelease對象后被釋放;編譯器會將@autoreleasepool{autorelease對象}變?yōu)橐韵麓a
      struct{
      /*
       push 函數(shù)入參POOL_BOUNDARY返回一個地址, 這個地址是起始位置, 
       從這個開始, 下一個位置開始存放 autorelease 對象, 
      */
      void *ctx = objc_autoreleasePoolPush();
         push進autorelease對象
         push進autorelease對象
         push進autorelease 對象
      objc_autoreleasePoolPop(ctx);
      }
      
    1. @autoreleasepool是由一個一個的autoreleasepoolpage組成, 所有的autoreleasepoolpage通過雙向鏈表連接在一次, 每個autoreleasepoolpage占用4096個字節(jié);除了自身成員變量所占用空間, 其余的都是用來存放autorelease對象(MRC環(huán)境);
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;
};
magic用來校驗AutoreleasePoolPage結構是否完整;
next指向當前棧頂?shù)谝粋€可用的地址;
thread指向當前的線程;
parent雙向鏈表中指向父指針;
child雙向鏈表中指向子指針;

通過雙向鏈表將多個autoreleasepoolpage連接起來;

3. Autoreleasepool的流程;

我們知道Autoreleasepool底層是一個或者多個AutoreleasepoolPage通過childparent連接起來的雙向鏈表結構, 這就類似棧的結構, 后面加入的總能最先被使用或者釋放, 因此如果有多個AutoreleasepoolPage時, 肯定是前面的AutoreleasepoolPage空間被用完了, 而鏈表尾部AutoreleasepoolPage空間未滿仍可以使用, 從源碼中也可以看到hotPage就是鏈表尾部的AutoreleasepoolPage, 而 coldPage則是棧底的AutoreleasepoolPage;因此我們可以整理Autoreleasepool使用時的執(zhí)行流程如下

  • 3.1 首先Autoreleasepool底層轉化為結構體, 實際上一個一個AutoreleasePage 雙向鏈表結構連接起來;
  • 3.2 執(zhí)行Autoreleasepool代碼時, 也就是執(zhí)行AutoreleasepoolPagepush操作時, 首先 判斷pool中是否有Autorelease對象, 如果沒有則創(chuàng)建一個占位 pool并返回結束;
  • 3.3 如果有Autorelease對象, 判斷是否有 hotPage;

    a. 如果有hotPage則判斷是否夠用, 不夠用則創(chuàng)建新的 page并通過childparent指針連接, 同時設置hotPage;
    b. 如果沒有hotPage則創(chuàng)建新的 page并設置為hotPage;

  • 3.4 在往AutoreleasePage(hotPage) 添加Autorelease對象時,實際執(zhí)行兩個步驟;

    a. 如果是第一次添加Autorelease對象, 首先往hotPage中添加POOL_BOUNDARY指針并返回它地址, 同時next指針向后移動; 如果不是第一次添加則字節(jié)執(zhí)行下面步驟;
    b. 往hotPage中添加Autorelease對象指針, 同時next指針向后移動;

  • 3.5 在Autoreleasepool代碼結束時, 也就是后面大括號時, 開始執(zhí)行AutoreleasepoolPagepop操作時, 入參3.4.a 返回的POOL_BOUNDARY地址, 作為標志(也就是大家說的哨兵對象地址)
    然后開始從hotPage通過while循環(huán)開始釋放Autorelease對象, 如果hotPage釋放完畢仍然未到達標志, 則通過parent指針向上查找, 繼續(xù)釋放; 釋放的同時next--跟著移動和重新設置hotPage;
    釋放到達POOL_BOUNDARY 標志位時結束, 同時釋放childPage的空間;

備 :抽時間整理流程圖;

補充 Autoreleasepool 的底層結構和源碼大致邏輯

1.Autoreleasepool的底層結構

通過clang語句將下面代碼轉轉后

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        Model *model = [[Model alloc] init];
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

可以得到Autoreleasepool其底層結構如下,

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
        Model *model = ((Model *(*)(id, SEL))(void *)objc_msgSend)((id)((Model *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Model"), sel_registerName("alloc")), sel_registerName("init"));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);

===>
#autoreleasepool 轉化為了__AtAutoreleasePool, 直接在.cpp 中搜索可以得知
struct __AtAutoreleasePool {
      ///構造函數(shù), 在結構體創(chuàng)建的時候調用
  __AtAutoreleasePool() {
        atautoreleasepoolobj = objc_autoreleasePoolPush();
     }
    ///析構函數(shù), 在結構體銷毀的時候調用;
  ~__AtAutoreleasePool() {
        objc_autoreleasePoolPop(atautoreleasepoolobj);
      }
  void * atautoreleasepoolobj;
};

===>
#去 objc4-781版本源碼中查詢objc_autoreleasePoolPush的結構如下, 
# 注意它是返回一個指針地址, 這個地址是POOL_BOUNDARY 的地址;
# 當在執(zhí)行 pop 操作時入參這個地址, 表示釋放到這個位置為止
void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

===>
#AutoreleasePoolPage的結構如下(繼承自AutoreleasePoolPageData)
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{        
         ///驗證AutoreleasePoolPageData 的完整性
    magic_t const magic;
        ///指向當前AutoreleasePoolPageData棧頂?shù)目捎每臻g的指針
    __unsafe_unretained id *next;
        ///當前線程
    pthread_t const thread;
         ///雙向鏈表結構, 指向前面AutoreleasePoolPageData
    AutoreleasePoolPage * const parent;
        ///雙向鏈表結構, 指向后面AutoreleasePoolPageData
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
        ///構造函數(shù), 注意 child 默認傳入的是空, parent指向前面的 page;
    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)
    {
    }
};
2.Autoreleasepoolpage的的 push 邏輯

我們接著上面的源碼繼續(xù)看Autoreleasepoolpagepush過程
首先看下一個重要的參數(shù) POOL_BOUNDARY他的定義如下

 #   define POOL_BOUNDARY nil

我們知道Autoreleasepoolpage中的next指向當前棧頂?shù)目捎玫刂? 下面以如下代碼來講解下POOL_BOUNDARYnext的協(xié)調作用

    @autoreleasepool {
        for (int i = 0; i < 5;  i++) {
            Model *model = [[Model alloc] init];
        }
    }

假設剛開始的時候POOL_BOUNDARY = nilnext指向當前棧頂?shù)目捎梦恢玫刂? 當我們向
Autoreleasepoolpage添加變量時, 這時POOL_BOUNDARY變?yōu)樵?next位置, 同時next向棧頂移動;

#下面看下Autoreleasepoolpage的 push函數(shù)
static inline void *push()   {
        id *dest;
        if (slowpath(DebugPoolAllocation)) {
            // Each autorelease pool starts on a new pool page.
            /*
          特殊情況: 調試模式下每一個 autoreleapool 都會創(chuàng)建新的 page; 
          判斷條件為DebugPoolAllocation, 請點擊查看其成立條件;
          正常使用autoreleapool不會走此流程, 正常情況下即使是多個 autoreleapool嵌套, 
          他們也公用 page, 通過各自 的 POOL_BOUNDARY 來作為邊界;
            */
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            ///正常情況下autoreleapool 的push邏輯
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

===>

# 看下autoreleaseFast(POOL_BOUNDARY)的執(zhí)行邏輯;
    static inline id *autoreleaseFast(id obj)   {
         /*
        注意這個名字 hotPage, hot說明是正在使用的 page, 例如, 如果有多個AutoreleasePoolPage
        那么一定是前面的AutoreleasePoolPage已經存儲滿了, 所以使用最后面的AutoreleasePoolPage;  
        這個空間尚未使用滿的AutoreleasePoolPage稱為hotPage; 注意它的值可能是 nil, 
        因為如果AutoreleasePool 中并沒有autoreleas 對象時, 
        并不會創(chuàng)建AutoreleasePoolPage, 這時 hotPage = nil;
        */
        AutoreleasePoolPage *page = hotPage();
        ///如果當前page 存在且沒有使用完則執(zhí)行add操作
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            ///如果當前 page 已經使用完畢. 則執(zhí)行autoreleaseFullPage操作
            return autoreleaseFullPage(obj, page);
        } else {
            ///當前 page 不存在, 執(zhí)行autoreleaseNoPage; 特殊情況不深入探究
            return autoreleaseNoPage(obj);
        }
    }

===>
# 下面看下 沒有AutoreleasePoolPage時的添加邏輯:
 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
        ASSERT(!hotPage());
         ///注意這個標志, 是否需要 PUSH 進去額外的 POOL_BOUNDARY 對象
        bool pushExtraBoundary = false;
        /*
        第一次執(zhí)行 AutoreleasePoolPage::push 操作時, 此時haveEmptyPoolPlaceholder() == false, 所以條件不滿足;
        從第二次開始, 這個條件就滿足了, 會將pushExtraBoundary = true;
        */
        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;
        }
        ///特殊情況, 向空的 pool 中添加元素, 直接返回 nil
        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;
        }
        /*
            注意這個判斷條件:  當?shù)谝淮蜛utoreleasePoolPage::push時入參POOL_BOUNDARY滿足, 
            調用setEmptyPoolPlaceholder(), 然后haveEmptyPoolPlaceholder() == true;
            如果嵌套AutoreleasePool時, 再次調用AutoreleasePoolPage::push時, 如果沒有實際
            添加autorelease 對象, 則此條件仍然滿足, 因為入參仍然是 POOL_BOUNDARY;
            只有實際添加autorelease對象入棧時此條件才不滿足;
        */
        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.
            ///返回的一個占位 pool, 實際上此時還沒有開辟AutoreleasePoolPage;
            return setEmptyPoolPlaceholder();
        }

        // We are pushing an object or a non-placeholder'd pool.
        // Install the first page.
        ///實際上第一次添加autorelease對象到AutoreleasePool 時正式創(chuàng)建AutoreleasePoolPage;
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        ///因為pushExtraBoundary == true, 所以實際上第一次添加autorelease對象 時, 是先添加POOL_BOUNDARY指針;
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        // Push the requested object or pool.
        ///正式添加autorelease對象
        return page->add(obj);
    }

===>
#實際的 add 操作
    id *add(id obj)    {
        ASSERT(!full());
        unprotect();
        ///首先將 next 指針記錄下來
        id *ret = next;  // faster than `return next-1` because of aliasing
        /*
        這行語句分解開等價于  *next = obj;  *next ++;
        因為根據(jù)運算符的結合性,  從右向左結合, 先執(zhí)行=運算符;
        執(zhí)行后把POOL_BOUNDAY 或者 autorelease 對象放入 next 原本位置;
        然后 next 的指向向后移動;
        */
        *next++ = obj;
        protect();
        return ret;
    }

===>
/*
如果 hotPage 滿了的時候 需要向后移動查找尚未;
調用autoreleaseFullPage(obj, page); 注意入參, 除了 obj(POOL_BOUNDARY)
 還有一個參數(shù)是 page, 作用是需要把 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);
        ///如果 child 指針指向的 page 存在則設置為 hotPage, 否則創(chuàng)建新的設置為 hotPage
        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());
        ///設置當前HotPage
        setHotPage(page);
        ///當 page 創(chuàng)建成功后, 再執(zhí)行新的添加操作;
        return page->add(obj);
    }

不考慮占位 autoreleasepool 時的添加過程

3.Autoreleasepoolpage的的 pop 邏輯
 static inline void
    pop(void *token)
    {
        AutoreleasePoolPage *page;
        id *stop;
        /*
        根據(jù)AutoreleasePoolPage的 push 我們知道它返回的是結構體 的地址;
        首先判斷是否最后一個是不是空的占位 pool, 如果是占位 pool 則執(zhí)行;
        如果多個AutoreleasePool嵌套(或者一個 pool), 最內層沒有實際Autorelease的對象, 
         那么它一定是一個占位 pool, 實際上沒有開辟AutoreleasePoolPage;
        */
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            page = hotPage();
            ///如果沒有 hotPage , 說明沒有AutoreleasePoolPage, 只有占位 pool;只需要將占位pool 置 nil 即可;
            if (!page) {
                // Pool was never used. Clear the placeholder.
                //看這句話翻譯, pool 沒有使用(就是沒有添加Autorelease對象), 清理占位 pool;
                return setHotPage(nil);
            }
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            /*
            注意官方注釋: pool 被使用了, 那就正常的 pop;將 page 指針指向 coldpage;
            關于 coldpage 的定義, 請自行去看下就行, 實際上就是指向棧底的那個 AutoreleasePoolPage;
            然后將 token 指針指實際的 page(去掉占位 pool ) 的 begin();
            */
            page = coldPage();
            token = page->begin();
        } else {
            /*
          走到這里說明所有的 page 都是被使用狀態(tài), 沒有占位 pool;
           調用pageForPointer()函數(shù)獲取入參 page 數(shù)據(jù)信息;
            */
            page = pageForPointer(token);
        }
        /*
        設置 stop 為 token ; 
        如果鏈表尾部是實際的 page:
            這個 token 的值是鏈表尾部 page 的POOL_BOUNDAY 的地址;
        如果鏈表尾部是占位的 pool:
            這個 token 的值是去掉占位 pool 后鏈表尾部 page 的POOL_BOUNDAY 的地址;
         */
        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.
                 /*
                 經過上面判斷, stop 應該指向入參 page 的 begin()地址;
                page 指向入參的 page, 所以它的 parent 指向應該是空;
                出現(xiàn)錯誤,執(zhí)行 badPop(), 請自行查看, 文中不再貼出具體邏輯;
                */
                return badPop(token);
            }
        }
        ///特殊情況, 調試模式(每個 pool 都會創(chuàng)建新的 page)不再深入探究, 請自行查看;
        if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }
        ///執(zhí)行popPage 操作, 注意入參 false
        return popPage<false>(token, page, stop);
    }

 ===>
#執(zhí)行 pagePop 操作, allowDebug的值是 false
  template<bool allowDebug>
    static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
        if (allowDebug && PrintPoolHiwat) printHiwat();
        ///實際的釋放操作, autorelease 對象在這里釋放的
        page->releaseUntil(stop);

        // memory: delete empty children
        ///刪除多余的 chiildPage;
        ///特殊情況/調試模式,  不深究
        if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            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);
        /*
      正常情況, 執(zhí)行 childPage的 kill 操作
      注意由于 child 默認是空, 所以, 執(zhí)行到里面是一定是多個 page 連接的情況
        */
        } else if (page->child) {
            // hysteresis: keep one empty child if page is more than half full
            ///看上面的官方注釋: 如果當前 page 使用率超過一半, 則保留一個空 page 備用;
            if (page->lessThanHalfFull()) {
                ///當前 page 的使用率不足一半, 則只保留當前 page;
                page->child->kill();
            }
            else if (page->child->child) {
              ///當前 page 的使用率超過一半, 則只保留當前 page+下一個 chiildPage;
                page->child->child->kill();
            }
        }
    }

===>
#真正釋放 autorelease 對象的邏輯
 void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        /*
          通過static inline void    pop(void *token)函數(shù)的邏輯我們可以得知, 正常情況下stop應該
          指向的是鏈表尾部 page 的 begin()位置, 也就是其 POOL_BOUNDARY 的地址;
          所以就從鏈表尾部開始遍歷, 直到 next 指向 POOL_BOUNDARY 位置結束;
        */
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            /*
            注意這句官方注釋: 每次從 hotPage 開始釋放, 防止重復釋放autoreleased對象;
            因為是棧結構, 后加入的先釋放, 先加入的后釋放, 從鏈表尾部開始釋放;
            */
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            while (page->empty()) {
                ///如果當前 page 已經釋放完畢, 通過 parent 指針找到前面的 page,并設置為 hotPage;
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            /*
            id obj = *--page->next;根據(jù)運算符向左結合性, 這行代碼等價于
            先將 next指針執(zhí)行--, 然后將這個位置的autorelease對象給 obj;
            因為 next 指針始終指向的是棧頂可以用的位置; 所以它的前面
            存放的是POOL_BOUNDARY或者autorelease對象;
            */
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();
            ///如果 obj 不是POOL_BOUNDARY指針, 那就是 autoreleased 對象, 執(zhí)行objc_release操作;
            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }
        ///已經遍歷到入參 page, 設置入參 page 為 hotPage;
        setHotPage(this);
#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            ASSERT(page->empty());
        }
#endif
    }
4._objc_autoreleasePoolPrint()函數(shù)打印

我們通過extern void _objc_autoreleasePoolPrint(void);方式可以調用私有函數(shù)來打印autoreleasepool的信息, 如下代碼

#import "ViewController1.h"
#import "Model.h"

extern void _objc_autoreleasePoolPrint(void);
 
@interface ViewController1 ()
@end

@implementation ViewController1
///在工程設置為此文件為MRC
- (void)viewDidLoad {
    [super viewDidLoad];
    @autoreleasepool {
        Model *model = [[[Model alloc] init] autorelease];
        Model *model2 = [[[Model alloc] init] autorelease];
        @autoreleasepool {
            Model *model2 = [[[Model alloc] init] autorelease];
            _objc_autoreleasePoolPrint();
        }
     }
}
@end

可以獲得如下打印結果



參考文章和下載鏈接
文中測試代碼
objc4源碼下載地址

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容