美文网首页
iOS 内存管理(五)-AutoreleasePool

iOS 内存管理(五)-AutoreleasePool

作者: 搬砖的crystal | 来源:发表于2022-04-08 14:06 被阅读0次

一、Autorelease 简介

1.基本介绍

AutoreleasePool(自动释放池)是 OC 中的一种内存管理机制,它持有释放池里的对象的所有权,在自动释放池销毁时,统一给所有对象发送一次 release 消息。通过这个机制,可以延迟对象的释放。

UIImage *img = [UIImage imageNamed:@"xxxx.png"];

这个 UIImage 对象是在类方法 imageNamed 里创建完后再返回,对象的所有权归方法持有,如果不延迟释放,在方法结束时对象就被释放了,返回的就为 nil。因此,需要将对象先加入 AutoreleasePool,所有权归自动释放池持有,只有自动释放池销毁时才释放,这样 UIImage 对象才能在方法结束后正常返回。

2.创建
(1)手动创建

通常使用 @autoreleasepool {} 代码块来手动创建一个自动释放池:

@autoreleasepool {
    //这里创建自动释放的对象,创建的对象会被加入到AutoreleasePool对象里
    ... ...
}

这个代码块等价于:

{
    //创建一个AutoreleasePool对象
    __AtAutoreleasePool *atautoreleasepoolobj = objc_autoreleasePoolPush(); 
    
    //这里创建自动释放的对象,创建的对象会被加入到AutoreleasePool对象里
    ... ...    

   //给所有自动释放的对象发送一次release消息,并销毁AutoreleasePool对象
   objc_autoreleasePoolPop(atautoreleasepoolobj)
}

`{}`表示AutoreleasePool对象的作用域
(2)在 Runloop 中的创建和销毁

通常情况下,在平时开发中不需要手动创建自动释放池,因为 Runloop 会自动创建和销毁 AutoreleasePool 对象。

App启动后,系统在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()

第一个 Observer 监视一个事件:

  • Entry(即将进入 Loop):调用 objc_autoreleasePoolPush 来创建自动释放池。

第二个 Observer 监视了两个事件:

  • Before waiting(准备进入休眠):先调用 objc_autoreleasePoolPop 销毁旧的自动释放池,再调用 objc_autoreleasePoolPush创建一个新的自动释放池。
  • Exit(即将退出 Loop):调用 objc_autoreleasePoolPop 销毁自动释放池。

第一个 Observe 的 order 是 -2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 的 order 是 2147483647,优先级最低,保证销毁自动释放池发生在其他所有回调之后。

在一个 RunLoop 事件开始的时候会自动创建一个 AutoreleasePool,在事件结束时再自动销毁。上面举例的 imageNamed 方法内部创建的对象也是加入到主线程 RunLoop 创建的 AutoreleasePool 中实现延迟释放的。因此,通常在开发中不需要开发者自己创建 AutoreleasePool

(3)手动创建 AutoreleasePool 的场景

虽然 Runloop 会自动创建和销毁自动释放池,但在有些情况下还是需要手动创建 AutoreleasePool苹果官方文档建议在下面这三种情况下可能需要开发者创建自动释放池:

  • 编写不基于 UI 框架的程序,例如命令行工具。

这一点的原因不是特别清楚,猜测是不基于 UI 框架的程序,可能不响应用户事件,导致不自动创建和销毁自动释放池。

  • 编写一个创建大量临时对象的循环。

在循环内使用自动释放池块可以在下一次迭代之前释放这些对象,有助于减少应用程序的最大内存占用,即降低内存峰值。

  • 编写非 Cocoa 程序时创建子线程。

Cocoa 程序中的每个线程都维护自己的自动释放池块堆栈。而编写一个非 Cocoa 程序,比如 Foundation-only program,这时如果创建了子线程,若不手动创建自动释放池,自动释放的对象将会堆积得不到释放,导致内存泄漏。

第二种情况举例:

//情况一:循环内不使用AutoreleasePool
for (int i = 0; i<1000000; i++) {

    NSString *string = [NSString stringWithFormat:@"%@", @"0123456789"];
    NSLog(@" ==== %p", string);
}

