自动释放池 AutoreleasePool

自动释放池是什么

自动释放池是OC中的一种内存自动回收机制,它可以将加入AutoreleasePool中的变量release的时机延迟,简单来说,就是当创建一个对象,在正常情况下,变量会在超出其作用域的时立即release。如果将对象加入到了自动释放池中,这个对象并不会立即释放,会等到runloop休眠/超出autoreleasepool作用域{}之后才会被释放

自动释放池的生命周期

  1. 从程序启动到加载完成,主线程对应的runloop会处于休眠状态,等待用户交互来唤醒runloop
  2. 用户的每一次交互都会启动一次runloop,用于处理用户的所有点击、触摸事件等
  3. runloop在监听到交互事件后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中
  4. 在一次完整的runloop结束之前,会向自动释放池中所有对象发送release消息,然后销毁自动释放池

自动释放池的数据结构

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

通过parent和child 可以看出AutorealeasePool内部结构是一条双向链表,AutoreleasePoolPage是双向链表的结点.

  1. autoreleasepool在加入要释放的对象时,底层调用的是objc_autoreleasePoolPush方法

  2. autoreleasepool在调用析构函数释放时,内部的实现是调用objc_autoreleasePoolPop方法

1. objc_autoreleasePoolPush进桟

 static inline void *push() 
    {
        id *dest;
        //判断是否为有 pool
        if (slowpath(DebugPoolAllocation)) {
            //如果没有,则通过autoreleaseNewPage方法创建
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
           //如果有,则通过autoreleaseFast压栈哨兵对象
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }
复制代码

objc_autoreleasePoolPush 源码分析

  1. 判断是否为有 pool
  2. 如果没有,则通过autoreleaseNewPage方法创建
  3. 如果有,则通过autoreleaseFast压栈哨兵对象
(1) autoreleaseNewPage方法

创建页 autoreleaseNewPage源码分析

  1. 通过hotPage获取当前页,判断当前页是否存在
  2. 如果不存在,则通过autoreleaseNoPage方法创建页
  3. 如果存在,则通过autoreleaseFullPage方法压栈对象
//******autoreleaseNewPage方法********//
 id *autoreleaseNewPage(id obj)
   {
        //1. 通过hotPage()获取当前操作页
        AutoreleasePoolPage *page = hotPage();
        //2. 判断当前页是否存在
        //2.1 如果存在,则通过autoreleaseFullPage方法压栈对象
        if (page) return autoreleaseFullPage(obj, page);
        //2.2 如果不存在,则通过autoreleaseNoPage方法创建页
        else return autoreleaseNoPage(obj);
   }
复制代码
//******hotPage()方法******
//获取当前操作页
static inline AutoreleasePoolPage *hotPage() 
 {
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
        tls_get_direct(key);
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
    if (result) result->fastcheck();
    return result;
 }
复制代码
① autoreleaseNoPage方法
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;
        //判断是否为空占位符,如果是,则压桟哨兵标识符置为yes
        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,则报错
        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;
        }
        //如果对象是哨兵标识符,且没有申请自动释放池内存,则设置一个空占位符存储在tls中,其目的是为了节省内存
        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);
        //设置 page为当前聚焦页
        setHotPage(page);
          // Push a boundary on behalf of the previously-placeholder'd pool.
          //如果压桟哨兵的标识符为yes,则压桟哨兵对象
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);//压桟哨兵对象
        }
        
        // Push the requested object or pool.
        //压桟对象
        return page->add(obj);
    }
复制代码
② autoreleaseFullPage方法
//添加自动释放池对象,当页满的时候调用这个方法
 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-while循环查找页面是否满了
        do {
            //如果子页面存在,则将页面替换为子页面
            if (page->child) page = page->child;
            //如果子页面不存在,则创建子页面
            else page = new AutoreleasePoolPage(page);
        } while (page->full());
        //设置为当前操作页面
        setHotPage(page);
        //将对象压桟
        return page->add(obj);
    }
复制代码
//添加释放对象
id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        //传入对象存储的位置
        id *ret = next;  // faster than `return next-1` because of aliasing
        //将obj压桟到next指针位置,然后next进行++,即下一个对象存储的位置
        *next++ = obj;
        protect();
        return ret;
    }
