iOS 内存管理(四): 自动释放池详解

1,498 阅读9分钟

准备

一、Autorelease简介

AppKitUIKit框架在事件循环(RunLoop)的每次循环开始时,在主线程创建一个自动释放池,并在每次循环结束时销毁它,在销毁时释放自动释放池中的所有autorelease对象。
通常情况下我们不需要手动创建自动释放池,但是如果我们在循环中创建了很多临时的autorelease对象,则手动创建自动释放池来管理这些对象可以很大程度地减少内存峰值。

二、自动释放池 原理分析

2.1 @autoreleasepool{} 分析

先创建一个mac工程,main.m中代码如下:

int main(int argc, const char * argv[]) {
 
    @autoreleasepool {
        
    }
    return 0;
}

执行clang -rewrite-objc main.m -o main.cpp命令得到main.cpp文件:

int main(int argc, const char * argv[]) {

    /* @autoreleasepool */ { 
        __AtAutoreleasePool __autoreleasepool; 
    }
    return 0;
}
  • 代码被转化为C++源码以后,@autoreleasePool块其实是__AtAutoreleasePool结构体的创建,创建时会调用构造函数,作用域结束时会调用析构函数

再看下__AtAutoreleasePool结构体:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};
  • 构造函数会调用objc_autoreleasePoolPush函数,并返回边界对象atautoreleasepoolobj
  • 析构函数会调用objc_autoreleasePoolPop函数,并传入边界对象atautoreleasepoolobj

所以,我们可以将main函数的代码简化如下:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

2.2 objc_autoreleasePoolPush与objc_autoreleasePoolPop

打开objc4-818.2 源码 ,查看objc_autoreleasePoolPushobjc_autoreleasePoolPop函数:

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

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

2.3 AutoreleasePoolPage 结构分析

AutoreleasePoolPage是一个C++类,打开objc源码,可以看到它继承自AutoreleasePoolPageData,结构如下:

class AutoreleasePoolPage : private AutoreleasePoolPageData 
{
public:
    static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MIN_SIZE;  // 4096 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;
    
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)  //空池占位
#   define POOL_BOUNDARY nil //边界对象(即哨兵对象)
    ...
}

struct AutoreleasePoolPageData
{
    magic_t const magic; // 用来校验 AutoreleasePoolPage 的结构是否完整 16字节
    __unsafe_unretained id *next; // 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向begin() 8字节
    pthread_t const thread; // 指向当前线程 8字节
    AutoreleasePoolPage * const parent; // 指向父结点,第一个结点的parent值为nil 8字节
    AutoreleasePoolPage *child; // 指向子结点,最后一个结点的child值为nil 8字节
    uint32_t const depth; // 代表深度,从0开始,往后递增1 4字节
    uint32_t hiwat;  // 代表high water mark 最大入栈数量标记 4字节
    
    // 构造函数
    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)
    {
    }
};
  • 自动释放池是由若干个AutoreleasePoolPage组成的双向链表结构,AutoreleasePoolPage中拥有parentchild指针,分别指向上一个和下一个page
  • 当前一个page的空间被占满(每个AutorelePoolPage的大小为4096字节)时,就会新建一个AutorelePoolPage对象并连接到链表中,后来的 Autorelease对象也会添加到新的page中。
  • 另外,当next== begin()时,表示AutoreleasePoolPage为空;当next == end(),表示AutoreleasePoolPage已满。

为什么多页?

  1. 操作过程需要加锁解锁,如果所有页面都在一页,操作非常复杂,一个对象进行操作其他的都得等待。
  2. 已满的页面都不进行操作了,只对没满的那个进行操作,效率比较高。
  3. 不用非得是一片连续的内存。

2.4 AutoreleasePoolPage 空间大小

上面的AutoreleasePoolPage结构中,我们看到结构体空间大小为PAGE_MIN_SIZE

#define PAGE_MIN_SHIFT          12
#define PAGE_MIN_SIZE           (1 << PAGE_MIN_SHIFT)
  • 1 << 12等于4096,也就是4k。

下面进行验证:

image.png

  • 示例中一共有505个对象,添加了504个以后又重新创建了一个hot page,说明一个page最多只能添加504个对象。
  • 504*8 + 56(成员变量大小) + 8(边界对象) = 4096,和上面说的大小一致。

2.5 哨兵对象/边界对象(POOL_BOUNDARY)

上面的AutoreleasePoolPage结构中,我们看到了有一个边界对象(哨兵对象):POOL_BOUNDARY

  • 边界对象其实就是nil的别名,而它的作用事实上也就是为了起到一个标识的作用。
  • 每当调用objc_autoreleasePoolPush方法时,会将POOL_BOUNDARY放到当前page的栈顶,并且返回这个边界对象。
  • 而在调用objc_autoreleasePoolPop方法时,又会将边界对象以参数传入,这样自动释放池就会向释放池中对象发送release消息,直至找到第一个边界对象为止。

