iOS-自动释放池AutoreleasePool

2,642 阅读10分钟

引言

首先来看如下代码,思考一下输出以及原因。

// DemoObject
@interface DemoObject : NSObject
@end

@implementation DemoObject

+ (DemoObject*)object {
    DemoObject *object = [[DemoObject alloc]init];
    return object;
}

@end

// AutoreleasePoolDemoVC
@interface AutoreleasePoolDemoVC ()
@end

@implementation AutoreleasePoolDemoVC

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak id object1;
    {
        object1 = [DemoObject object];
    }
    NSLog(@"object1 = %@", object1);
    
    __weak id object2;
    {
        object2 = [[DemoObject alloc]init];
    }
    NSLog(@"object2 = %@", object2);
}

输出如下:

 object1 = <DemoObject: 0x2811d0370>
 object2 = (null)

对比上述代码可以发现除了获取DemoObject实例的方法不一样其他的一模模一样样,问题应该就在这里了。其实对于第二种方式系统也已经给出了提示: 那么为什么object1没有被释放呢? 原因就是我们今天需要分析的这个AutoreleasePool自动释放池了。

自动释放池AutoreleasePool简介

以下摘自苹果官方文档:

在引用计数(相对于垃圾回收)环境中,自动释放池包含了接受autorelease消息的对象, 当池子倾倒的时候,会对里面的每一个对象发送release消息。因此对一个对象发送autorelease消息而不是release消息,可以增加对象的生命周期直到它所在的AutoreleasePool倒掉

在引用计数环境中,Cocoa期望有一个有效的AutoreleasePool,否则的话,autorelease对象不会被释放进而导致内存泄漏。 在上述代码中,我们可以看到pdealloc方法没有被调用。同时我们利用_objc_autoreleasePoolPrint打印当前自动释放池中的内容,发现了确实存在p确实在自动释放池中,但是因为自动释放池并没有触发pop操作,因此p无法调用release方法,造成了内存泄漏,不过在上述情况中,执行完代码程序直接就退出了,应该也没啥问题,仅供实例参考。

此处还有一个问题,我们并没有创建自动释放池,但是打印的结果是有的,这是什么时候创建的呢?在后面的分析中会说明。

源码分析

重写cpp文件

想要了解自动释放池的底层实现,我们需要利用到clang指令

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

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

我们利用如下指令对上述文件进行重写:

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

最后得到一个cpp文件,只摘出有用的部分如下:

// main
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

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

可以看到重写之后的文件中,@autoreleasepool变成了__AtAutoreleasePool结构体类型的变量,该结构体的初始化方法和析构方法如上图所示,其实就相当于在我们的代码段的开始和结束插入了一下两行代码:

atautoreleasepoolobj = objc_autoreleasePoolPush();

// 自己的代码
...

objc_autoreleasePoolPop(atautoreleasepoolobj);

很显然,这两行代码是内存释放池的核心所在,接下来我们就根据源码来一探究竟。 自动释放池源码位于libobjc中,当前版本为objc4-781.2

objc_autoreleasePoolPush

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

可以看到其实是调用了AutoreleasePoolPagepush函数。自动释放池应该是跟这个AutoreleasePoolPage有着千丝万缕的关系。因此我们先来看一下AutoreleasePoolPage的结构

AutoreleasePoolPage

struct AutoreleasePoolPageData
{
	magic_t const magic; // 16
	__unsafe_unretained id *next; // 8
	pthread_t const thread; // 8
	AutoreleasePoolPage * const parent; // 8
	AutoreleasePoolPage *child; // 8
	uint32_t const depth; // 4
	uint32_t hiwat; // 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)
	{
	}
};

class AutoreleasePoolPage : private AutoreleasePoolPageData {
    id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }

    id * end() {
        return (id *) ((uint8_t *)this+SIZE);
    }

    bool empty() {
        return next == begin();
    }

    bool full() { 
        return next == end();
    }
    
    static void * operator new(size_t size) {
        return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
    }
    ...
    #define PAGE_SIZE               I386_PGBYTES
    #define I386_PGBYTES            4096
}