//情况二:循环内使用AutoreleasePool
for (int i = 0; i<1000000; i++) {

    @autoreleasepool {

        NSString *string = [NSString stringWithFormat:@"%@", @"0123456789"];
        NSLog(@" ==== %p", string);
    }
}

分别运行上面两种情况可以看到,在循环过程中,第一种情况的内存占用一直在增加,第二种情况的内存不会增加。这是因为:

情况一:循环过程中,创建的 NSString 对象一直在堆积,只有在循环结束才一起释放,所以内存一直在增加。
情况二:每一次迭代中都会创建并销毁一个 AutoreleasePool,而每一次创建的 NSString 对象都会加入到 AutoreleasePool 中,所以在每次 AutoreleasePool 销毁时,NSString 对象就会被释放,这样内存就不会增加。

这个场景中 AutoreleasePool 是通过立即释放对象来降低内存峰值,而前面又说自动释放池用来延迟对象的释放,这两者其实不矛盾,本质是一样的,都是在自动释放池销毁时调用 objc_autoreleasePoolPop 来释放池中的对象。只不过调用的时机不同,这里的 @autoreleasepool {} 是在超出自己的作用域时就调用函数来销毁,而前面的是在 Runloop 休眠或退出时才调用函数来销毁,所以调用的时机不同,才会实现立即或者延迟释放的目的。

3.哪些对象可以被添加到自动释放池

在 MRC 模式下,只要给对象发送 autorelease 消息,这个对象就会被添加到自动释放池。
但在 ARC 模式下,是由编译器自动给对象发送 autorelease 消息,且不会给所有的对象都发送,只会给被编译器识别为自动释放的对象发送。一般来说,使用类方法(工厂方法)实例化的对象才是自动释放的对象,才能被添加到自动释放池,而使用 newalloccopy 关键字生成的对象和 retain 了的对象,不会被添加到自动释放池中。
UIImage 对象为例:

for (int i = 0; i<1000000; i++) {

    @autoreleasepool {

        //1.自动释放的对象,需要被添加到自动释放池中
        UIImage *image = [UIImage imageNamed:@"test.png"];
        
        //2.非自动释放的对象,不能被添加到自动释放池中
        UIImage *image = [[UIImage alloc] init];
        
        NSLog(@" ==== image = %p", image);
    }
}

二、原理分析

自动释放池是一个栈的结构,被划分成了一个以 page 为结点的双向链表,根据需要添加和删除页面。每一页的大小为 4096 字节,里面存储着指向自动释放的对象或者 pool_boundary 哨兵的指针。hot page表示当前活跃的页,存储新添加的自动释放的对象。

使用 @autoreleasePool 代码块可以创建一个自动释放池,通过 Clang 编译后发现底层实现如下:

{
    //创建一个AutoreleasePool对象
    __AtAutoreleasePool *atautoreleasepoolobj = objc_autoreleasePoolPush(); 
    
    //这里创建自动释放的对象,创建的对象会被加入到AutoreleasePool对象里
    ... ...    

   //给所有自动释放的对象发送一次release消息,并销毁AutoreleasePool对象
   objc_autoreleasePoolPop(atautoreleasepoolobj)
}

因此,单个 Autoreleasepool 的运行过程可以简单地理解为 objc_autoreleasePoolPush[对象 autorelease]objc_autoreleasePoolPop 这三个过程。

1. push 操作

objc_autoreleasePoolPush 函数在底层是调用 AutoreleasePoolPagepush 函数。在源码中断点调试可知,push 函数的调用链为:push -> autoreleaseFast(POOL_BOUNDARY) -> autoreleaseNoPage(POOL_BOUNDARY)

