OC内存管理-自动释放池

一、自动释放池简介

@autoreleasepool {
    NSLog(@"Hello, World!");
}

对于autoreleasepool是怎么实现的呢?直接xcrun查看下对应的c++实现:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_lm_vm92ggh11h13qgfr1gn110j00000gn_T_main_264076_mi_0);
    }
    return 0;
}

可以看到@autoreleasepool被转换成了__AtAutoreleasePool __autoreleasepool__AtAutoreleasePool定义如下:

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

__AtAutoreleasePool是一个结构体,有一个成员变量atautoreleasepoolobj,在构造函数中调用objc_autoreleasePoolPush,在析构函数中调用objc_autoreleasePoolPop
那么@autoreleasepool其实也就相当于:

objc_autoreleasePoolPush()
......
objc_autoreleasePoolPop()

当然也可以通过汇编验证:


image.png

objc_autoreleasePoolPush下符号断点:

image.png

定位到了objc_autoreleasePoolPush的源码在objc库中。

@autoreleasepool 底层实际调用了objc_autoreleasePoolPushobjc_autoreleasePoolPop

二、自动释放池的结构

objc_autoreleasePoolPushobjc_autoreleasePoolPop对应源码如下:

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

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

可以看到是属于AutoreleasePoolPage命名空间的。
AutoreleasePoolPage定义如下:

/***********************************************************************
   Autorelease pool implementation

   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. 
**********************************************************************/

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
    
private:
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const COUNT = SIZE / sizeof(id);
    static size_t const MAX_FAULTS = 2;
......
}

AutoreleasePoolPage本身有一些静态成员变量,继承自AutoreleasePoolPageData。注释说明了自动释放池是一个线程的栈指针的集合。有一个边界对象(哨兵对象)。本身是一个双向链表,并且有hotpagecoldpage

2.1 AutoreleasePoolPageData

struct AutoreleasePoolPageData
{
......
    magic_t const magic;//16 用来校验 AutoreleasePoolPage 的结构是否完整
    __unsafe_unretained id *next;//8 next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
    pthread_t const thread;//8 指向当前线程
    AutoreleasePoolPage * const parent;//8 指向父结点,第一个结点的 parent 值为 nil
    AutoreleasePoolPage *child;//8 指向子结点,最后一个结点的 child 值为 nil
    uint32_t const depth;//4 代表深度,从 0 开始,往后递增 1
    uint32_t hiwat;//4 代表 high water mark 最大入栈数量标记

    //构造函数
    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 值为 nilparentchild说明了它是一个双向链表。
  • depth:代表深度,从 0 开始,往后递增 1。也就是有多少页。
  • hiwat:代表 high water mark 最大入栈数量标记。

magic_t结构如下:

struct magic_t {
    static const uint32_t M0 = 0xA1A1A1A1;
#   define M1 "AUTORELEASE!"
    static const size_t M1_len = 12;
    uint32_t m[4];
......
}

M0M1_lenstatic变量,所以magic_t占用空间与m[4]相关,也就是16字节。整个AutoreleasePoolPageData占用的内存空间为56字节。

有如下代码:

extern void _objc_autoreleasePoolPrint(void);

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

输出:

objc[94411]: ##############
objc[94411]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[94411]: 2 releases pending.
objc[94411]: [0x10400b000]  ................  PAGE  (hot) (cold)
objc[94411]: [0x10400b038]  ################  POOL 0x10400b038
objc[94411]: [0x10400b040]       0x103366e90  NSObject
objc[94411]: ##############

可以看到直接输出了AutoreleasePoolPage中有2个对象,一个是obj另外一个是哨兵对象(POOL 0x10400b038)。

1._objc_autoreleasePoolPrintobjc源码中用于调试的函数,打印了AutoreleasePoolPage中的内容:

image.png

2.可以通过单个文件配置-fno-objc-arc或者整个项目配置Objective-C Automatic Reference CountingNO关闭arc
image.png

自动释放池是一个双向链表的结构,由多个AutoreleasePoolPage组成,AutoreleasePoolPage本身占用56个字节空间大小。

三、自动释放池的压栈与创建

3.1 push

static inline void *push() 
{
    id *dest;
    if (slowpath(DebugPoolAllocation)) {
        // Each autorelease pool starts on a new pool page.
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        //非debug调用 autoreleaseFast
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    return dest;
}

直接调动autoreleaseFast,哨兵对象POOL_BOUNDARY传递的是nil

3.2 autoreleaseFast

static inline id *autoreleaseFast(id obj)
{
    //获取 hotPage
    AutoreleasePoolPage *page = hotPage();
    //page 存在 并且没有满
    if (page && !page->full()) {
        //添加对象进入page
        return page->add(obj);
    } else if (page) {//page存在满了
        return autoreleaseFullPage(obj, page);
    } else { //page不存在创建,首次需要创建。
        return autoreleaseNoPage(obj);
    }
}
  • page存在并且没有存满的情况下调用add添加对象。
  • page存在并且存满的情况调用autoreleaseFullPage创建新的page
  • page不存在则调用autoreleaseNoPage创建page

3.3 autoreleaseNoPage

#   define POOL_BOUNDARY nil

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    ......
    // We are pushing an object or a non-placeholder'd pool.

    // Install the first page.
    //创建新的page
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    //设置page为hotPage
    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.
    //添加obj到page中
    return page->add(obj);
}
  • 调用AutoreleasePoolPage构造方法创建page
  • 设置pagehotPage
  • 添加哨兵对象,哨兵对象是一个nil
  • page中添加obj

3.3.1 AutoreleasePoolPage 创建 page

AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
    AutoreleasePoolPageData(begin(),//page存储的起始位置,也就是 _next 下一个元素的位置
                            objc_thread_self(),//线程
                            newParent,//parent,首次为nil
                            newParent ? 1+newParent->depth : 0,//depth 首次 0
                            newParent ? newParent->hiwat : 0) //hiwat 首次 0
{
    if (objc::PageCountWarning != -1) {
        checkTooMuchAutorelease();
    }

    if (parent) {//首次没有parent
        parent->check();
        ASSERT(!parent->child);
        parent->unprotect();
        //设置parent的child为自己。
        parent->child = this;
        parent->protect();
    }
    protect();
}

在其中会判断parent从而设置它的child为当前pageAutoreleasePoolPage中调用了AutoreleasePoolPageData的构造函数。传递的第一个参数为begin

image.png

