自动释放池 AutoreleasePool

2,407 阅读17分钟

自动释放池是什么

自动释放池是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字节
    ...
}

AutorealeasePool就是由AutoreleasePoolPage构成的双向链表

IMG_1904.JPG AutoreleasePoolPage是双向链表的节点,单个 AutoreleasePoolPage 结构如下:

IMG_1903.JPG 其中有 56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的 0x100816038 ~ 0x100817000 都是用来存储加入到自动释放池中的对象。 每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000)

1:begin() 和 end() 这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。

2:next 指向下一个为空的内存地址,如果 next 指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中。

自动释放池实质上是一个指针堆栈。每个指针要么指向要被释放的对象,要么是POOL_BOUNDARY(哨兵对象)。

token本质就是指向POOL_BOUNDARY的指针,存储着每次push时插入的POOL_BOUNDARY的地址。 POOL_BOUNDARY 就是哨兵对象,它是一个宏,值为nil,标志着一个自动释放池的边界。

autoreleasepool在初始化时,内部是调用objc_autoreleasePoolPush方法

autoreleasepool在调用析构函数释放时,内部是调用objc_autoreleasePoolPop方法

1. objc_autoreleasePoolPush进桟

一个 push 操作其实就是创建一个新的 autoreleasepool ,对应 AutoreleasePoolPage 的具体实现就是往 AutoreleasePoolPage 中的 next 位置插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址。

push.png

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

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"可能是没有pool被push过
        //或者push过一个空的占位符池,但是还没有内容
        ASSERT(!hotPage());

        bool pushExtraBoundary = false;
        //判断是否为空占位符,如果是,则压桟哨兵标识符置为yes
        if (haveEmptyPoolPlaceholder()) {
            pushExtraBoundary = true;
        }
        //如果对象不是哨兵标识符,且没有pool,则报错
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            _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();//设置空的占位符
        }

        //初始化第一页
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        //设置 page为当前页
        setHotPage(page);
        //如果压桟哨兵的标识符为yes,则压桟哨兵对象
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);//压桟哨兵对象
        }
        
        //压桟对象
        return page->add(obj);
    }

无hotPage,创建hotPage加入其中. 这个时候,由于内存中没有 AutoreleasePoolPage,就要从头开始构建这个自动释放池的双向链表,那么当前页表作为第一张页表,是没有parent指针的。 autoreleaseFullPage

/**添加自动释放池对象,当页满的时候调用这个方法
*一直遍历,直到找到一个未满的 AutoreleasePoolPage
*如果找到最后还是没找到,就新建一个 AutoreleasePoolPage
*将找到的或者构建的page作为hotPage,然后将obj加入,即入栈
*/
 id *autoreleaseFullPage(id obj, AutoreleasePoolPage *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);
    }

add

//压桟对象
id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        //传入对象存储的位置(比' return next-1 '更快,因为有别名)
        id *ret = next; 
        //将obj压桟到next指针位置,然后next进行++,即下一个对象存储的位置
        *next++ = obj;
        protect();
        return ret;
    }
(2) 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);
   }

autoreleaseNewPage源码分析

  1. 通过hotPage获取当前页,判断当前页是否存在
  2. 如果不存在,则通过autoreleaseNoPage方法创建页
  3. 如果存在,则通过autoreleaseFullPage方法压栈对象

2. objc_autoreleasePoolPop出桟

我们一般都会在这个方法中传入一个哨兵对象 POOL_SENTINEL,如下图一样释放对象:

pop.png

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

        // 删除空子项
        if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
            //特殊情况:在每个页面池调试期间删除所有内容
            //获取当前页面
            AutoreleasePoolPage *parent = page->parent;
            //将当前页面杀掉
            page->kill();
            //设置父节点页面为当前操作页
            setHotPage(parent);
        } else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            //特殊情况:当调试丢失的自动释放池时,删除所有pop(top)
            page->kill();
            setHotPage(nil);
        } else if (page->child) {
            //滞后:如果页面超过一半是满的,则保留一个空的子节点
            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

main 函数解读

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

在终端中使用clang -rewrite-objc main.m 命令将上述OC代码重写成C++的实现 搜索main我们可以看到main函数的实现重写成了如下代码:

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

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_49_sdbnp0nd07q4m_sh4_gw52r40000gn_T_main_9e48ee_mi_0);
    }
    return 0;
}

通过对比可以发现,苹果通过声明一个__AtAutoreleasePool类型的局部变量,@autoreleasepool被转换成__AtAutoreleasePool 结构体类型 所以整个 iOS 的应用都是包含在一个自动释放池 block 中的 @autoreleasepool{} 本质上是一个结构体:

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

这个结构体在初始化的时候会调用:objc_autoreleasePoolPush() 方法 在析构的时候,会调用:objc_autoreleasePoolPop 方法 这表明,main函数实际工作的时候,是这样的:

int main(int argc, const char * argv[]) {
    {
        //这里的 atautoreleasepoolobj 就是一个 POOL_SENTINEL
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();
        
        // do things you want
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

局部释放池

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时,会先释放里面的,在释放外面的

  2. 哪些对象可以加入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操作,释放其中的对象