(1)push
static inline void *push() 
{
    id *dest;
    //这个判断不知道哪个实际场景可以触发,运行后都是false。源码注释:halt when autorelease pools are popped out of order, and allow heap debuggers to track autorelease pools
    //老版本源码里没有这个判断,直接走 dest = autoreleaseFast(POOL_BOUNDARY)
    if (slowpath(DebugPoolAllocation)) {
        //这里基本不会进,而且全局搜索发现autoreleaseNewPage函数只在这里有调用
        // Each autorelease pool starts on a new pool page.
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        //每次都走这里
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);

    //返回的dest就是后面执行pop函数的入参
    return dest;
}

push 里直接调用 autoreleaseFast(POOL_BOUNDARY) 函数,返回的 dest 即为后面执行 pop 函数的入参,本质是哨兵地址。

(2)autoreleaseFast(POOL_BOUNDARY)
static inline id *autoreleaseFast(id obj)
{
    //获取当前操作的page,hot page
    AutoreleasePoolPage *page = hotPage();
    //判断当前页
    if (page && !page->full()) {
        //如果page存在并且没有满
        return page->add(obj);
    } else if (page) {
        //如果page存在并且已经满了
        return autoreleaseFullPage(obj, page);
    } else {
        //如果page不存在
        return autoreleaseNoPage(obj);
    }
}

这个方法里先获取当前操作的 page,然后进行判断:
如果当前页存在,并且没有满,就调用 add 函数压栈对象。
如果当前也存在且已经满了,就调用 autoreleaseFullPage 函数创建新页面,并压栈对象。
如果当前页不存在,即自动释放池为 nil 或者为 empty(空的),就调用 autoreleaseNoPage 函数来设置空白占位符,或者创建第一页。

这里 push 操作调用的 autoreleaseFast,会执行 autoreleaseNoPage 函数,入参为 POOL_BOUNDARY

(3)autoreleaseNoPage(POOL_BOUNDARY)
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    //"No page"表示还没创建自动释放池,或者创建了一个空内容的释放池。
    // "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;
    //若设置了空白占位符,则需要压栈哨兵
    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;
    }
    //若当前obj不是哨兵,且释放池为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;
    }
    //若当前obj为哨兵,就设置空白占位符并返回。只有push操作时当前obj才是哨兵
    else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        //push操作会走到这里,然后返回
        // 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();
    }

    //从这里开始,下面就是第一次autorelease操作处理的事项
    
    // We are pushing an object or a non-placeholder'd pool.

    // Install the first page.
    //压栈第一个第一个对象时,需要创建第一页
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    //将第一页设置为hot page
    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.
    //压栈对象并返回
    return page->add(obj);
}

autoreleaseNoPage 函数在 push 操作和第一次 autorelease 操作时会调用。在 push 操作时,会在这里调用 setEmptyPoolPlaceholder 函数来设置一个空白占位符,表示已创建了一个自动释放池。

//设置空白占位符
static inline id* setEmptyPoolPlaceholder()
{
    ASSERT(tls_get_direct(key) == nil);
    //通过"key-value"形式将空白占位符存储在tls中,并返回空白占位符的地址
    //tls为当前线程的本地存储,用于保存当前线程的一些信息
    tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
    return EMPTY_POOL_PLACEHOLDER;
}

//判断是否设置空白占位符
static inline bool haveEmptyPoolPlaceholder()
{
     //通过key-value获取值来判断
     id *tls = (id *)tls_get_direct(key);
     return (tls == EMPTY_POOL_PLACEHOLDER);
 }

//自动占位符,地址为 0x1
# define EMPTY_POOL_PLACEHOLDER ((id*)1)

//key值
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
# define AUTORELEASE_POOL_KEY  ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY3)

因此,push 操作本质上是设置一个空白占位符,地址为 0x1 ,并存储在当前线程的 tls 中,通过这个空白占位符来判断自动释放池是否已创建。在 push 过程中,既没有创建 page,也没有压栈哨兵。

2. autorelease 操作

给对象发送 autorelease 消息即可加入到自动释放池,autorelease 方法在底层是调用 AutoreleasePoolPageautorelease 函数,入参为对象本身。

