ios內(nèi)存管理-autoreleasepool

研究@autoreleasepool之前,我們先來看下他的基本構(gòu)成和實(shí)現(xiàn)。
main.m文件中添加如下代碼:

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

clang編譯后:

int main(int argc, char * argv[]){
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

    }
    return 1;
}

全局搜索__AtAutoreleasePool

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

在main.cpp文件中有一個(gè)__AtAutoreleasePool結(jié)構(gòu)體,里面包含了構(gòu)造函數(shù)和和析構(gòu)函數(shù)。
下面我們根據(jù)objc源碼看下具體實(shí)現(xiàn)。

一、autoreleasePool數(shù)據(jù)結(jié)構(gòu)

通過objc_autoreleasePoolPush方法,我們會發(fā)現(xiàn)一個(gè)結(jié)構(gòu)體AutoreleasePoolPage,在這里有一個(gè)注釋,很重要:

Autorelease pool implementation
    // 先進(jìn)后出
   A thread's autorelease pool is a stack of pointers. 
   Each pointer is either an object to release, or POOL_BOUNDARY which is 
     an autorelease pool boundary.
   A pool token is a pointer to the POOL_BOUNDARY for that pool. When 
     the pool is popped, every object hotter than the sentinel is released.
   The stack is divided into a doubly-linked list of pages. Pages are added 
     and deleted as necessary. 
   Thread-local storage points to the hot page, where newly autoreleased 
     objects are stored. 

AutoreleasePoolPageData屬性的基本構(gòu)成:

class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
    magic_t const magic; // 16
    __unsafe_unretained id *next; //8
    pthread_t const thread; // 8
    AutoreleasePoolPage * const parent; //8
    AutoreleasePoolPage *child; //8
    uint32_t const depth; // 4
    uint32_t hiwat; // 4

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

struct magic_t {
    static const uint32_t M0 = 0xA1A1A1A1;
#   define M1 "AUTORELEASE!"
    static const size_t M1_len = 12;
    uint32_t m[4]; //4*4
……
}
  • magic 檢查校驗(yàn)完整性的變量
  • next 指向新加入的autorelease對象下一個(gè)位置
  • thread page當(dāng)前所在的線程
  • parent 父節(jié)點(diǎn) 指向前一個(gè)page
  • child 子節(jié)點(diǎn) 指向下一個(gè)page
  • depth 鏈表的深度,節(jié)點(diǎn)個(gè)數(shù)
  • hiwat high water mark 數(shù)據(jù)容納的一個(gè)上限

其他相關(guān)屬性:

  • EMPTY_POOL_PLACEHOLDER 空池占位
  • POOL_BOUNDARY 是一個(gè)邊界對象 nil,之前的源代碼變量名是 * POOL_SENTINEL哨兵對象,用來區(qū)別每個(gè)page即每個(gè) AutoreleasePoolPage 邊界
  • PAGE_MAX_SIZE =4096, 為什么呢?其實(shí)就是虛擬內(nèi)存每個(gè)扇區(qū)4096個(gè)字節(jié),也就是4K對齊。

關(guān)于構(gòu)造函數(shù)AutoreleasePoolPageData,第一個(gè)next是什么呢,跳轉(zhuǎn)后定位到下面:

id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }

lldb調(diào)試,打印發(fā)現(xiàn) sizeof(*this) = 56,也就是一個(gè)AutoreleasePoolPageData的大小,這個(gè)在前面已經(jīng)標(biāo)注。
注意一點(diǎn):結(jié)構(gòu)體指針為8,結(jié)構(gòu)體的大小要根據(jù)內(nèi)部屬性計(jì)算,static修飾的變量空間不在結(jié)構(gòu)體內(nèi)。
所以,page從56字節(jié)的位置開始裝對象,因此一頁可以裝的數(shù)量:(4096-56)/8 = 505,下面來驗(yàn)證一下:

extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {(int i=0; i<1+504 +505; i++) {
            NSObject *objc = [[NSObject alloc] autorelease];
        }
        _objc_autoreleasePoolPrint();
        
        sleep(3);//防止打印沒結(jié)束,當(dāng)前線程以及結(jié)束
    }
    return 0;
}

注意:第一個(gè)頁數(shù)據(jù)504+1,這個(gè)1是邊界即哨兵,比較特殊。具體可以參考_objc_autoreleasePoolPrint實(shí)現(xiàn)。

二、push