验证

苹果提供了一个调试函数:_objc_autoreleasePoolPrint,可以用来打印自动释放池的创建信息,看下面的测试代码:

extern void _objc_autoreleasePoolPrint(void); // 需要用extern修饰才能在外部访问

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

查看打印结果:

2021-09-20 18:19:42.833783+0800 KCObjcBuild[10265:184146] objc:<NSObject: 0x10127ea90>
objc[10265]: ##############
objc[10265]: AUTORELEASE POOLS for thread 0x1000ebe00
objc[10265]: 2 releases pending.
objc[10265]: [0x10200b000]  ................  PAGE  (hot) (cold)
objc[10265]: [0x10200b038]  ################  POOL 0x10200b038
objc[10265]: [0x10200b040]       0x10127ea90  NSObject
objc[10265]: ##############
  • 可以看到自动释放池添加的对象被打印了出来,其中x10127ea90 NSObject是我们创建的objc对象,还有一个POOL 0x10200b038地址就是哨兵对象。

2.6 objc_autoreleasePoolPush

经过前面的分析,objc_autoreleasePoolPush最终调用的是 AutoreleasePoolPagepush方法,该方法的具体实现如下:

static inline void *push() 
{
    return autoreleaseFast(POOL_BOUNDARY);
}

static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        return page->add(obj); // 没满进行添加操作
    } else if (page) {
        return autoreleaseFullPage(obj, page);// 满了走这个流程
    } else {
        return autoreleaseNoPage(obj);// 没有page,走这个流程
    }
}

id *add(id obj)
{
    id *ret;
    ret = next;  // faster than `return next-1` because of aliasing
    *next++ = obj;
}

static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    do {
        if (page->child) page = page->child; // 无限递归找到最后一个
        else page = new AutoreleasePoolPage(page); // 新建page
    } while (page->full());

    setHotPage(page); // 设置为hotPage
    return page->add(obj);
}

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
    {
    // 通过构造函数新建page
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page); // 设置为hotPage

    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY); // 先添加哨兵
    }

    // 再添加obj
    return page->add(obj);
}

看上面代码,push函数中调用了autoreleaseFast函数,函数中又分三种情况:

  • 当前page存在且不满,调用page->add(obj)方法将对象添加至page的栈中,即next指向的位置。
  • 当前page存在但是已满,调用autoreleaseFullPage初始化一个新的page,调用page->add(obj)方法将对象添加至page的栈中。
  • 当前page不存在时,调用autoreleaseNoPage创建一个hotPage,先将边界对象(POOL_BOUNDARY)添加至page的栈中,再调用page->add(obj) 方法将对象添加至page的栈中。

再看下AutoreleasePoolPage的构造函数:

AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
		AutoreleasePoolPageData(begin(),
				        objc_thread_self(), // 当前线程
					newParent,
					newParent ? 1+newParent->depth : 0, // +1操作
					newParent ? newParent->hiwat : 0)
{
    if (objc::PageCountWarning != -1) {
        checkTooMuchAutorelease();
    }

    if (parent) {
        parent->check();
        ASSERT(!parent->child);
        parent->unprotect();
        parent->child = this; // 给父类的子类赋值本身
        parent->protect();
    }
    protect();
}

id * begin() {
    // 从成员变量下面开始添加
    return (id *) ((uint8_t *)this+sizeof(*this));
}

2.7 objc_autoreleasePoolPop

objc_autoreleasePoolPop最终调用的是 AutoreleasePoolPagepop方法,并传入边界对象(POOL_BOUNDARY),该方法的具体实现如下:

static inline void
pop(void *token) // POOL_BOUNDARY的地址
{
    AutoreleasePoolPage *page;
    id *stop;
    page = pageForPointer(token);// 通过POOL_BOUNDARY找到对应的page
    stop = (id *)token;
    
    return popPage<false>(token, page, stop);
}

template<bool allowDebug>
static void popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    page->releaseUntil(stop); // 向栈中的对象发送release消息,直到遇到第一个哨兵对象

    // memory: delete empty children
    // 删除空掉的节点
    if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
        AutoreleasePoolPage *parent = page->parent; // 拿到parent
        page->kill(); // 本身杀掉
        setHotPage(parent); // 把parent设置为hotPage
    } else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        page->kill();
        setHotPage(nil);
    } else if (page->child) {
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

void releaseUntil(id *stop) 
{
    while (this->next != stop) {
        
        AutoreleasePoolPage *page = hotPage();

        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));

        if (obj != POOL_BOUNDARY) {
            for (int i = 0; i < count + 1; i++) {
                objc_release(obj);
            }
        }
    }

    setHotPage(this);
}