inline id 
objc_object::rootAutorelease()
{
    //若是TaggedPointer对象,直接返回,不处理
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

__attribute__((noinline,used))
id 
objc_object::rootAutorelease2()
{
    ASSERT(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

//autorelease操作
static inline id autorelease(id obj)
{
    ASSERT(obj);
    ASSERT(!obj->isTaggedPointer());
    //压栈对象
    id *dest __unused = autoreleaseFast(obj);
    ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    return obj;
}

从代码中可知,若对象是 TaggedPointer 类型,则不能被添加到自动释放池。 autorelease 操作实际上是通过调用 autoreleaseFast 函数来压栈对象,实现逻辑为(源码上面已附):
hot page 不存在,即压栈的是第一个对象,此时是空白的自动释放池,就调用 autoreleaseNoPage 函数来创建第一页 page,并设为 hot page,先压栈哨兵,然后压栈对象。
hot page 存在且没满时,就调用 add 函数来压栈对象。
hot page 存在且已满,就调用 autoreleaseFullPage 函数来新建一页 page,再压栈对象。

(1)hotPage
//设置hot page
static inline void setHotPage(AutoreleasePoolPage *page) 
{
    if (page) page->fastcheck();
    //通过key-value形式,将当前page存储在tls中,即为hot page
    tls_set_direct(key, (void *)page);
}

//获取hot page
static inline AutoreleasePoolPage *hotPage() 
{
     //从当前线程的tls中通过key获取到page,即为hot page
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
        tls_get_direct(key);
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
    if (result) result->fastcheck();
    return result;
}

hot page 和上面的空白占位符一样,存储在当前线程的 tls 中,通过 key 来读写。

(2)add
id *add(id obj)
{
    ASSERT(!full());
    unprotect();
    id *ret = next;  // faster than `return next-1` because of aliasing
    //先从next指针指向的地址里开始存放obj的地址,为8字节
    //再将next指针像高地址偏移8字节,指向的地址用于存放下一次添加的obj的地址
    *next++ = obj;
    protect();
    return ret;
}

add(obj) 实际上是先从当前 next 指针指向的地址里开始存放 obj 的地址,为 8 字节大小,再将 next 指针像高地址偏移 8 字节,新指向的地址用于存放下一次添加的 obj 的地址。当压栈第一个对象时,需要先压栈哨兵。

//在第一页里先压栈哨兵
page->add(POOL_BOUNDARY);

# define POOL_BOUNDARY nil

POOL_BOUNDARYnil,说明压栈哨兵其实只是将 next 指针偏移8字节,从新地址上开始存放对象地址。因此,哨兵其实是自动释放池中对象的边界,而这个边界地址就是在后面执行 pop 操作时的入参。

(3)autoreleaseFullPage

压栈对象时,若 hot page 满了,就调用 autoreleaseFullPage 函数来新建 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);
     
    //这个函数传进来的已经是hot page
    //do-while循环获取没满的page
    do {
        //如果存在子页面,就用子页面替换当前page来判断
        if (page->child) page = page->child;
        //如果没有子页面,而当前page已满,则新建页面
        else page = new AutoreleasePoolPage(page);
    } while (page->full());   //判断page是否已满

    //将page设置为hot page
    setHotPage(page);
    //压栈对象
    return page->add(obj);
}

//判断page是否已满
bool full() { 
   //通过next的值和page的结束地址来判断
    return next == end();
}

//page的end地址
id * end() {
    //this为当前page的首地址,通过首地址加上4096字节,就可得出当前page的结束地址
    return (id *) ((uint8_t *)this+SIZE);
}

//SIZE即为每页AutoreleasePoolPage的大小,为4096字节
static size_t const SIZE = 4096

因此,压栈对象时,若 hot page 已满,通过 do-while 循环来判断每一个子页面是否已满,若没满,则将当前子页面设为 hot page,若都满了,就新建一个页面并设为 hot page,再压栈对象。判断 page 是否已满,是通过将 page 的结束地址和 next 指针对比,若相等,则表示 page 已满。

3.pop 操作

pop 操作是向自动释放池里的每个对象发送一次 release 消息,然后销毁掉释放池里的页面。objc_autoreleasePoolPop 函数在底层是调用 AutoreleasePoolPagepop 函数,入参为哨兵地址。

static inline void
pop(void *token)
{
    //传进来的token即为哨兵地址
    
    AutoreleasePoolPage *page;
    id *stop;
    //如果token为空白占位符
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        // Popping the top-level placeholder pool.
        //获取hot page
        page = hotPage();
        if (!page) {
            // Pool was never used. Clear the placeholder.
            //如果page不存在,说明这个自动释放池没有压栈任何对象,直接清除占位符
            //hotPage和空白占位符在tls中存储都是同一个key,所以可以清空
            return setHotPage(nil);
        }

        // Pool was used. Pop its contents normally.
        // Pool pages remain allocated for re-use as usual.
        //容错处理,hot page存在,而哨兵地址异常,则需要更正
        //如果页面存在,获取到第一页
        page = coldPage();
        //将token的值设置为第一页的哨兵地址
        token = page->begin();
    } else {
        //获取token所在的页
        page = pageForPointer(token);
    }

    //将token赋值给stop,则stop此时就是哨兵地址
    stop = (id *)token;
    //容错判断,若stop指向的不是哨兵地址
    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin()  &&  !page->parent) {
            //若stop指向当前页的begin(),且当前页没有父页面,说明当前也是第一页,stop指向的就是哨兵地址,不做任何处理
            // 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 {
            //若stop指向当前页的begin(),而当前页有父页面,说明stop指向的不是哨兵地址,则报异常并返回
            // Error. For bincompat purposes this is not 
            // fatal in executables built with old SDKs.
            return badPop(token);
        }
    }

    //这个判断基本为false
    if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
        return popPageDebug(token, page, stop);
    }
 
    //出栈页
    return popPage<false>(token, page, stop);
}