复制代码
(2) autoreleaseFast方法
static inline id *autoreleaseFast(id obj)
    {
        //1. 获取当前操作页
        AutoreleasePoolPage *page = hotPage();
        //2. 判断当前操作页是否满了
        if (page && !page->full()) {
            //如果未满,则压桟
            return page->add(obj);
        } else if (page) {
            //如果满了,则安排新的页面
            return autoreleaseFullPage(obj, page);
        } else {//页面不存在,则新建页面
            return autoreleaseNoPage(obj);
        }
    }
复制代码

autoreleaseFast方法, 压栈对象,源码解读

  1. 获取当前操作页,并判断页是否存在以及是否满了
  2. 如果页存在,且未满,则通过add方法压栈对象
  3. 如果页存在,且满了,则通过autoreleaseFullPage方法安排新的页面
  4. 如果页不存在,则通过autoreleaseNoPage方法创建新页

2. objc_autoreleasePoolPop出桟

    static inline void
    pop(void *token)
    {
        AutoreleasePoolPage *page;
        id *stop;
        //判断页面是否为空占位符
        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 (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);
            }
        }

        if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }
        //出桟页
        return popPage<false>(token, page, stop);
   }
复制代码
1. 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
            //特殊情况:在每个页面池调试期间删除所有内容
            //获取当前页面
            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
            //特殊情况:当调试丢失的自动释放池时,删除所有pop(top)
            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();
            }
        }
    }
复制代码
2. 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,如果不是,则进入循环
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            //获取当前操作页
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            //如果当前页是空的
            while (page->empty()) {
                page = page->parent;//将page赋值为父节点
                setHotPage(page);//设置父节点为当前操作页
            }

            page->unprotect();
            //next进行--操作,即出桟
            id obj = *--page->next;
            //将页索引置为SCRIBBLE,表示已被释放
            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
    }
复制代码

releaseUntil源码解析

通过循环遍历,判断对象是否等于stop,其目的是释放stop之前的所有的对象,首先通过获取page的next释放对象(即page的最后一个对象),并对next进行递减,获取上一个对象,判断是否是哨兵对象,如果不是则自动调用objc_release释放

3. kill()销毁当前页

kill实现,主要是销毁当前页,将当前页赋值为父节点页,并将父节点页的child对象指针置为nil

 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;
        //获取最后一个页
        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);
    }
复制代码

3. 进桟出桟总结

1) 压栈(push)

在页中压栈普通对象主要是通过next指针递增进行的

  • 当没有pool,即只有空占位符(存储在tls中)时,则创建页,压栈哨兵对象

  • 当页未满,将autorelease对象插入到栈顶next指针指向的位置(向一个对象发送autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置)

  • 当页满了(next指针马上指向栈顶),建立下一页page对象,设置页的child对象为新建页,新page的next指针被初始化在栈底(begin的位置),下次可以继续向栈顶添加新对象。

image.png

2) 出桟(pop)

在页中出栈普通对象主要是通过next指针递减进行的

  • 根据传入的哨兵对象地址找到哨兵对象所处的page

  • 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置.(从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page(在一个page中,是从高地址向低地址清理))

  • 当页空了时,需要赋值页的parent对象为当前页

image.png

局部释放池

1. 创建一个新的自动释放池

ARC下

@autoreleasepool {
  Student *s = [[Student alloc] init];
}
复制代码

MRC下

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init;
Student *s = [[Student alloc] init];
[pool drain];
复制代码

其中对象s会被加入到自动释放池,当ARC下代码执行到右大括号时(相当于MRC执行代码[pool drain];)会对池中所有对象依次执行一次release操作

那既然这样的话,我在ARC下我直接这样写

{
  Student *s = [[Student alloc] init];
}
复制代码

MRC下我直接这样写

Student *s = [[Student alloc] init];
[s release];
复制代码

效果和你用自动释放池是一样的啊,那我为什么要用它呢,还要初始化对象浪费时间浪费内存。

以上用到的Autoreleasepool叫做局部释放池,带着上面的问题,我们来看局部释放池的应用

2. 局部释放池的应用

看这段代码