static inline id *autoreleaseFast(id obj)
    {
       AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            //當(dāng)前page存在,且不滿,則add
            return page->add(obj);
        } else if (page) {
            //當(dāng)前page存在,且滿
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }

添加對象:

id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }
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.
        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }
static __attribute__((noinline)) id *autoreleaseNoPage(id obj)
{
    assert(!hotPage());
    bool pushExtraBoundary = false;
    if (haveEmptyPoolPlaceholder()) { //是否存在未使用的空池
        pushExtraBoundary = true;
    } else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
        //obj 不是哨兵對象時(shí), 說明上一次的 push() 沒有成功, 如果打開了丟失 pools 的調(diào)試
        _objc_inform(...); //輸出一些調(diào)試信息
        objc_autoreleaseNoPool(obj); //這個(gè)函數(shù)沒有找到源碼
        return nil;
    } else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        //如果 obj 是哨兵對象, 并且沒有開啟 pool 內(nèi)存申請的調(diào)試
        return setEmptyPoolPlaceholder(); //將本自動釋放池標(biāo)記為未使用的空池
    }
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); //創(chuàng)建一個(gè) page 節(jié)點(diǎn), 這里會調(diào)用構(gòu)造函數(shù)
    setHotPage(page); //將該節(jié)點(diǎn)設(shè)置為 hotPage
    if (pushExtraBoundary) { //如果本自動釋放池是一個(gè)未使用的空池
        page->add(POOL_BOUNDARY); //加入哨兵對象
    }
    return page->add(obj); //將 obj 加入自動釋放池
}
static inline bool haveEmptyPoolPlaceholder()
{
     id *tls = (id *)tls_get_direct(key); //取出本線程對應(yīng)的 hotPage
     return (tls == EMPTY_POOL_PLACEHOLDER); //如果為空池標(biāo)識符則返回 true
}

pop 源碼:

static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) { //如果 token 為空池標(biāo)志
        if (hotPage()) { //如果有 hotPage, 即池非空
            pop(coldPage()->begin()); //將整個(gè)自動釋放池銷毀
        } else {
            setHotPage(nil); //沒有 hotPage, 即為空池, 設(shè)置 hotPage 為 nil
        }
        return;
    }
    page = pageForPointer(token); //根據(jù) token 找到所在的 節(jié)點(diǎn)
    stop = (id *)token; //token 轉(zhuǎn)換給 stop
    if (*stop != POOL_BOUNDARY) { //如果 stop 中存儲的不是哨兵節(jié)點(diǎn)
        if (stop == page->begin()  &&  !page->parent) {
            //存在自動釋放池的第一個(gè)節(jié)點(diǎn)存儲的第一個(gè)對象不是哨兵對象的情況, 有兩種情況導(dǎo)致:
            //1. 頂層池唄是否, 但留下了第一個(gè)節(jié)點(diǎn)(有待深挖)
            //2. 沒有自動釋放池的 autorelease 對象(有待深挖)
        } else {
            //非自動釋放池的第一個(gè)節(jié)點(diǎn), stop 存儲的也不是哨兵對象的情況
            return badPop(token); //調(diào)用錯(cuò)誤情況下的 badPop()
        }
    }
    return popPage<false>(token, page, stop);
}

static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop){
    page->releaseUntil(stop); //將自動釋放池中 stop 地址之后的所有對象釋放掉
    if (...) {
        //這一段代碼都是調(diào)試用代碼
    } else if (page->child) { //如果 page 有 child 節(jié)點(diǎn)
        if (page->lessThanHalfFull()) { //如果 page 已占用空間少于一半
            page->child->kill(); //kill 掉 page 的 child 節(jié)點(diǎn)
        } else if (page->child->child) { //如果 page 的占用空間已經(jīng)大于一半, 并且 page 的 child 節(jié)點(diǎn)有 child 節(jié)點(diǎn)
            page->child->child->kill(); //kill 掉 child 節(jié)點(diǎn)的 child 節(jié)點(diǎn)
        }
    }
}

void releaseUntil(id *stop)
{
    // 釋放當(dāng)前page中的autorelease對象
    while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage();

        // 如果當(dāng)前page都釋放完了還沒遇到POOL_BOUNDARY,就繼續(xù)釋放上一個(gè)page的
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        // 依次取出autorelease對象
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        // 釋放autorelease對象
        if (obj != POOL_BOUNDARY) {
            objc_release(obj);
        }
    }

    setHotPage(this);
}