pop 操作中传进来的 token 即为哨兵地址,这里主要处理两件事:
如果当前是只有空白占位符的自动释放池,则清除掉空白符,然后返回,这个释放池也就被销毁了。
如果不是空白的自动释放池,先确保 token 为哨兵地址,然后获取到 token 所在的页 page,并设置结束位 stop(也是哨兵地址),最后调用 popPage 出栈页,完成 pop 操作。

也就是说,如果当前是空白的自动释放池, pop 操作就会清除掉空白占位符,销毁释放池;如果不是空白的自动释放池,pop 操作是通过调用 popPage 函数来出栈页,这个函数的三个入参:tokenstop 均为哨兵地址,page 为当前哨兵所在的页。如果自动释放池没有嵌套,page 就为自动释放池的第一页,如果有嵌套,每个子 pool 都会有一个哨兵,page 就为当前 pool 哨兵所在的最外层 pool 中的页。

(1)coldPage
static inline AutoreleasePoolPage *coldPage() 
{
    //获取到当前页
    AutoreleasePoolPage *result = hotPage();
    //while循环获取到自动释放池的第一页
    if (result) {
        while (result->parent) {
            result = result->parent;
            result->fastcheck();
        }
    }
    return result;
}

这个函数其实就是通过 hotPage 来获取到自动释放池的第一页。

(2)begin
id * begin() {
    //当前页的首地址 + 自身成员占用的内存(56字节),即为begin的地址
    return (id *) ((uint8_t *)this+sizeof(*this));
}

页面的首地址加上自身成员占用的 56 字节大小,就是 begin 位置。第一页,begin 位置为哨兵的起始地址,其它页则为该页第一个对象的起始地址。

(3)pageForPointer
static AutoreleasePoolPage *pageForPointer(const void *p) 
{
    //p为传进来的token,即哨兵地址
    return pageForPointer((uintptr_t)p);
}

static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
{
    AutoreleasePoolPage *result;
    //SIZE是一页page的大小,为4096字节
    //p % SIZE,是因为当pool嵌套时,需要先release子pool里的对象,通过这个方法可以获取到子pool距其在父pool中所在页起始位置的偏移量
    uintptr_t offset = p % SIZE;

    ASSERT(offset >= sizeof(AutoreleasePoolPage));

   //p - offset,即可获取到子pool所在的父pool的页的起始地址
    result = (AutoreleasePoolPage *)(p - offset);
    result->fastcheck();

    return result;
}

