自动释放池AutoreleasePool的探究

2020/5/6 23:26:43

本文主要是介绍自动释放池AutoreleasePool的探究,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、autoreleasepool概念

autoreleasepool本质是自动延迟对象的释放,即对象使用完之后,它不会立即释放,而是加入到释放池,等到某个合适的时刻,对释放池中的对象进行统一释放。

官方文档对主线程的自动释放池有这么一段描述:

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

二、ARCMRCautoreleasepool的区别

MRC下需要手动管理自动释放池的创建和释放,MRC下只需要使用@autoreleasepool将对应的代码包含起来即可。

- (void)MRCTest {

    Person *person;
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    person = [[Person alloc] initWithName:@"jam" age:24];
    [person autorelease];
    NSLog(@"before pool release person: %@", person);
    [pool release];
    NSLog(@"after pool release person: %@", person); //crash 
}

输出结果:
before pool release person: name:jam, age:24
crash ...

- (void)ARCTest {
    Person *person;
    @autoreleasepool {
    person = [[Person alloc] initWithName:@"jam" age:24];
    NSLog(@"before end release pool person: %@", person);
    }
    NSLog(@"after end release pool person: %@", person);
}
输出结果:
before end release pool person: name:jam, age:24
after end release pool person: name:jam, age:24
复制代码

根据日志输出得知:MRC下调用自动释放池release方法后,会对在autorelease对象进行释放,因此,此后访问的person变量为野指针,再去访问自然会导致crash。而ARC下,@autoreleasepool并不会立即在结束括号符后,立即释放person变量,而是会在一个合适的时间点。具体是在什么时候,下面会讲解到。

ps:x-code下对特定文件设置使用MRC的方式:-fno-objc-arc

三、autoreleasepoolrunloop的关系

在断点调试中,使用po [NSRunLoop currentLoop]

由上图可知:自动释放池在runloop中注册了两个observer,分别都会以_wrapRunLoopWithAutoreleasePoolHandler进行回调。不过两个observer中的activitiesorder有些不同。

a. 首先看activities的区别:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
复制代码

第一个observeractivities0x01,即kCFRunLoopEntry,第二个observeractivities0xa0(转换为二进制为10100000),即kCFRunLoopBeforeWaiting | kCFRunLoopExit

b. 两者order的区别,这里的order表示的是runloop执行事件的优先级。

order = -2147483647
order = 2147483647

int32 max: 2147483647
int32 min: -2147483648
复制代码

根据上面activitiesorder的对比,得知:

第一个observerrunloop监听kCFRunLoopEntry时的优先级为-2147483647(优先级最高),即保证该observer回调会发生在其他事件回调之前。

第二个observerrunloop监听kCFRunLoopBeforeWaiting | kCFRunLoopExit时的优先级为2147483647,即保证该observer回调会发生在其他事件回调之后

这两个observer分别在回调时对自动释放池进行了什么操作呢?我们通过一个小例子来看看

Person *p;
//此处打断点
p = [[Person alloc] initWithName:@"jam" age:24];
NSLog(@"p: %@", p);
复制代码

我们先在声明临时变量p处设置一个断点,然后使用watchpoint set variable p命令监测变量p的变化,然后继续运行程序,会不断触发到断点,其中会在某个时刻分别显示这么两段内容:

CoreFoundation`objc_autoreleasePoolPush:
-> 0x107e6a2fc <+0>: jmpq *0x1e88d6(%rip) ; (void *)0x000000010a9bd50f: objc_autoreleasePoolPush

CoreFoundation`objc_autoreleasePoolPop:
-> 0x107e6a2f6 <+0>: jmpq *0x1e88d4(%rip) ; (void *)0x000000010a9bd5b3: objc_autoreleasePoolPop
复制代码

很明显这两段内容是跟自动释放池相关,分别对应释放池的pushpop操作,而这两个操作其实就是通过上面两个observer的回调之后的相关调用。(这两者的关联的确没有什么很好的证据证明,只能说是根据上面的例子推测而来)

