在我们平时开发中,经常会涉及到对象的使用,但我们往往都不知道我们所开辟的对象是在何时被回收的,有没有及时的被释放,在苹果引入ARC之后,平时书写OC代码的时候都很少对内存释放做过处理,但是有一些场景还是需要我们手动去管理内存的释放;比如大量的for循环创建对象时候,比如子线程中存在持续性任务没办法及时关闭,这个时候也要我们手动管理临时变量的释放;
概念:对象执行 autorelease 方法或者直接在 autoreleasePool 中创建对象,会将对象添加到 autoreleasePool 中,当自动释放池销毁的时候,会对所有对象做 release 操作;
创建autoreleasePool
MRC环境创建:
// 1、生成一个NSAutoreleasePool对象
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 2、调用autorelease方法
id object = [[NSObject alloc] init];
[object autorelease];
// 3、对象销毁
[pool drain];
ARC环境创建:
@autoreleasepool {
//LLVM
id object = [NSArray array];
}
MRC环境下,我们创建的对象需要调用autorelease添加到自动释放池中,而ARC环境下,我们只要通过@autoreleasepool就可以;
通过查找源码会发现,ARC内部其实已经帮我们自动实现了autorelease方法,帮我们添加到autoreleasePool中;
#ifndef AUTORELEASE
/**
* Basic autorelease operation ... calls [NSObject-autorelease]<br />
* Does nothing when ARC is in use.
*/
#define AUTORELEASE(object) [(id)(object) autorelease]
#endif
接下来就是分析autoReleasePool内部具体的工作原理;
因为main入口函数就为我们提供了一个现成的autoReleasePool
所以可以直接通过clang得到编译后的内容,找到autoReleasePool
clang main.m -rewrite-objc
会发现@autoreleasepool本质上就是帮我们生成一个__AtAutoreleasePool对象
通过检索__AtAutoreleasePool会发现,它其实就是一个结构体,里面包含了两个函数,一个构造函数,一个析构函数;当autoreleasepool初始化的时候就会调用__AtAutoreleasePool()构造函数,当作用域离开的时候就会调用析构函数~__AtAutoreleasePool();
构造函数就是通过objc_autoreleasePoolPush()生成atautoreleasepoolobj对象,析构函数就是调用objc_autoreleasePoolPop函数,把atautoreleasepoolobj传递进去,这时候我们发现,我们应该重点分析的方法是objc_autoreleasePoolPush及objc_autoreleasePoolPop;
这个时候就得查看官方源码了;
objc_autoreleasePoolPush
在NSObject.mm定位到objc_autoreleasePoolPush,内部是直接调用了AutoreleasePoolPage的push方法;
到这里又引出了新的类-->AutoreleasePoolPage,进一步观察会发现它是继承自AutoreleasePoolPageData;
接下来我们就来探索一下push方法的实现;点击去看会发现push其实就是静态内联函数,
push方法里面判断自动释放池AutoreleasePoolPage是否初始化过了,初始化过了就调用autoreleaseFast,否则就调用autoreleaseNewPage初始化一个新的page,观察发现这两个方法里面都传入了一个POOL_BOUNDARY参数,这个又是什么呢??
# define POOL_BOUNDARY nil
点击去看会发现POOL_BOUNDARY是一个nil,这个就有点懵了,传进去nil有何意义???
POOL_BOUNDARY
POOL_BOUNDARY其实是一个哨兵对象也可以看成一个边界,用来分割不同的@autoreleasepool;
我们有时候会为了代码的可读性,或使用多层@autoreleasepool
的嵌套,这些autoreleasepool
会用同一个AutoreleasePoolPage
对象。以下面的三个嵌套为例,在同一个page
中的顺序是下图这样。不同的@autoreleasepool
以POOL_BOUNDARY
做分割。
@autoreleasepool {
NSObject *p1 = [[NSObject alloc] init];
NSObject *p2 = [[NSObject alloc] init];
@autoreleasepool {
NSObject *p3 = [[NSObject alloc] init];
@autoreleasepool {
NSObject *p4 = [[NSObject alloc] init];
}
}
}
autoreleaseFast
接下来分析一下autoreleaseFast方法,在函数内部,通过hotPage获取当前的page,
1、如果page存在并且page有存储空间,就调用add方法将对象存到page中;
2、如果page存在但是没有存储空间,这个时候就调用autoreleaseFullPage
函数创建新的page
,并且将链表的末尾指向新创建的page
。
3、如果没有page不存在
,则调用autoreleaseNoPage
函数创建一个新的page;
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);
}
}
add
add函数核心逻辑就是将obj
放入next
指针的位置,并且对next
指针进行++
,指向下一个位置。*next++
表示先用后加,先将obj
存入next的地址,随后+1;用ret保存next的地址,即上一个对象地址,并返回;
id *add(id obj) {
ASSERT(!full());
unprotect();
id *ret = next;
*next++ = obj;
protect();
return ret;
}
这里又引发一个问题,next指针刚开始指向哪里呢???
通过进一步探索会发现,begin即this(即当前page的首地址)加上当前结构体的需要占用的内存空间,即自动释放池第一个对象从这里开始存储;
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();
}
简单画了一个流程粗略表示一个obj对象入栈的过程;
那么每一个page能存储多少空间呢?
从最新的源码中可以看出,SIZE最大可以是PAGE_MAX_SIZE是1按位左移14,即end指针指向2^14+this;
autoreleaseFullPage
在
autoreleaseFullPage
函数中,会从page
的链表中,从前往后找到末尾的节点child。创建一个新的
page
,在创建函数AutoreleasePoolPage
中会对parent
和child
指针做处理,返回的page
可以直接用。调用
setHotPage
将page
设置到tls_set_direct哈希表中,tls_set_direct(key, (void *)page); 标记成当前可用的page;并且调用page
的add
函数将autorelease
修饰的对象,添加到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);
}
autoreleaseNoPage
autoreleaseNoPage函数的核心代码其实比较简单,就是创建一个新的page,并且也是一样,调用setHotPage设置当前可供使用的page;随后设置POOL_BOUNDARY
哨兵对象,并且把对象添加进去。
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// 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);
}
objc_autoreleasePoolPop
分析完push的流程之后,接下来就分析一下pop的流程,即自动释放池释放的过程;
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
objc_autoreleasePoolPop函数内部其实也是用到AutoreleasePoolPage对象,只是调用pop函数;
pop
判断
autoreleasepool
是否为空,通过EMPTY_POOL_PLACEHOLDER
占位符判断,为空则清空这个page
。autoreleasepool不
为空,则调用pageForPointer方法,对传入的指针token取模,p % SIZE,再用p减去偏移量offset,拿到result返回给page使用,这个表示当前token在最后一个page上的位置;传入的
stop
是否不等于POOL_BOUNDARY
标识,如果不等于则可能是一个有问题的page
。调用
popPage
方法,释放对象。
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);
}
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;
}
popPage
接下来就是探索一下popPage如何释放对象;
popPage
内部核心代码就是执行releaseUntil方法进行释放操作;-
按照
page
达到一半就扩容的原则,后面的if
语句会判断执行pop
后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
在releaseUntil
函数内部,核心逻辑是从当前page
,从后到前调用objc_release
,释放被autorelease
修饰的对象。
AutoreleasePoolPage *page = hotPage(),获取当前的
hotPage
。判断
page
是否为空,如果为空则表示里面的对象被释放完,则将page
的父节点page
设置为hotPage
。获得上一个节点,
->
的算数优先级比--
要高,所以是先通过next
获取当前节点地址,这是一个为空的待存入节点,随后执行--
操作获取上一个对象地址,跟前文提到的*next++ = obj有异曲同工之妙;通过
memset
将上一个节点释放。判断上一个节点是否占位符号
POOL_BOUNDARY
,如果不是则调用objc_release
释放对象。在
while
循环结束后,将当前page
设置为hotPage
。
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();
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;
// create an obj with the zeroed out top byte and release that
id obj = (id)entry->ptr;
int count = (int)entry->count; // grab these before memset
#else
id obj = *--page->next;
#endif
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
// release count+1 times since it is count of the additional
// autoreleases beyond the first one
for (int i = 0; i < count + 1; i++) {
objc_release(obj);
}
#else
objc_release(obj);
#endif
}
}
setHotPage(this);
}
hotPage、coldPage概念
hotPage
hotPage可以被理解为最后一个可插入对象的page,即链表的末尾的page,当我们获取hotPage => AutoreleasePoolPage *page = hotPage() 以及setHotPage时,都是对链表末尾的page进行操作;
hotPage
函数内部本质上是一个page
和key
的映射。通过当前的AUTORELEASE_POOL_KEY存取;
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
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;
}
通过tls_get_direct获取当前的page,这个tls其实就是thread local storage哈希表,即从当前线程的局部私有存储空间中取出该数据,只有当前线程才能访问的数据,这里就可以看出,autoreleasepool跟当前线程其实是有一定的对应关系,每个线程都有其对应的autoreleasepool,AutoreleasePoolPage
对象和线程一一对应,;
coldPage
coldPage就是查询函数,通过查询链表page的根节点
static inline AutoreleasePoolPage *coldPage()
{
AutoreleasePoolPage *result = hotPage();
if (result) {
while (result->parent) {
result = result->parent;
result->fastcheck();
}
}
return result;
}
page释放时机
下面这段主要参考探秘AutoreleasePool实现原理
被autorelease
修饰的对象,释放时机有两种。
如果通过代码添加一个
autoreleasepool
,在作用域结束时,随着pool
的释放,就会释放pool
中的对象。这种情况是及时释放的,并不依赖于runloop
。另一种就是由系统自动进行释放,系统会在
runloop
开始的时候创建一个pool
,结束的时候会对pool
中的对象执行release
操作
runloop
如果是系统创建的pool
,需要手动开启runloop
,主线程默认已经开启并运行,子线程需要调用currentRunLoop
方法开启并运行runloop
,子线程中系统创建pool
的流程才会正常工作。
包括主线程在内的每个线程,如果在线程中使用到了AutoreleasePool
,则会创建两个Observer
并添加到当前线程的Runloop
中,通过这两个Observer
进行对象的自动内存管理。
// activities = 0x1,kCFRunLoopEntry
<CFRunLoopObserver 0x60000012f000 [0x1135c2bb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10eee6276)}
// activities = 0xa0,kCFRunLoopBeforeWaiting | kCFRunLoopExit
<CFRunLoopObserver 0x60000012ef60 [0x1135c2bb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10eee6276)}
首先会创建一个Observer
并监听kCFRunLoopEntry
消息,时机是在进入Runloop
前,此Observer
的优先级设置为-2147483647
的最高优先级,以保证回调发生在Runloop
其他事件前。
然后创建另一个Observer
,并监听kCFRunLoopBeforeWaiting
和kCFRunLoopExit
消息,时机分别在进入Runloop
休眠和退出Runloop
时,将Observer
的优先级设置为2147483647
,以保证回调发生在Runloop
其他事件之后。
两个Observer
都有相同的回调函数_wrapRunLoopWithAutoreleasePoolHandler
,在第一次回调时会在内部调用_objc_autoreleasePoolPush
函数,创建自动释放池。
在kCFRunLoopBeforeWaiting
将要进入休眠前,调用_objc_autoreleasePoolPop
函数释放自动释放池中的对象,并调用_objc_autoreleasePoolPush
函数创建一个新的释放池。在kCFRunLoopExit
将要退出Runloop
时调用_objc_autoreleasePoolPop
函数,释放自动释放池中的对象。
对象添加到自动释放池过程
讲完了autoreleasePool的开辟跟回收过程,以及回收时机,最后对象添加到自动释放池过程
前文已经提到了,系统LLVM会在帮我们自动的调用一个autorelease方法,我们就顺着这个方法看看,它是如何一步步的插入到自动释放池中的;
-(id) autorelease
{
return _objc_rootAutorelease(self);
}
NEVER_INLINE id
_objc_rootAutorelease(id obj)
{
ASSERT(obj);
return obj->rootAutorelease();
}
inline id
objc_object::rootAutorelease()
{
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);
}
static inline id autorelease(id obj)
{
ASSERT(!obj->isTaggedPointerOrNil());
id *dest __unused = autoreleaseFast(obj);
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || (id)((AutoreleasePoolEntry *)dest)->ptr == obj);
#else
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
#endif
return obj;
}
1、autorelease先调用_objc_rootAutorelease方法
2、_objc_rootAutorelease内部在调用当前对象的rootAutorelease方法
3、rootAutorelease方法内部再调用当前对象的rootAutorelease2方法
4、rootAutorelease2方法在调用AutoreleasePoolPage::autorelease方法;
5、AutoreleasePoolPage::autorelease方法最后是调用autoreleaseFast,这个时候就跟我们分析autoreleasePush中的方法遥相呼应了;
总结就是:objc_object::autorelease >> objc_object::_objc_rootAutorelease >> _objc_rootAutorelease >> objc_object::rootAutorelease >> objc_object::rootAutorelease2 >> AutoreleasePoolPage::autorelease >> AutoreleasePoolPage::autoreleaseFast
注:
ARC的规则:alloc/new/copy/mutableCopy 开头的方法返回的对象不是 autorelease 对象;在我们平时开发中,不要出现alloc/new/copy/mutableCopy 开头的方法,否则很容易会导致内存泄露;