for (int i = 0; i < largeNumber; i++) { 
    NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
    str = [str stringByAppendingString:@" - world"]; 
}
复制代码
  • [NSString stringWithFormat:@”hello -%04d”, i]方法创建的对象会加入到自动释放池里,对象的释放权交给了RunLoop 的释放池
  • RunLoop 的释放池会等待Runloop即将进入睡眠或者即将退出的时候释放一次
  • for循环中线程一直在做事情,Runloop不会进入睡眠
  • 上边的代码for循环生成的NSString对象会无法及时释放,造成瞬时内存占用过大

解决办法,每次循环时都手动创建一个局部释放池,及时创建,及时释放,这样NSString对象就会及时得到释放

for (int i = 0; i < largeNumber; i++) {
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
        str = [str stringByAppendingString:@" - world"];
    }
}
复制代码

在for循环大量使用imageNamed:之类的方法生成UIImage对象可能是个更要命的事情,内存随时可能因为占用过多被系统杀掉。这种情况下利用Autoreleasepool可以大幅度降低程序的内存占用。

3. Autorelease对象什么时候释放

当创建了局部释放池时,会在@autoreleasepool{}的右大括号结束时释放,及时释放对象大幅度降低程序的内存占用。在没有手动加@autoreleasepool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的(每个线程对应一个runloop),而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池。

面试题

  1. 临时变量什么时候释放?

如果在正常情况下,一般是超出其作用域就会立即释放
如果将临时变量加入了自动释放池,会延迟释放,即在runloop休眠或者autoreleasepool作用域之后释放

  1. AutoreleasePool原理

自动释放池的本质是一个AutoreleasePoolPage结构体对象,是一个栈结构存储的页,每一个AutoreleasePoolPage都是以双向链表的形式连接
自动释放池的压栈和出栈主要是通过结构体的构造函数和析构函数调用底层的objc_autoreleasePoolPush和objc_autoreleasePoolPop,实际上是调用AutoreleasePoolPage的push和pop两个方法
每次调用push操作其实就是创建一个新的AutoreleasePoolPage,而AutoreleasePoolPage的具体操作就是插入一个POOL_BOUNDARY,并返回插入POOL_BOUNDARY的内存地址。而push内部调用autoreleaseFast方法处理,主要有以下三种情况

  • 当page存在,且不满时,调用add方法将对象添加至page的next指针处,并next递增
  • 当page存在,且已满时,调用autoreleaseFullPage初始化一个新的page,然后调用add方法将对象添加至page栈中
  • 当page不存在时,调用autoreleaseNoPage创建一个hotPage,然后调用add方法将对象添加至page栈中
  • 当执行pop操作时,会传入一个值,这个值就是push操作的返回值,即POOL_BOUNDARY的内存地址token。

所以pop内部的实现就是根据token找到哨兵对象所处的page中,然后使用 objc_release 释放 token之前的对象,并把next 指针到正确位置

  1. AutoreleasePool能否嵌套使用?

可以嵌套使用,其目的是可以控制应用程序的内存峰值,使其不要太高
可以嵌套的原因是因为自动释放池是以栈为节点,通过双向链表的形式连接的,且是和线程一一对应的
自动释放池的多层嵌套其实就是不停的pushs哨兵对象,在pop时,会先释放里面的,在释放外面的

  1. 哪些对象可以加入AutoreleasePool?alloc创建可以吗?

使用new、alloc、copy关键字生成的对象和retain了的对象需要手动释放,不会被添加到自动释放池中
设置为autorelease的对象不需要手动释放,会直接进入自动释放池
所有 autorelease 的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中 面试题5:AutoreleasePool的释放时机是什么时候?
App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是 -2147483647,优先级最高,保证创 建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即 将退出 Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

  1. thread 和 AutoreleasePool的关系

每个线程,包括主线程在内都维护了自己的自动释放池堆栈结构
新的自动释放池在被创建时,会被添加到栈顶;当自动释放池销毁时,会从栈中移除对于当前线程来说,会将自动释放的对象放入自动释放池的栈顶;在线程停止时,会自动释放掉与该线程关联的所有自动释放池
总结:每个线程都有与之关联的自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈,对于当前线程来说,释放对象会被压栈到栈顶,线程停止时,会自动释放与之关联的自动释放池 面试题7:RunLoop 和 AutoreleasePool的关系
主程序的RunLoop在每次事件循环之前之前,会自动创建一个 autoreleasePool
并且会在事件循环结束时,执行drain操作,释放其中的对象

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享