void kill() 
{
    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);
}

上述代码中,先根据传入的边界对象地址找到边界对象所处的page,然后选择当前page中最新加入的对象一直向前清理,直到边界所在的位置;清理的方式是向这些对象发送一次release消息,使其引用计数减一;

另外,清空page对象还会遵循一些原则:

  1. 如果当前的page中存放的对象少于一半,则子page全部删除;
  2. 如果当前的page存放的多余一半(意味着马上将要满),则保留一个子page,节省创建新page的开销;

2.8 自动释放池可嵌套 验证

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[[NSObject alloc] init] autorelease];
        NSLog(@"objc:%@",objc);

        @autoreleasepool {
            NSObject *objc2 = [[[NSObject alloc] init] autorelease];
            NSLog(@"objc2:%@",objc2);

            _objc_autoreleasePoolPrint();
        }
    }
    return 0;
}
2021-09-20 21:54:23.661658+0800 KCObjcBuild[16800:319949] objc:<NSObject: 0x101a58ab0>
2021-09-20 21:54:23.663090+0800 KCObjcBuild[16800:319949] objc2:<NSObject: 0x1006282b0>
objc[16800]: ##############
objc[16800]: AUTORELEASE POOLS for thread 0x1000ebe00
objc[16800]: 4 releases pending.
objc[16800]: [0x102016000]  ................  PAGE  (hot) (cold)
objc[16800]: [0x102016038]  ################  POOL 0x102016038
objc[16800]: [0x102016040]       0x101a58ab0  NSObject
objc[16800]: [0x102016048]  ################  POOL 0x102016048
objc[16800]: [0x102016050]       0x1006282b0  NSObject
objc[16800]: ##############
  • 根据打印结果可以看到,自动释放池依次添加了0x102016038边界对象 -> objc对象 -> 0x102016048边界对象 -> objc2对象。

三、autorelease 对象在什么时候释放?

autorelease对象的释放有系统干预释放手动干预释放两种情况。

  • 系统干预释放是不指定@autoreleasepool,所有autorelease对象都由主线程的RunLoop创建的@autoreleasepool来管理。
  • 手动干预释放就是将autorelease对象添加进我们手动创建的@autoreleasepool中。

3.1 系统干预释放

我们先创建一个iOS工程,示例代码如下,并查看运行结果:

@implementation SSLPerson

- (void)dealloc
{
    NSLog(@"%s", __func__);
}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    SSLPerson *person = [[[SSLPerson alloc] init] autorelease];
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    NSLog(@"%s", __func__);
}
@end

运行结果:
-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[SSLPerson dealloc]
-[ViewController viewDidAppear:]
  • 可以看到,调用了autorelease方法的person对象并没有在viewDidLoad方法结束后释放,而是在viewWillAppear方法结束后才释放,说明在viewWillAppear方法结束的时候,调用了pop()方法释放了person对象。其实这是由RunLoop控制的,下面来讲解一下RunLoop@autoreleasepool的关系。

3.2 RunLoop 与 @autoreleasepool

RunLoop中对自动释放池的操作可以用下图来表示:

11742578-8c8f92dadbbfc5ff.png

  • kCFRunLoopEntry:在即将进入RunLoop时,会自动创建一个__AtAutoreleasePool结构体对象,并调用objc_autoreleasePoolPush()函数。
  • kCFRunLoopBeforeWaiting:在RunLoop即将休眠时,会自动销毁一个__AtAutoreleasePool对象,调用objc_autoreleasePoolPop()。然后创建一个新的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPush()
  • kCFRunLoopBeforeExit,在即将退出RunLoop时,会自动销毁最后一个创建的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPop()

所以,在iOS工程中系统干预释放的autorelease对象的释放时机是由RunLoop控制的,会在当前RunLoop每次循环结束时释放。以上person对象在viewWillAppear方法结束后释放,说明viewDidLoadviewWillAppear方法在同一次循环里。

3.3 手动干预释放

再来看一下手动干预释放的情况:

@implementation SSLPerson

- (void)dealloc
{
    NSLog(@"%s", __func__);
}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    @autoreleasepool {
        SSLPerson *person = [[[SSLPerson alloc] init] autorelease];
    }
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    NSLog(@"%s", __func__);
}
@end

运行结果:
-[SSLPerson dealloc]
-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[ViewController viewDidAppear:]

可以看到,手动添加到指定的@autoreleasepool中的autorelease对象,在@autoreleasepool大括号结束时就会释放了,不受RunLoop控制。