三、嵌套

int main(int argc, char * argv[]){
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] autorelease];
        @autoreleasepool {
            NSObject *obj = [[NSObject alloc] autorelease];
            @autoreleasepool {
                NSObject *obj = [[NSObject alloc] autorelease];
                _objc_autoreleasePoolPrint();
            }
        }
    }
    
    while (1) {
        
    }
}

打印結(jié)果:

objc[15807]: ##############
objc[15807]: AUTORELEASE POOLS for thread 0x1199565c0
objc[15807]: 7 releases pending.
objc[15807]: [0x7fa191006000]  ................  PAGE  (hot) (cold)
objc[15807]: [0x7fa191006038]    0x6000016040a0  NSObject
objc[15807]: [0x7fa191006040]  ################  POOL 0x7fa191006040
objc[15807]: [0x7fa191006048]    0x600001608190  NSObject
objc[15807]: [0x7fa191006050]  ################  POOL 0x7fa191006050
objc[15807]: [0x7fa191006058]    0x600001608140  NSObject
objc[15807]: [0x7fa191006060]  ################  POOL 0x7fa191006060
objc[15807]: [0x7fa191006068]    0x6000016081b0  NSObject
objc[15807]: ##############

push函數(shù)會把POOL_BOUNDARY存儲到AutoreleasePoolPage對象中,用來標(biāo)記當(dāng)前@autoreleasepool的邊界。一個(gè){}對應(yīng)一個(gè)POOL_BOUNDARY,pop時(shí),會依次釋放對象,直到遇到POOL_BOUNDARY

四、宏定義解釋

EMPTY_POOL_PLACEHOLDER

根據(jù)注釋以及代碼分析, 可以大致得出這個(gè)宏定義用作 pool 中沒有 add 入對象時(shí)的標(biāo)記。當(dāng)一個(gè)自動釋放池被創(chuàng)建但是沒有加入任何 Autorelease 對象時(shí), 會讓這個(gè)自動釋放池的句柄等于 EMPTY_POOL_PLACEHOLDER, 并不為其分配內(nèi)存。
注釋說在頂層(即 libdispatch)進(jìn)行 pool 的 push 和 pop 但并沒有使用這個(gè)池子的時(shí)候, 能節(jié)省空間, OC類中有許多自帶 block 的方法, 比如 UIView 的 animation 系列方法, 和 NSArray 以及 NSDictionary 的 enumerat 系列方法中, 自帶的 block 會內(nèi)嵌 AutoreleasePool:

[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        //這里的代碼已經(jīng)處于內(nèi)嵌的 @autoreleasepool {} 中了
    }];

驗(yàn)證:

    //1
    __block __weak NSArray * weakArray;
    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @autoreleasepool {
            NSArray * array = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%@", self], nil];
            weakArray = array;
        }
    });
    NSLog(@"%@", weakArray);
    //2
    [UIView animateWithDuration:1. animations:^{
        NSArray * array = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%@", self], nil];
        weakArray = array;
    } completion:^(BOOL finished) {
        NSLog(@"%@", weakArray);
    }];

以上兩段代碼輸出都為 null, 但是如果將第一段代碼中的 @autoreleasepool 刪除, weakArray 將輸出有值, 證明第二段代碼的 block 中內(nèi)嵌了 @autoreleasepool。
如果內(nèi)嵌了@autoreleasepool 的代碼塊中沒有使用到 Autorelease 對象, 卻為池子分配了節(jié)點(diǎn), 無疑是對內(nèi)存的浪費(fèi)。
EMPTY_POOL_PLACEHOLDER 即是為了解決這種情況下發(fā)生的內(nèi)存浪費(fèi)而存在的。

POOL_BOUNDARY

POOL_BOUNDARY 會在建立新的自動釋放池時(shí)作為第一個(gè)對象加入到池中, 被稱為哨兵對象, 哨兵對象是自動釋放池中非常巧妙而且重要的一環(huán)。
我們已經(jīng)知道 @autoreleasepool {}是在作用域的開始使用 push()方法來創(chuàng)建自動釋放池, 在作用域結(jié)束時(shí), 使用 pop() 方法來銷毀自動釋放池。
在嵌套結(jié)構(gòu)中 push() 方法不一定會創(chuàng)建新的 page 節(jié)點(diǎn), 如果當(dāng)前節(jié)點(diǎn)未滿則會直接插入一個(gè)哨兵對象, 如果當(dāng)前節(jié)點(diǎn)已滿則創(chuàng)建一個(gè)新的 page 節(jié)點(diǎn)并且插入一個(gè)哨兵對象, push() 函數(shù)的返回值就是這個(gè)哨兵對象的地址(哨兵對象的值是 nil, 但哨兵對象的地址不為 nil), 然后在 pop() 方法調(diào)用時(shí), 傳入這個(gè)哨兵對象的地址, 對這個(gè)地址之后的 autorelease 對象發(fā)送 release 方法。