因此,当runloop进入kCFRunLoopEntry时,自动释放池会进行push操作,当runloop进入kCFRunLoopBeforeWaiting | kCFRunLoopExit状态时,自动释放池会进行pop操作。即系统在每一个runloop迭代中都加入了自动释放池push和pop

四、@autoreleasepool的原理

通过使用clang编译器对main.m文件进行重新改写为cpp文件来一探究竟。

clang -rewrite-objc main.m
复制代码

运行后,发现会出错,提示fatal error: 'UIKit/UIKit.h' file not found,此时,可以通过下面的命令来解决:

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

其实这里主要是通过-isysroot选项指定了编译所使用的的SDK目录,即x-code下的SDK目录。

//.m
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
    // Setup code that might create autoreleased objects goes here.
    appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

//.cpp
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);
}
复制代码

可以看到,生成后的cpp文件中,新增了一个__AtAutoreleasePool结构体的变量

struct __AtAutoreleasePool {
    __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
    ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
    void * atautoreleasepoolobj;
};
复制代码

根据这个结构体的定义,可以看出在初始化时,会调用objc_autoreleasePoolPush()方法,在其析构函数,即该结构体实例销毁时,会调用objc_autoreleasePoolPop(atautoreleasepoolobj)方法。

五、objc_autoreleasePoolPushobjc_autoreleasePoolPop的原理

在上面runloop@autorelesepool的探究过程中,最后都会停留到这两个方法中,接下来,我们通过查看源码来探究下这两个方法具体做了哪些工作。(ps:可以在这里下载可编译的runtime源码)

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

NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
复制代码

根据上面的代码,可以看到pushpop操作分别调用了AutoreleasePoolPage的类方法。我们先看下AutoreleasePoolPage的定义:

class AutoreleasePoolPage : private AutoreleasePoolPageData
{...}

struct AutoreleasePoolPageData
{
magic_t const magic; //检查完整性的校验
__unsafe_unretained id *next; 
pthread_t const thread; //当前线程
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};
复制代码

这里比较值得关注的有:

a. parentchild变量构成双向链表

b. next变量作为指向新添加autorelease对象的下一个位置,用于以栈的形式存储

自动释放池数据结构如上所示:双链表+栈

了解完AutoreleasePoolPage的结构后,我们来分别细看下pushpop操作

push操作

static inline void *push() 
{
    id *dest;
    if (slowpath(DebugPoolAllocation)) { //debug模式下会直接生成一个新的page
    // 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
复制代码

这里会根据是否为debug模式,来进行不同的处理,这里可以暂时忽略debug模式下的处理,即调用autoreleaseFast方法,并传入一个nil对象,最后返回dest对象作为push方法的返回值。

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);
    }
}
复制代码

a. 首先它通过hotPage方法获取到当前的page,若page存在且空间未满,则将obj添加到page中。

b. 若page存在但空间已经满了,则需要新建一个子page来存储obj

c. 若page不存在,则创建一个新page来存储obj

  • 当前page的获取和存储(这里的当前page指的是AutoreleasePoolPage链表中当前所处于的节点page)
//获取page
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;
}

//设置page
static inline void setHotPage(AutoreleasePoolPage *page) 
{
    if (page) page->fastcheck();
    tls_set_direct(key, (void *)page);
}

//AutoreleasePoolPage声明内
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
复制代码

可以看到两者分别调用tls_get_directtls_set_direct方法对page分别进行读取和存储。

static inline void *tls_get_direct(tls_key_t k)
{ 
    ASSERT(is_valid_direct_key(k));
    if (_pthread_has_direct_tsd()) {
    return _pthread_getspecific_direct(k);
    } else {
    return pthread_getspecific(k);
    }
}

static inline void tls_set_direct(tls_key_t k, void *value) 
{ 
    ASSERT(is_valid_direct_key(k));
    if (_pthread_has_direct_tsd()) {
    _pthread_setspecific_direct(k, value);
    } else {
    pthread_setspecific(k, value);
    }
}
复制代码