  • begin初始给了next指针,偏移自身56字节。
  • thread设置为当前线程。
  • parent设置为前一个page,首次为nil
  • depthparent.depth + 1也就是加了1page,首次赋值为0,所以当前page = depth + 1
  • hiwat设置为parenthiwat,首次赋值为0

thisAutoreleasePoolPage,返回值为加56,也就是AutoreleasePoolPageData本身占用的内存空间。说明每个page都有56字节存储自身内容,然后才存储自动释放的对象:

image.png

先存储自身的成员变量,首次创建page会先存哨兵对象(nil)再存储自动释放的对象。

3.4 autoreleaseFullPage

page满的情况下调用了autoreleaseFullPage

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 {
        //找到最后一个page
        if (page->child) page = page->child;
        //创建新的page
        else page = new AutoreleasePoolPage(page);
    } while (page->full());
    //设置page为hotpage
    setHotPage(page);
    //obj添加进page
    return page->add(obj);
}
  • 找到最后一个page然后创建新的page,参数是最后一个page
  • 设置创建的pagehotpage
  • obj加入page

3.5 page->add

id *add(id obj)
{
    ASSERT(!full());
    unprotect();
    id *ret;
//多次持有逻辑的处理,count++
......
    ret = next;  // faster than `return next-1` because of aliasing
    //内存平移 存储obj 然后next指向下一个空间。
    *next++ = obj;
 done:
    protect();
    return ret;
}

核心逻辑是存储obj指针并且next指针平移。如果对象已经在自动释放池中则会进行count++(从0开始计数,实际数量需要count +1)。

四、自动释放池满页临界值

首先为什么分页呢?
如果不分页所有对象都会在一页,操作复杂,管理不便,并且需要分配一块很大的内存,如果分页则不需要连续的内存。并且开锁解锁只针对单个页面更安全。

既然有full逻辑,那么什么情况下page会添加满呢?

bool full() { 
    return next == end();
}

也就是page存储没有空间了,那么每个page占用多大内存呢?
有如下验证代码:

extern void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        for(int i = 0; i < 505; i++) {
            NSObject *obj = [[NSObject alloc] autorelease];
        }
        _objc_autoreleasePoolPrint();
    }
    return 0;
}

输出:

objc[13893]: ##############
objc[13893]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[13893]: 506 releases pending.
objc[13893]: [0x10680a000]  ................  PAGE (full)  (cold)
objc[13893]: [0x10680a038]  ################  POOL 0x10680a038
objc[13893]: [0x10680a040]       0x1010206f0  NSObject
objc[13893]: [0x10680a048]       0x101705390  NSObject
......
objc[13893]: [0x10680aff8]       0x1017072f0  NSObject
objc[13893]: [0x105009000]  ................  PAGE  (hot) 
objc[13893]: [0x105009038]       0x101707300  NSObject
objc[13893]: ##############