结构如下图所示: 可以看出AutoreleasePoolPage是一个双向链表结构,同时内部是一个栈结构。虽然AutoreleasePoolPage内部只有7个变量,一共占了56个字节的大小,但是重写了new方法,每一次都会开辟4096(4k)大小的空间 几个核心变量如下:

  • next id*类型,指向当前page中的下一个可插入位置。
  • thread 当前page所在的线程
  • parent 前一个page
  • child 后一个page
  • depth 深度,代表当前page在整个双向链表中的位置

在大致了解了AutoreleasePoolPage的结构之后,我们继续来看push函数

static inline void *push() 
    {
        id *dest;
        if (slowpath(DebugPoolAllocation)) {
            // 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);
        return dest;
    }
    
#   define POOL_BOUNDARY nil

其中的POOL_BOUNDARY可以理解是一个哨兵对象,为nil。

autoreleaseFast

我们直接来看autoreleaseFast的实现

static inline id *autoreleaseFast(id obj)
    {	
    	// 获取当前的hotpage
        AutoreleasePoolPage *page = hotPage();
        // page存在并且没满
        if (page && !page->full()) {
            return page->add(obj);
        // page满了
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
        // page不存在
            return autoreleaseNoPage(obj);
        }
    }

首先从当前的线程本地存储tls中取出当前的hotPage,其中tls在之前的文章多线程@synchronized锁中有提到过。 接下来分为三种情况来插入obj,在我们当前的流程中,插入的obj为哨兵对象POOL_BOUNDARY

  1. hotpage存在并且没有满
  2. hotpage满了
  3. hotpage不存在 接下来分情况分析

1. hotpage存在并且没有满

此时直接往hotpage中插入即可

id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        // 记录需要插入的位置
        id *ret = next;  // faster than `return next-1` because of aliasing
        // 在对应的位置插入obj,然后自增1
        *next++ = obj;
        protect();
        // 返回,此时ret存放的就是obj的指针
        return ret;
    }

2. hotpage满了

根据上文中的full()函数,当前next指向当前pageend时,当前page处于满状态,此时需要开辟新的page来存放obj

autoreleaseFullPage

static __attribute__((noinline))
    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {
        // The hot page is full.  当前的hotpage满了
        // Step to the next non-full page, adding a new page if necessary. 步入下一个不满的page,有需要的话添加一个新的page
        // Then add the object to that page.接下来将obj插入到新的page
        ASSERT(page == hotPage());
        ASSERT(page->full()  ||  DebugPoolAllocation);

        do {
            // 沿着链表查找下一个不满的page
            if (page->child) page = page->child;
            // 找到链接的最后一页也满了,此时需要创建新的page
            else page = new AutoreleasePoolPage(page);
        } while (page->full());
		
        setHotPage(page);
        return page->add(obj);
    }

AutoreleasePoolPage

AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
		AutoreleasePoolPageData(begin(),
								objc_thread_self(),
								newParent,
								newParent ? 1+newParent->depth : 0,
								newParent ? newParent->hiwat : 0)
    { 
        if (parent) {
            parent->check();
            ASSERT(!parent->child);
            parent->unprotect();
            parent->child = this;
            parent->protect();
        }
        protect();
    }

主要就是初始化新的page并且建立parent和当前page的关系。

初始化完毕之后需要将当前page设置为hotpage,具体操作就是存入tls,然后调用add方法添加object

3. hotpage不存在

static __attribute__((noinline))
    id *autoreleaseNoPage(id obj)
    {
    	// 此处省略无关代码
        ...
        // Install the first page.
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        ...
        // Push the requested object or pool.
        return page->add(obj);
    }

如果tls中不存在hotpage,应该是自动释放池第一次添加object。此时的page需要新创建,而且也没有父结点,同时还需要设置hotpage

至此,objc_autoreleasePoolPush函数分析完了,总结一下,当前场景下主要的操作是:新生成一个AutoreleasePoolPage并设置为hotpage,将哨兵对象POOL_BOUNDARY插入到当前的hotpage中,返回插入的object即哨兵对象在自动释放池中的位置。

autorelease

我们知道自动释放池push()时会会插入哨兵对象,那么在文章最开始的实例代码中object1是怎么插入的呢?如图位置打下断点: 在xcode中查看汇编代码如下: 可以看到[DemoObject object]方法最终调用了objc_autoreleaseReturnValue方法。源码如下:

// Prepare a value at +1 for return through a +0 autoreleasing convention.
id 
objc_autoreleaseReturnValue(id obj)
{
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

    return objc_autorelease(obj);
}

objc_autorelease()->objc_object::autorelease()->rootAutorelease()->objc_object::rootAutorelease2()->AutoreleasePoolPage::autorelease()

最后的最后调用了autoreleaseFast方法,将我们的object插入到了自动释放池中。

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

这里也解释了文章开头,我们没有显示调用push方法,但是自动释放池已经存在。因为autorelease调用了autoreleaseFast函数,参数为需要延迟释放的对象。而objc_autoreleasePoolPush本质是也是调用了autoreleaseFast函数,只不过参数为哨兵对象POOL_BOUNDARY

objc_autoreleasePoolPop

objc_autoreleasePoolPop(atautoreleasepoolobj);

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

static inline void
    pop(void *token)
    {
        AutoreleasePoolPage *page;
        id *stop;
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        	// 忽略无关代码
            ...
        } else {
            page = pageForPointer(token);
        }
        stop = (id *)token;
        // 忽略无关代码
        ...
        return popPage<false>(token, page, stop);
    }

pop函数需要找到当前token所在的page,然后执行popPage方法。

pageForPointer

static AutoreleasePoolPage *pageForPointer(const void *p) 
    {
        return pageForPointer((uintptr_t)p);
    }

    static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
    {
        AutoreleasePoolPage *result;
        uintptr_t offset = p % SIZE;

        ASSERT(offset >= sizeof(AutoreleasePoolPage));

        result = (AutoreleasePoolPage *)(p - offset);
        result->fastcheck();

        return result;
    }

p为当前token的位置,p对SIZE求余可以的得到在当前page中的相对位置,p - offset得到当前page的起始位置,即找到token所在的page

popPage

template<bool allowDebug>
    static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
        ...

        page->releaseUntil(stop);

	...
        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();
            }
        }
    }
  • 释放page直到找到stop标记。
  • 根据当前page的容量来进行对应的操作。

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
        
        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;
                setHotPage(page);
            }

            page->unprotect();
            // page->next代表下一个需要插入的位置,此时是没有值的,但是上一个8字节地址是有值的,根据取值符*可以拿到对应的obj指针
            id obj = *--page->next;
            // 将page中next指向的位置改为SCRIBBLE
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }

        setHotPage(this);
		...
    }

以上图为例,此时的hotpagepageN,需要poptokenPOOL_BOUNDARY。根据上述函数的描述操作如下:

1. 找到hotpage,即pageN
2. 校验pageN是否为空,否则循环寻找pageN的父结点并赋值为hotpage
3. 从hotpage的next减8字节,即page中最后一个autorelease对象开始,依次执行release操作,同时将page中的当前位置赋值为`SCRIBBLE`
4. 循环执行1-3知道当前page的next指向token,如果token不为POOL_BOUNDARY,也已经执行了release操作
5. 当前page设置为hotpage

操作完毕后,page1的结构如下: 接下来我们通过代码来验证一下:

其中:

  • 0x7ff23e81e000为当前的AutoreleasePoolPage的起始地址,读取0x7ff23e81e000的内存可以看到更多的数据

  • 0x7ff23e81e048为当前的page->next指向的位置

  • 0x10e991dc0为当前page所在的线程

  • 因为当前的自动释放池只有两个对象,没有父结点,也没有子结点,对应的depth和hiwat都为0,因此0x7ff23e81e0200x7ff23e81e030的数据都是0

  • 0x7ff23e81e038为插入push的时候插入的哨兵对象POOL_BOUNDARY在当前page中的地址,可以看到和起始地址相差0x38也就是56,和AutoreleasePoolPage结构中的变量大小一致,POOL_BOUNDARY的本质也就是nil,所以也是0

  • 0x6000006145a0object的地址

kill

接着上文中的popPage函数,针对page的不同容量做了不同的处理。

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

参考

黑幕背后的Autorelease

自动释放池的前世今生 ---- 深入解析 autoreleasepool