这里使用了TLS(Thread Local Storage)线程局部变量进行存储,也就是说使用当前线程的局部存储空间对page进行存储,这样实现了线程和自动释放池的关联,不同线程的自动释放池也是独立的,互不干扰

  • 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);
    do {
    if (page->child) page = page->child;
    else page = new AutoreleasePoolPage(page);
    } while (page->full());
    setHotPage(page);
    return page->add(obj);
}

复制代码

如上,若当前page空间不足,则不断往后遍历,直到找到有空间的page,若找到最后也没有,则创建一个子page,并更新当前page节点,以便下一次可以直接添加(而不需要遍历查找)

  • page不存在的情况
static __attribute__((noinline))
    ....
    // Install the first page.
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    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);
}
复制代码

如上,page不存在的情况,会创建一个新page(作为链表的头部节点),并更新到TLS中。

  • add操作:不管上面哪种情况,最后都会调用add方法将对象添加到对应的page
id *add(id obj)
{
    ASSERT(!full());
    unprotect();
    id *ret = next; // faster than `return next-1` because of aliasing
    *next++ = obj;
    protect();
    return ret;
}
复制代码

上面提到过*next为新添加对象的位置,所以这里将*next的赋值为当前对象,并移动到下一个位置。

  • autoreleaseFast方法的调用

a. AutoreleasePoolPage:push方法,传入POOL_BOUNDARY(nil)对象

当调用push方法时,都会传入一个nil对象,作为“哨兵对象”,以便标识每次pushpop之间添加的对象区间,这样当执行pop操作时,就能准确释放对应的对象(直到“哨兵”位置)。

如上,当进行pop操作时,会将obj2-5的对象进行释放。

b. AutoreleasePoolPage:autorelease方法,传入实际的obj对象

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;
}
复制代码

在ARC下,编译器会在适当的位置插入autorelease方法。因此,会将对象自动添加到自动释放池中。

pop操作

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.
        page = coldPage();
        token = page->begin();
    } else {
        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. 这里传入的参数token为上面push操作返回的,即push操作后,返回的"哨兵"对象的指针。

  2. EMPTY_POOL_PLACEHOLDER是对只有1个pool情况下的优化,可以先不考虑该细节。

  3. 通过pageForPointer方法获取当前到page

  4. if (*stop != POOL_BOUNDARY),根据上面的第一点,可以知道,token应该为p操作完后,返回的“哨兵”对象,若不是,则进行异常处理。

  • 获取到“哨兵”对象所在的page
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;
}
复制代码

因为每一个page的大小是固定的,所以可以通过p % SIZE的方法获取到偏移量,然后通过p - offset获取到page的起始地址。

template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    if (allowDebug && PrintPoolHiwat) printHiwat();
    page->releaseUntil(stop);
    // memory: delete empty children
    if (allowDebug && DebugPoolAllocation && page->empty()) {
        // special case: delete everything during page-per-pool debugging
        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
        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();
        }
    }
}
复制代码

这里主要通过releaseUntil方法进行释放对象,释放后,会根据page的空间进行调整,前两个if判断都是debug模式下,可以先不用管,最后一个else if其实就是对剩余的空闲空间进行回收。

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();
    id obj = *--page->next;
    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
}
复制代码

这里用while循环从当前page的不断遍历,直到next指向了stop

  1. 获取到当前page,因为如果包含多个page,会顺着链表往前遍历page
  2. 当前page为空,则往前遍历,并更新当前page
  3. 获取到当前需要释放的对象,然后将该位置设置为SCRIBBLEnext指针往前移。
  4. 最后,若当前对象不为“哨兵”对象,则对该对象进行释放

具体流程如下图所示:

六、autoreleasepoolNSThread的关系

两者的关联主要涉及的有两个点:

a. autoreleasepool依赖于当前线程的TLS,这个上面也分析过了;

b. autoreleasepool在不同线程中的创建和释放,这里主要探讨这个问题

  • 主线程中,系统已经在main.m中通过@autoreleasepool创建了自动释放池,所以我们无需额外去创建和释放了
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
复制代码
  • 那么在子线程中,我们是否需要像主线程一样,使用@autoreleasepool方法进行创建和释放呢?