那也就是存储了504 + POOL_BOUNDARY一共505个对象指针,加上page本身56字节一共505 * 8 + 56 = 4K,也就是一页4K大小。接着第二页由于没有POOL_BOUNDARY那么就会存储505obj

在源码中有PAGE_MIN_SIZE的定义(112 = 4K):

image.png

  • AutoreleasePoolPage大小为4K,每一页都有本身的成员变量56kb,剩余空间存储对象指针。
  • 一个AutoreleasePoolPage只有一个哨兵对象。

五、自动释放池出栈(objc_autoreleasePoolPop)

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

ctxt值为0x1。最终调用了popPage

static inline void
pop(void *token)
{
......
    //token 也为 page开始位置,page地址   stop 为page的 begin
    return popPage(token, page, stop);
}

最终调用popPage释放page

5.1 popPage

template
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    if (allowDebug && PrintPoolHiwat) printHiwat();
    //调用对象的release,移动next指针
    page->releaseUntil(stop);

    // memory: delete empty children
    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);
    } else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {//少于一半
            page->child->kill();//释放child
        }
        else if (page->child->child) {//否则释放child-child
            page->child->child->kill();
        }
    }
}
  • 调用releaseUntil释放对象以及移动next指针。

5.1.1 releaseUntil

void releaseUntil(id *stop) 
{
    while (this->next != stop) {
        // Restart from hotPage() every time, in case -release 
        // autoreleased more objects
        //获取hotPage
        AutoreleasePoolPage *page = hotPage();

        // fixme I think this `while` can be `if`, but I can't prove it
        //page为空往前找,并且设置hotPage
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
        AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;

        // create an obj with the zeroed out top byte and release that
        id obj = (id)entry->ptr;
        int count = (int)entry->count;  // grab these before memset
#else
        id obj = *--page->next;
#endif
        //page中存储的地址设为 0xA3
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        //不为哨兵对象调用 objc_release
        if (obj != POOL_BOUNDARY) {
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
            // release count+1 times since it is count of the additional
            // autoreleases beyond the first one
            for (int i = 0; i < count + 1; i++) {
                //release
                objc_release(obj);
            }
#else
            objc_release(obj);
#endif
        }
    }
    //设置hotPage 为release后指向的page
    setHotPage(this);
......
}
  • 循环调用对象的objc_release释放对象。
  • 这里count是因为对象可能被自动释放池多次持有。

5.1.2 kill

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;
    //找到最后一个child page
    while (page->child) page = page->child;
    //循环释放page
    AutoreleasePoolPage *deathptr;
    do {
        deathptr = page;
        page = page->parent;
        if (page) {
            page->unprotect();
            //child置为nil
            page->child = nil;
            page->protect();
        }
        //释放page空间
        delete deathptr;
    } while (deathptr != this);
}
  • 根据 page 找到最后的child
  • child置空,释放page开辟的空间。

AutoreleasePoolPage根据入栈顺序先调用objc_release释放对象,最后释放page对应的内存空间。
childparent用于存储和释放过程中page的查找与操作。

六、自动释放池扩展

6.1 自动释放池嵌套

既然自动释放池会开辟page存储自动释放的对象,并且自动释放池与线程时对应的关系,那么如果自动释放池嵌套的情况是如何存储的呢?

@autoreleasepool {
    NSObject *obj = [[NSObject alloc] autorelease];
    @autoreleasepool {
        NSObject *obj1 = [[NSObject alloc] autorelease];
        //输出全部
        _objc_autoreleasePoolPrint();
    }
    //只输出外层的
    _objc_autoreleasePoolPrint();
}

输出:

objc[86251]: ##############
objc[86251]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[86251]: 4 releases pending.
objc[86251]: [0x104810000]  ................  PAGE  (hot) (cold)
objc[86251]: [0x104810038]  ################  POOL 0x104810038
objc[86251]: [0x104810040]       0x10371cde0  NSObject
objc[86251]: [0x104810048]  ################  POOL 0x104810048
objc[86251]: [0x104810050]       0x1037119d0  NSObject
objc[86251]: ##############

可以看到自动释放池在page中存储是按顺序存储的。

  • 1page可能会对应多个自动释放池,1个自动释放池也可能存储在多个page中。
  • 1个自动释放池对应1个哨兵对象。

6.2 autorelease 验证

既然在MRC下主动调用autorelease的对象会加入自动释放池,那么在ARC下什么情况下会被加入自动释放池?