这个函数就是获取哨兵所在的页,当 pool 嵌套时,先 releasepool,就需要获取到子 pool 在父 pool 里所处的页。

(4)popPage
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    /**
     *  出栈页,这个函数的三个入参:
     *  token:哨兵地址
     *  page:哨兵所在的页,一般为自动释放池的第一页
     *  stop:哨兵地址
     */  
    
    //pop操作入参的allowDebug为false
    if (allowDebug && PrintPoolHiwat) printHiwat();

    //release all objs,给自动释放池中哨兵后的所有对象发送一次release消息
    //执行完这一步,自动释放池里就只剩下空页面了
    page->releaseUntil(stop);

    // memory: delete empty children
    //kill page,移除页面,至少会保留第一页
    if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        //debug判断,pop操作不会进这里,因为传的allowDebug为false
        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
        //debug判断,pop操作不会进这里,因为传的allowDebug为false
        page->kill();
        setHotPage(nil);
    } else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        //有多个页面的自动释放池,在pop操作时会进入这里
        if (page->lessThanHalfFull()) {
            //若当前页的对象数量比一半还少,从第一页的子页面开始kill,只保留第一页
            //如果没有嵌套pool,实际上在上面执行完releaseUntil(stop)后,page都是空页面,所以pop操作基本会走这里
            //如果有嵌套pool,传进来的page可能为子pool处于最外层pool的页,此时release完子pool的对象指针后,当前页保存的外层pool的对象指针数可能会大于页数量的一半,就会触发moreThanHalfFull的条件
            page->child->kill();
        }
        else if (page->child->child) {
            //当前页的对象指针数量比一半还多,且页面数量大于3,就从第三个页开始kill,保留第一页和第二页
            page->child->child->kill();
        }
    }
}

从代码分析可知,pop 操作调用 popPage 来出栈页时,主要做了两个处理:
release all objs:给自动释放池中哨兵后的所有对象发送一次 release 消息。这一步执行完,自动释放池里就只剩下空页面了。
kill page:移除页面,只保留第一页,其他页面都会被移除。

接下来再分析 popPage 函数中几个关键函数的实现:

  • releaseUntil