在ARC中,我们知道编译器会在合适的位置自动插入autorelease方法,而我们上面分析push操作的时候提到过autoreleaseFast方法也会在autorelease方法的时候调用。因此,不管我们有没手动创建自动释放池,它都会添加到autoreleasepool中。

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

//编译后:
NSObject *obj = [[NSObject alloc] init];
[obj 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;
}
复制代码

自动释放池的创建清楚了,再来看看它的释放操作。我们知道主线程中的@autoreleasepool会通过objc_autoreleasePoolPop方法进行释放。而在子线程中并没有调用这样的方法,那又要如何进行释放呢?我们先看个例子:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadRun) object:nil];
    [thread start];
}

- (void)threadRun {
    Person *p = [[Person alloc] initWithName:@"jam" age:24 date:[NSDate date]];
    self.person = p; //此处打断点
    NSLog(@"run in %@", [NSThread currentThread]);
}
复制代码

self.person = p的位置打断点,然后设置观察对象watchpoint set variable p,再不断执行,直到线程执行完,找到对应线程的断点,可以看到:

点进去看,可以看到起调用过程:

这里有个_pthread_tsd_cleanup函数的调用

void
_pthread_tsd_cleanup(pthread_t self)
{
	int i, j;
	void *param;
	for (j = 0;  j < PTHREAD_DESTRUCTOR_ITERATIONS;  j++)
	{
		for (i = 0;  i < _POSIX_THREAD_KEYS_MAX;  i++)
		{
			if (_pthread_keys[i].created && (param = self->tsd[i]))
			{
				self->tsd[i] = (void *)NULL;
				if (_pthread_keys[i].destructor)
				{
					(_pthread_keys[i].destructor)(param);
				}
			}
		}
	}
}
复制代码

很明显,该函数会对当前线程的TLS的资源进行清除,遍历所有pthread_key_t,调用其析构函数。我们知道autoreleasepool在线程中有对应的pthread_key_t

static pthread_key_t const key = AUTORELEASE_POOL_KEY;

static void init()
{
    int r __unused = pthread_key_init_np(AutoreleasePoolPage::key, 
                                         AutoreleasePoolPage::tls_dealloc);
    ASSERT(r == 0);
}

static void tls_dealloc(void *p) 
{
    if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
        // No objects or pool pages to clean up here.
        return;
    }

    // reinstate TLS value while we work
    setHotPage((AutoreleasePoolPage *)p);

    if (AutoreleasePoolPage *page = coldPage()) {
        if (!page->empty()) objc_autoreleasePoolPop(page->begin());  // pop all of the pools
        if (slowpath(DebugMissingPools || DebugPoolAllocation)) {
            // pop() killed the pages already
        } else {
            page->kill();  // free all of the pages
        }
    }
    
    // clear TLS value so TLS destruction doesn't loop
    setHotPage(nil);
}
复制代码

因此,子线程中自动释放池的创建和释放都无需我们进行额外的操作。当然,在某些场景下,也可以手动通过@autoreleasepool进行创建和释放。

七、autoreleasepoolenumerateObjectsUsingBlock

enumerateObjectsUsingBlock方法会自动在内部添加一个@autoreleasepool,以保证下一次迭代前清除临时对象,从而降低内存峰值。

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        
        NSArray *arr = @[@"str1", @"str2", @"str3"];
        [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            id o = obj; //此处设置断点
            NSLog(@"obj: %@", o);
        }];
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
复制代码

我们通过在id o = obj位置设置断点,然后添加观察变量watchpoint set variable o,再运行程序,会发现每次迭代结束后,都会调用自动释放池的releaseUnitl方法:

相关资料

  • Using Autorelease Pool

  • NSAutoReleasePool

  • autoreleasepool and nsthread

  • 黑幕背后的Autorelease



这篇关于自动释放池AutoreleasePool的探究的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程