天天看点

iOS内存管理之autorelease

当你需要延迟调用release方法的时候会使用autorelease。如:

- (NSString *)fullName {
    NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@",
                                          self.firstName, self.lastName] autorelease];
    return string;
}
           

在上面代码中,通过alloc生成并持有对象,根据iOS内存管理规则,在失去对象的引用之前,我们必须要放弃该对象的所有权。如果使用release,那么对象在返回之前就会失去所有权,导致返回一个无用的对象。使用autorelease,会保证调用放在使用该返回值之前,该对象不会释放,也就是说,autorelease可以保证对象在跨越“方法调用边界”后存活。

这时你可能会有一个疑问,使用autorelease的对象到底在什么时候释放呢?接下来,我们将会围绕这个问题逐步进行研究。

AutoreleasePool是如何工作的?

Coco框架会帮我们维护一个autorelease pool用来将对象临时保存在内存中,以便稍后释放。

iOS应用程序是在event loop(事件循环)中运行的。当应用程序加载时,程序就会进入事件循环并等待用户的交互事件。当一个点击事件发生时,Cocoa框架会检测到该事件,然后创建事件对象,并生成一个autorelease pool对象。整个流程如下表所示:

iOS内存管理之autorelease

当事件处理方法完成执行时,控制权会返回给Cocoa框架。Cocoa框架会销毁autorelease Pool,它会向池子中的每一个对象都发送一条release消息。

autorelease实现

通过查看Objc源码,来确认autorlease的实现:

- (id)autorelease {
    return ((id)self)->rootAutorelease();
}

inline id objc_object::rootAutorelease()
{
    assert(!UseGC);

    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

id objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}
           

通过源码可以发现,当调用对象的autorelease方法时,最终调用的是AutoreleasePoolPage的autorelease方法。

接下来我们看一下AutoreleasePoolPage的实现,

AutoreleasePoolPage是使用C++实现的一个类,截取源码中的主要部分,其实现如下:

class AutoreleasePoolPage 
{
#define POOL_SENTINEL nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);
    
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
    
    public:
    static inline id autorelease(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }


    static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_SENTINEL);
        } else {
            dest = autoreleaseFast(POOL_SENTINEL);
        }
        assert(*dest == POOL_SENTINEL);
        return dest;
    }

    static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;

        page = pageForPointer(token);
        stop = (id *)token;
        if (DebugPoolAllocation  &&  *stop != POOL_SENTINEL) {
            // This check is not valid with DebugPoolAllocation off
            // after an autorelease with a pool page but no pool in place.
            _objc_fatal("invalid or prematurely-freed autorelease pool %p; ", 
                        token);
        }

        if (PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);

        // memory: delete empty children
        if (DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (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();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }
    
    id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;
        *next++ = obj;
        protect();
        return ret;
    }

    void releaseAll() 
    {
        releaseUntil(begin());
    }
}
           

通过分析上面的源码,我们可以了解到对象调用autorelease方法的过程如下:

  • 获取正在使用的AutoreleasePoolPage对象。
  • 获取当前AutoreleasePoolPage对象的next指针,将autorelease对象地址复制给next指针,next指针向上移动一个位置。
  • 若一个AutoreleasePoolPage空间被占满时,会新建一个AutoreleasePoolPage对象,链接链表,后来的autorelease对象会加入到新的page中。

AutoreleasePool的生命周期

在ARC下,我们使用@autoreleasepool{}来使用AutoreleasePool:

@autoreleasepool {
}
           

使用clang(clang -rewrite-objc)命令将其转化成C++代码,关键代码如下:

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

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

因此我们可以得出结论:Autorelease Pool的生命周期是由 void * objc_autoreleasePoolPush(void)和void objc_autoreleasePoolPop(void *)这两个方法实现的。

通过查看Objc源码,确定这两个方法的实现:

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

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

通过源码,我们可以确定,void * objc_autoreleasePoolPush(void)和 void objc_autoreleasePoolPop(void *)这两个方法是对AutoreleasePoolPage::push()和AutoreleasePoolPage::pop(ctxt)方法的封装。

通过分析AutoreleasePoolPage源码中push()和pop()方法的实现,我们可以知道AutoreleasePool对象的生命周期

push()过程:

  • 获取当前正在使用的AutoreleasePoolPage对象。若当前对象为空,则生成一个新的page对象。若当前page存在且已满,则创建一个新的page,通过链表的形式将两个page链接起来。
  • 向当前page对象中next位置追加一个哨兵对象(POOL_SENTINEL)并返回该地址,这个地址作为pop操作的入参。

注意:每调用一次push()操作都会创建一个新的autoreleasepool对象,即往AutoreleasePoolPage中插入一个POOL_SENTINEL,并且返回插入的 POOL_SENTINEL 的内存地址。

pop()过程:

  • 根据传入的哨兵对象地址找到其所在的page。
  • 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次release消息,并将next指针移回正确位置。
  • 从最新加入的对象一直向前清理,可以跨越若干个page,直到哨兵对象所在的位置。
    iOS内存管理之autorelease

autorelease对象在什么时候释放呢?

通过前面的分析,我们可以知道,autorelease对象是在AutoReleasePool的生命周期结束时释放的。在没有显示生成AutoReleasePool对象的情况下,autorelease对象在当前runloop迭代结束时释放的,Cocoa框架会为每个runloop添加AutoreleasePool的push和pop操作。

ARC下autorelease优化策略

先看一下,ARC下,调用NSMutableArray类的array方法时,编译器为我们做了哪些操作

+ (id)array {
    id obj = objc_msgSend(NSMutableArray, @selector(alloc));
    objc_msgSend(obj, @selector(init));
    return objc_autoreleaseReturnValue(obj);
}

id obj = objc_retainAutoreleasedReturnValue([NSMutableArray array]);
           

objc_autoreleaseReturnValue函数会检查使用该函数的方法或函数调用方的执行命令列表,如果方法或函数的调用方在调用了方法或函数后紧接着调用objc_retainAutoreleasedReturnValue()函数,那么就不将返回的对象注册到autoreleasepool中,而是直接传递到方法或函数的调用方。

iOS内存管理之autorelease

拾遗

使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 这里被一个局部@autoreleasepool包围着
}];
           

在普通for循环和for in循环中没有。for循环和for in循环中遍历产生大量autorelease变量时,就需要手加局部 AutoreleasePool啦。

扫一扫关注iOS学习社区公号,了解更多iOS知识以及提升工作技能

iOS内存管理之autorelease