void releaseUntil(id *stop) 
{
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
    
    //popPage函数传进来的stop为哨兵地址,this为自动释放池的第一页
    
    //this ->next,即第一页的next指针
    //判断第一页的对象是否被release完
    while (this->next != stop) {
        // Restart from hotPage() every time, in case -release 
        // autoreleased more objects
       
        //获取到hot page,当前操作的页面,即最后一页
        AutoreleasePoolPage *page = hotPage();

        // fixme I think this `while` can be `if`, but I can't prove it
        //如果当前page为空,则获取到父页面,并将父页面设为hot page
        //确保下一次获取的hot page为这个父页面,实现了页面从后往前的遍历
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        //每次都获取当前页的最后一个对象,实现了对象从后往前遍历释放
        id obj = *--page->next;
        //获取完后偏移next指针,确保一直指向当前页的最后一个对象地址
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        //如果对象不为nil(不是哨兵),就释放
        if (obj != POOL_BOUNDARY) {
            objc_release(obj);
        }
    }

    //释放完所有的对象后,就将第一页设为hot page
    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 函数的处理逻辑为:从后往前遍历自动释放池里的页面,在每一页中再从后往前遍历释放对象,直到哨兵为止。释放完对象后,释放池中就只剩下空的页面,并将第一页设为 hot page

  • lessThanHalfFull
//判断当前页是否还没存满一半
bool lessThanHalfFull() {
    //next:当前页最后一个对像指针在页里的地址
    //begin():前面分析了,为当前页的首地址 + 58字节(自身成员的大小)
    //end():当前页的结束地址 
    return (next - begin() < (end() - begin()) / 2);
}

//当前页的结束地址
id * end() {
   //当前页的首地址 + 4096字节,就为当前页的结束地址 
   //每页大小为4096字节
    return (id *) ((uint8_t *)this+SIZE);
}

从代码分析可知,是通过当前页最后一个对象指针在页中地址的偏移量计算,来判断当前页是否存满一半。

  • kill
void kill() 
{
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
    
   //pop操作是从自动释放池的第二页开始kill
   //因此这里的this是第二页,可以通过打印输出来印证

    //从第二页开始,循环遍历子页面,最终获取到自动释放池的最后一页
    AutoreleasePoolPage *page = this;
    while (page->child) page = page->child;

    //从最后一页开始往前遍历销毁,直到this(即第二页)销毁后停止
    //每页的处理逻辑为:先获取到当前页面的父页面,再将父页面的子页面设为nil,即销毁了当前页面
    AutoreleasePoolPage *deathptr;
    do {
        deathptr = page;
        //获取到父页面
        page = page->parent;
        if (page) {
            page->unprotect();
            //销毁子页面
            page->child = nil;
            page->protect();
        }
        delete deathptr;
    } while (deathptr != this);
}

从代码分析可知,kill 函数是从自重释放池的队后一页开始往前遍历销毁,直到销毁第二页后停止。每一次销毁页面,都是先获取到当前页面的父页面,再将父页面的子页面设为 nil,即实现了页面销毁。

因此,pop 操作时,如果不是空白的自动释放池,是先从后往前遍历自动释放池里的所有页面,将每一页里的对象从后往前逐步 release,然后再次从后往前遍历销毁这些空页面,第一页保留不销毁,最后自动释放池里就只剩下一页空的 page,并且为 hot page,这就完成了 pop 操作。

4.总结

经过对源码的深入分析,我们可以得出以下结论:
(1)一个自动释放池的运行过程分为这三步:push 操作、[对象 autorelease] 以及 pop 操作。
(2)push 操作本质上是设置一个空白占位符,地址为 0x1,并存储在当前线程的本地存储 tls 中,设置了这个空白占位符就表示已经创建了一个空的自动释放池。在 push 过程中,既没有创建 page,也没有压栈哨兵。
(3)autorelease 操作就是压栈对象,给对象发送 autorelease 消息,将对象添加到自动释放池中,在这个过程中会做如下处理:

  • 如果压栈的是第一个对象,此时是空白的自动释放池,就创建第一页 page,并设为 hot page,然后先压栈哨兵,再压栈对象。
  • hot page 存在且没满时,直接压栈对象。
  • hot page 存在且已满,新建一页 page,再压栈对象。

(4)pop 操作出栈时,会根据自动释放池的情况分别做不同的处理:

  • 若是空白的自动释放池,即内部只有空白占位符,此时pop操作会清除占位符,销毁自动释放池。
  • 若自动释放池存有压栈对象,就先 release 对象,再 kill 页面。最后,自动释放池并没有被销毁,里面还存有一张空的 page,也是之前的第一页。

(5)对于不是空白的自动释放池,pop 操作过程中具体做了如下处理:

  • release 对象:从后往前遍历自动释放池里的页面,在每一页中再从后往前遍历释放对象,直到哨兵为止。release 后,池中只剩下空白的页面,然后将第一页设为 hot page
  • kill 页面:从后往前遍历销毁这些空页面,第一页保留不销毁。

三、AutoreleasePool嵌套分析

//释放池嵌套
int main(int argc, char * argv[]) {
    
    //pool1,里面添加1个自动释放对象
    @autoreleasepool {

        NSObject *obj = [[NSObject alloc] autorelease];

        //pool2,里面添加3个自动释放对象
        @autoreleasepool {

            NSInteger count = 3;
            for(NSInteger i=0; i<count; i++){

                NSObject *obj = [[NSObject alloc] autorelease];
            }

            //pool3,里面添加2个自动释放对象
            @autoreleasepool {

                NSInteger count = 2;
                for(NSInteger i=0; i<count; i++){

                    NSObject *obj = [[NSObject alloc] autorelease];
                }

                //创建完三个pool后打印
                _objc_autoreleasePoolPrint();
            }
            
            //超出pool3的作用域时打印
            _objc_autoreleasePoolPrint();
        }
        
        //超出pool2的作用域时打印
        _objc_autoreleasePoolPrint();
    }

    //超出pool1作用域打印
    _objc_autoreleasePoolPrint();

    return 0;
}

如代码所示,在 main 函数里嵌套创建了 3 个自动释放池,然后在不同时期打印内存结构。

创建后打印
objc[41194]: ##############
objc[41194]: AUTORELEASE POOLS for thread 0x114550dc0
objc[41194]: 9 releases pending.
objc[41194]: [0x7fe8e9815000]  ................  PAGE  (hot) (cold)
objc[41194]: [0x7fe8e9815038]  ################  POOL 0x7fe8e9815038
objc[41194]: [0x7fe8e9815040]    0x600001c5c060  NSObject
objc[41194]: [0x7fe8e9815048]  ################  POOL 0x7fe8e9815048
objc[41194]: [0x7fe8e9815050]    0x600001c5c070  NSObject
objc[41194]: [0x7fe8e9815058]    0x600001c5c080  NSObject
objc[41194]: [0x7fe8e9815060]    0x600001c5c090  NSObject
objc[41194]: [0x7fe8e9815068]  ################  POOL 0x7fe8e9815068
objc[41194]: [0x7fe8e9815070]    0x600001c5c0a0  NSObject
objc[41194]: [0x7fe8e9815078]    0x600001c5c0b0  NSObject
objc[41194]: ##############

可以看到,线程里维护了一个自动释放池的堆栈。

  • 第一个创建的 pool 为最外层释放池,当前只新建了一页 page
  • 嵌套创建的子 pool 在内存结构上来看,按顺序存于最外层释放池的 page 中。
超出作用域时打印
//超出pool3
objc[41194]: ##############
objc[41194]: AUTORELEASE POOLS for thread 0x114550dc0
objc[41194]: 6 releases pending.
objc[41194]: [0x7fe8e9815000]  ................  PAGE  (hot) (cold)
objc[41194]: [0x7fe8e9815038]  ################  POOL 0x7fe8e9815038
objc[41194]: [0x7fe8e9815040]    0x600001c5c060  NSObject
objc[41194]: [0x7fe8e9815048]  ################  POOL 0x7fe8e9815048
objc[41194]: [0x7fe8e9815050]    0x600001c5c070  NSObject
objc[41194]: [0x7fe8e9815058]    0x600001c5c080  NSObject
objc[41194]: [0x7fe8e9815060]    0x600001c5c090  NSObject
objc[41194]: ##############
//超出pool2
objc[41194]: ##############
objc[41194]: AUTORELEASE POOLS for thread 0x114550dc0
objc[41194]: 2 releases pending.
objc[41194]: [0x7fe8e9815000]  ................  PAGE  (hot) (cold)
objc[41194]: [0x7fe8e9815038]  ################  POOL 0x7fe8e9815038
objc[41194]: [0x7fe8e9815040]    0x600001c5c060  NSObject
objc[41194]: ##############
//超出pool1
objc[41194]: ##############
objc[41194]: AUTORELEASE POOLS for thread 0x114550dc0
objc[41194]: 0 releases pending.
objc[41194]: [0x7fe8e9815000]  ................  PAGE  (hot) (cold)
objc[41194]: ##############

从三次打印结果可知,虽然子 pool 按顺序存在最外层释放池的 page 中,但每个 pool 还是各自管理自己的自动释放对象。

  • 当超出子 pool 的作用域时,子 pool 会被销毁,里面的对象也都会被释放。
  • 当超出最外层 pool 的作用域时,里面的对象会被释放,但释放池不会被销毁,会保留一个空白的 page
  • 每个线程都会维护自身的自动释放池堆栈,只有当线程被销毁时,最外层 pool 才会被销毁。

相关文章

网友评论

      本文标题:iOS 内存管理(五)-AutoreleasePool

      本文链接:https://www.haomeiwen.com/subject/gcsvfrtx.html