@autoreleasepool {
    HPObject *hpObj = [HPObject alloc];
    NSLog(@"hpObj: %@",hpObj);

    __autoreleasing  HPObject *hpObj1 = [HPObject alloc];
    NSLog(@"hpObj1: %@",hpObj1);

    HPObject *hpObj2 = [HPObject object];
    NSLog(@"hpObj2: %@",hpObj2);

    _objc_autoreleasePoolPrint();
}

输出:

hpObj: 
hpObj1: 
hpObj2: 
objc[96721]: ##############
objc[96721]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[96721]: 3 releases pending.
objc[96721]: [0x101016000]  ................  PAGE  (hot) (cold)
objc[96721]: [0x101016038]  ################  POOL 0x101016038
objc[96721]: [0x101016040]       0x10331ed10  HPObject
objc[96721]: [0x101016048]       0x10331ffe0  HPObject
objc[96721]: ##############

可以看到hpObj没有加入自动释放池。__autoreleasing修饰的对象会加入自动释放池。
那么这块是怎么区分的呢?
allocnewcopymutableCopy开头命名的方法返回的对象不会被加入自动释放池,其余方法返回的对象会被加入自动释放池。

@autoreleasepool {
    HPObject *allocObj = [HPObject allocObject];
    NSLog(@"allocObj: %@",allocObj);
    
    HPObject *newObj = [HPObject newObject];
    NSLog(@"newObj: %@",newObj);

    HPObject *initObj = [HPObject initObject];
    NSLog(@"initObj: %@",initObj);

    HPObject *copyObj = [HPObject copyObject];
    NSLog(@"copyObj: %@",copyObj);

    HPObject *mutableCopyObj = [HPObject mutableCopyObject];
    NSLog(@"mutableCopyObj: %@",mutableCopyObj);

    HPObject *otherObj = [HPObject otherObject];
    NSLog(@"otherObj: %@",otherObj);

    _objc_autoreleasePoolPrint();
}

输出:

image.png

单独每个验证都与结论一致,一起验证只有第一个非copy/new/copy/mutableCopy开始命名的方法返回的对象才加入自动释放池。
经过断点验证initObjotherObject都会走_objc_rootAutorelease的逻辑,但是在_objc_autoreleasePoolPrint调用之前,otherObject已经调用objc_autoreleasePoolPop释放了。所以才会不一致。

对应的汇编还原代码:

+(void *)allocObject {
    rax = objc_alloc_init(@class(HPObject), _cmd);
    return rax;
}

+(void *)newObject {
    rax = objc_alloc_init(@class(HPObject), _cmd);
    return rax;
}

+(void *)initObject {
    rax = objc_alloc_init(@class(HPObject), _cmd);
    rax = [rax autorelease];
    return rax;
}

+(void *)copyObject {
    rax = objc_alloc_init(@class(HPObject), _cmd);
    return rax;
}

+(void *)mutableCopyObject {
    rax = objc_alloc_init(@class(HPObject), _cmd);
    return rax;
}

+(void *)otherObject {
    rax = objc_alloc_init(@class(HPObject), _cmd);
    rax = [rax autorelease];
    return rax;
}

只有以非alloc/new/copy/mutableCopy开头的方法在编译阶段ARC才会调用autorelease。当然如果返回值以__autoreleasing修饰也会加入自动释放池。

alloc/new/copy/mutableCopy开头并且同时__autoreleasing修饰的情况:

image.png

总结:
自动释放池结构:


自动释放池结构
  • @autoreleasepool底层调用的是objc_autoreleasePoolPushobjc_autoreleasePoolPop
  • 自动释放池的存储数据的是一以AutoreleasePoolPage为解除的双向链表结构。其中包含了parentchild指针连接AutoreleasePoolPagebeginnext标记自动释放池的开始和结束。
    • 第一个pageparent 值为 nil
    • 最后一个pagechild 值为 nil
    • begin标记page开始的位置,从56字节开始(56字节存储page本身的数据)。
  • next指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin()
  • 每一个AutoreleasePoolPage都对应一个线程,一个线程可能对应多个page
  • 每个自动释放池第一个存储的对象是哨兵对象,并且每个自动释放池只有一个哨兵对象。POOL_BOUNDARY是一个为nil的空对象。
  • 多次添加同一个哨兵对象对应的count会进行+1
  • 每个page大小为4k
  • 自动释放池的释放会先遍历释放对象,然后释放对应page的空间。
  • __autoreleasing修饰的对象会被加入自动释放池。
  • alloc、new、copy、mutableCopy开头命名的方法返回的对象不会被加入自动释放池(编译期间就确定了)。

你可能感兴趣的:(OC内存管理-自动释放池)