五、autoreleasepool、runloop和多線程的關(guān)系

5.1 autoreleasepool和runloop

  1. RunLoop和線程的一一對應(yīng)的,對應(yīng)的方式是以key-value的方式保存在一個(gè)全局字典中
  2. 主線程的RunLoop會在初始化全局字典時(shí)創(chuàng)建
  3. 子線程的RunLoop會在第一次獲取的時(shí)候創(chuàng)建,如果不獲取的話就一直不會被創(chuàng)建
  4. RunLoop會在線程銷毀時(shí)銷毀

系統(tǒng)在主線程的runloop中注冊了2個(gè)observer:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@", [NSRunLoop mainRunLoop]);
}

// 部分打印
observers = (
    "<CFRunLoopObserver 0x600001330320 [0x7fff80617cb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), 
      context = <CFArray 0x600002c607e0 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f9c54802048>\n)}}",
    "<CFRunLoopObserver 0x6000013303c0 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), 
      context = <CFArray 0x600002c607e0 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f9c54802048>\n)}}"
)

關(guān)于CFRunLoopActivity:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),         // 1,進(jìn)入runloop
    kCFRunLoopBeforeTimers = (1UL << 1),  // 2,即將處理timers
    kCFRunLoopBeforeSources = (1UL << 2), // 4,即將處理sources
    kCFRunLoopBeforeWaiting = (1UL << 5), // 32,即將休眠
    kCFRunLoopAfterWaiting = (1UL << 6),  // 64,剛被喚醒
    kCFRunLoopExit = (1UL << 7),          // 128,退出runloop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

第一個(gè)Observer:activities為0x1(十進(jìn)制為1),它用來監(jiān)聽kCFRunLoopEntry的。當(dāng)監(jiān)聽到kCFRunLoopEntry時(shí),會調(diào)用objc_autoreleasePoolPush函數(shù)。
第二個(gè)Observer:activities為0xa0(十進(jìn)制為160 = 32 + 128),它用來監(jiān)聽kCFRunLoopBeforeWaitingkCFRunLoopExit。當(dāng)監(jiān)聽到kCFRunLoopBeforeWaiting時(shí),會先調(diào)用objc_autoreleasePoolPop函數(shù),再調(diào)用objc_autoreleasePoolPush函數(shù)。當(dāng)監(jiān)聽到kCFRunLoopExit時(shí),會調(diào)用objc_autoreleasePoolPop函數(shù)

autorelease對象在當(dāng)前RunLoop休眠之前釋放。局部對象在方法結(jié)束時(shí)釋放,因?yàn)锳RC生成的是release,而不是autorelease。

5.2 autoreleasepool和多線程

主線程我們前面已經(jīng)討論過,主線程默認(rèn)為我們開啟 Runloop,Runloop 會自動幫我們創(chuàng)建Autoreleasepool,并進(jìn)行Push、Pop 等操作來進(jìn)行內(nèi)存管理。
在子線程你創(chuàng)建了 Pool 的話,產(chǎn)生的 Autorelease 對象就會交給 pool 去管理。如果你沒有創(chuàng)建 Pool ,但是產(chǎn)生了 Autorelease 對象,就會調(diào)用 autoreleaseNoPage 方法。在這個(gè)方法中,會自動幫你創(chuàng)建一個(gè) hotpage,并調(diào)用page->add(obj)將對象添加到 AutoreleasePoolPage 的棧中,也就是說你不進(jìn)行手動的內(nèi)存管理,也不會內(nèi)存泄漏啦!
如果沒有pool管理(沒有pop),在線程退出時(shí),也會釋放資源,調(diào)用tls_dealloc方法,清空對象以及AutoreleasePoolPage。

參考:
iOS 各個(gè)線程 Autorelease 對象的內(nèi)存管理
探索子線程autorelease對象的釋放時(shí)機(jī)
子線程AutoRelease對象何時(shí)釋放

最后編輯于
?著作權(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ù)。

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