@autoreleasepool 编译(clang)后会被转换成:
__AtAutoreleasePool __autoreleasepool;
__AtAutoreleasePool是一个结构体,定义如下:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
__AtAutoreleasePool中包含一个构造函数及一个析构函数:构造函数调用 objc_autoreleasePoolPush() 并返回一个 atautoreleasepoolobj 对象;而析构函数将atautoreleasepoolobj 对象作为 objc_autoreleasePoolPop() 的入参。
@autoreleasepool{}实际是下面两行的替换:
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do something
objc_autoreleasePoolPop(atautoreleasepoolobj);
先看objc_autoreleasePoolPush()和objc_autoreleasePoolPop()的实现,在runtime源码的NSObject.mm文件中:
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
objc_autoreleasePoolPush()和objc_autoreleasePoolPop()实际上分别调用的AutoreleasePoolPage的push()和pop(ctxt) 函数。
自动释放池的主要底层数据结构是 __AtAutoreleasePool 和 AutoreleasePoolPage
AutoreleasePoolPage
在 AutoreleasePoolPage 定义的上方有一大段注释自动释放池实现:
线程的自动释放池是指针的栈,即自动释放池是栈结构。
每个指针要么是要释放的对象,要么是自动释放池边界POOL_BOUNDARY(哨兵对象,表示一个 autorelease pool 的边界)。
释放池 token 是一个指向这个释放池 POOL_BOUNDARY的指针。当释放池被 pop 的时候,在这个哨兵对象后面添加的那些hotter对象都会被 release。
这个栈(即 autorelease pool)是一个以 page 为结点的双向链表,这些 page 根据需要来添加或者删除。
Thread-local storage(TLS,即线程局部存储)指向 hot page,这个 hot page 是指最新添加的 autorelease 对象所在的那个 page。
这里需要注意的是,栈上只存指针(就是对象的地址),对象本身是存在堆上的。
AutoreleasePoolPage定义
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
friend struct thread_data_t;
public:
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MIN_SIZE; // 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);
// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nil
...
}
AutoreleasePoolPage 继承自 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;
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)
{
}
};
这些参数的意思如下:
magic_t const magic:16bytes,用来校验AutoreleasePoolPage结构的完整性
__unsafe_unretained id next:8bytes,*指向的是AutoreleasePoolPage中下一个为空的内存地址(新来的对象会存储到next处,初始化时指向 begin())
pthread_t const thread:8bytes,指向当前线程,保存了当前页所在的线程(一个AutoreleasePoolPage属于一个线程,一个线程中可以有多个AutoreleasePoolPage)。
AutoreleasePoolPage * const parent:8bytes,指向父page,第一个page的parent为nil
AutoreleasePoolPage child:8bytes,*指向子page,最后一个page的child为nil
uint32_t const depth:4bytes,表示深度,从0开始
uint32_t hiwat: 4bytes,代表 high water mark,表示最大入栈数量标记
COUNT :page里的对象个数
EMPTY_POOL_PLACEHOLDER :空池占位。
POOL_BOUNDARY :哨兵对象或边界对象, nil别名,用来区别每个 AutoreleasePool的边界,用来区分不同的自动释放池,以解决自动释放池嵌套的问题。
PAGE_MAX_SIZE :定义的大小(在下面定义中可以看到是4096bytes)
SIZE:AutoreleasePoolPage的大小
define BYTE_SIZE 8 /* byte size in bits */
#define I386_PGBYTES 4096 / bytes per 80386 page /
#define PAGE_SIZE I386_PGBYTES
#define PAGE_MAX_SIZE PAGE_SIZE
define PAGE_MIN_SIZE PAGE_SIZE
一个 AutoreleasePoolPage 会开辟 PAGE_MAX_SIZE 的内存(4096 bytes,可能会根据不同设备及系统分配不同的内存),除了 AutoreleasePoolPage 的成员变量所占空间(共 56 bytes),其余空间将会用来存储加入到自动释放池的对象,一个AutoreleasePoolPage能存放505个对象(即(4096-56)/8 = 505) 。
一个 AutoreleasePoolPage 初始状态结构如下:
begin() 和 end() 这两个函数帮助快速获取AutoreleasePoolPage存储对象地址的起至位置。
next 指向了最新下一个为空的内存地址,当 next == end() 时,表示当前 page 已经满了。
初始化一个AutoreleasePool后:
初始化一个AutoreleasePool会在AutoreleasePoolPage添加一个POOL_BOUNDARY,并将这个POOL_BOUNDARY返回,来确保在 pop 调用的时候,不会出现异常。POOL_BOUNDARY会存放在当前 next 指向的位置,当对象存放完成后,next 指针会指向下一个为空的地址。
向AutoreleasePool添加一个对象
next 原来指向的地址存入一个 obj(id*,指向autorelease对象的指针),而next 指针则指向下一个为空的地址。
下面去源码里验证一下
objc_autoreleasePoolPush 函数
首先自动释放池初始化,返回一个atautoreleasepoolobj哨兵对象:
** void * atautoreleasepoolobj = objc_autoreleasePoolPush();**
这里objc_autoreleasePoolPush()实现上面已经写出来了,里面返回的是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;
}
push()函数通过调用 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);
}
}
hotPage()是正在使用的AutoreleasePoolPage。
autoreleaseFast() 函数里面分为三种情况:
【1】当前 hotPage存在且没有满时:
调用 page->add(obj) 函数将对象加入该 hotPage 中,即 next 指向的位置;
【2】当前hotpage 存在但是已满时:
调用 autoreleaseFullPage(obj, page) 函数,该函数会先查找 hotPage 的 child,如果有则将 child page 设置为 hotPage,如果没有则将创建一个新的 hotPage,之后在这个新的 hotPage 上执行 **page->add(obj) **操作;
【3】当前 page不存在时:
调用** autoreleaseNoPage(obj) 函数,该函数会创建一个 hotPage,然后执行 page->add(obj) **操作。
接下来看看 add() 函数的定义:
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
add() 函数会把传入的对象obj 存放在原本 next 所在的位置, 而next 指针移到下一个空位置。
再看看autoreleaseFullPage(obj, page),(当前 hotPage 已满):
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);
}
从autoreleaseFullPage的实现可以看出:
【1】当前hotPage 已满;
【2】从传入的 page 开始遍历整个双向链表,直到查找到一个未满的AutoreleasePoolPage;如果没有找到未满的AutoreleasePoolPage,则创建一个新的AutoreleasePoolPage。最后得到新的page;
【3】将上一步中的page设置为hotpage;
【4】调用add(obj),将obj对象添加的page中。
最后我们再来看看autoreleaseNoPage(obj),(没有 hotPage):
id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
ASSERT(!hotPage());
if (obj == POOL_BOUNDARY) {
// We are pushing a pool with no pool in place,
// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();
}
// 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);
}
这里对函数实现进行了删减只留下正常流程:
【1】当前没有可用的AutoreleasePoolPage;
【2】创建一个AutoreleasePoolPage并设置为 hotPage;
【3】条件性给这个hotPage添加一个POOL_BOUNDARY哨兵对象(这里条件不影响流程认知,不做说明);
【4】然后执行** page->add(obj) **操作。
不存在 AutoreleasePoolPage也就意味着没有可用的自动释放池,就要从头开始构建这个自动释放池的双向链表,也就是说,新的 AutoreleasePoolPage 是没有 parent 指针的。
最后,看一下autorelease(id obj)实现,同样也调用了 autoreleaseFast(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;
}
AutoreleasePoolPage 的autorelease 函数的和 push 操作的实现非常相似。
只不过push操作插入的是一个POOL_BOUNDARY ,而 autorelease操作插入的是一个具体的autoreleased 对象,向一个对象发送 autorelease 消息,就会把该对象 add 进 page 里。
objc_autoreleasePoolPop(void *ctxt)
在调用时 (atautoreleasepoolobj是要释放的释放池的哨兵对象)
objc_autoreleasePoolPop(atautoreleasepoolobj);
objc_autoreleasePoolPop的实现上面已经列出,实际调用AutoreleasePoolPage的pop()函数:
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
//token对用的自动释放池是一个空的占位池
}else{
**page =pageForPointer(token);**
}
stop = (id*)token;
return popPage(token, page, stop);
}
page =pageForPointer(token); 和 return popPage(token, page, stop);
static AutoreleasePoolPage *pageForPointer(uintptr_tp)
{ AutoreleasePoolPage *result; uintptr_toffset = p %SIZE; ASSERT(offset >=sizeof(AutoreleasePoolPage)); result = (AutoreleasePoolPage*)(p - offset); result->fastcheck(); returnresult; }
这里 token 是一个指向这个释放池 POOL_BOUNDARY的指针。
pageForPointer(token) 会获取哨兵对象所在 AutoreleasePoolPage:主要是通过指针与 page 大小取模得到其偏移量(因为所有的 AutoreleasePoolPage 在内存中都是对齐的),最后通过 fastCheck() 函数检查得到的是不是一个 AutoreleasePoolPage。
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();
}
}
}
popPage()中调用的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();
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
}
popPage()函数首先调用 releaseUntil()函数循环释放栈中的对象,直到 stop;releaseUntil()函数会先把 next 指针向前移动,取到将要释放的一个指针,之后调用 memset 擦除该指针所占内存,再调用 objc_release 函数释放该指针指向的对象,这样通过 next 指针循环往前查找去释放对象,期间可往前跨越多个 page,直到找到传进来的哨兵对象为止。当有嵌套的 autoreleasepool 时,会清除一层后再清除另一层,因为 pop 是会释放到上次 push 的位置为止,每次一层,互不影响。
popPage()函数中在releaseUntil()函数后面判断,如果传入的哨兵对象所在 page 有 child,有两种情况:一是当前 page 使用不满一半,从 child page 开始将后面所有 page 都 kill();二是当前 page 使用超过一半,从 child page 的 child page(即孙子,如果有的话)开始将后面所有的 page 都 kill()
kill()函数删除双向链表中的每一个的page,找到当前page的 child 方向尾部 page,然后反向释放并且把其parent节点的 child 指针置空。
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);
}
[NSObject autorelease]
- (id)autorelease
—id _objc_rootAutorelease(self);
——obj->rootAutorelease();
——— id objc_object::rootAutorelease()
————id objc_object::rootAutorelease2()
————— AutoreleasePoolPage::autorelease((id)this);
到这里我们发现,一个对象autorelease,最终还是AutoreleasePoolPage的autorelease;
AutoreleasePoolPage的autorelease上面已经介绍过。
总结
一、自动释放池是一个双向链表,链表的每一个元素就是就是一个AutoreleasePoolPage。
二、释放池主要通过 push 及 pop 操作来管理:
每调用一次 objc_autoreleasepoolpush 操作就会创建一个新的autoreleasepool ,往 AutoreleasePoolPage 中插入一个POOL_BOUNDARY ,并返回这个POOL_BOUNDARY的内存地址。
当销毁一个自动释放池时,会调用objc_autoreleasepoolpop()函数并传入一个POOL_BOUNDARY,会从自动释放池中最后一个对象开始,依次给它们发送release消息,直到遇到这个POOL_BOUNDARY。
三、向一个对象发送 autorelease 消息,会把该对象 add 进 AutoreleasePoolPage 里
四、从main函数中知道iOS项目默认是包裹在大的释放池中;RunLoop开始循环、休眠和退出时会对释放池进行objc_autoreleasepoolpush/objc_autoreleasepoolpop操作。
五、线程和释放池一一对应;
扩展
一、autoreleasepool的使用场景?
使用场景:多次创建临时变量导致内存上涨时,需要延迟释放
苹果的文档有说大意如下:
1、程序不是基于 UI 框架的,如命令行工具(因为它并没有内置的”autorelease pool”的支持)
2、你编写的循环创建了大量的临时对象(在循环体内创建一个@autoreloeasePool{}在循环中使用autorelease pool block可以降低内存峰值)
for (NSURL url in urls) {
@autoreleasepool {
/ Process the string, creating and autoreleasing more objects. */
}
}
3、如果创建了一个辅助线程。当线程开始执行的时候你必须立马创建一个autorelease pool block,否则可能造成autoreleased对象的内存泄漏。
4、使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:
[array enumerateObjectsUsingBlock:^(idobj,NSUIntegeridx,BOOL*stop) {
// 这里被一个局部@autoreleasepool包围着
}];
二、autorelease 对象会在什么时候释放?
分两种情况:
我们自己添加的 @autoreleasepool:会在大括号结束时释放(Any object (such as fileContents) sent an autorelease message inside the autorelease pool block is released at the end of the block.)
不使用 @autoreleasepool:不手动指定 autoreleasepool 的 autorelease 对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的 runloop 迭代结束时释放。(runloop循环系统会隐式创建一个新的autoreleasepool并在循环结束后释放)
一个线程创建的时候就会有一个autorelease pool的创建,并且在线程退出的时候,清空整个autorelease pool子线程中的autorelease变量
三、子线程中的autorelease变量什么时候释放?子线程里面,需要加autoreleasepool吗?
在子线程创建了 autoreleasepool 的话,产生的 autorelease 对象就会交给 autoreleasepool 去管理,在线程退出的时候,清空整个autoreleasepool。
如果没有创建 autoreleasepool ,但是产生了 autorelease 对象,就会调用AutoreleasePoolPage 的 autoreleaseNoPage 方法。该方法会自动创建一个 hotpage,并调用page->add(obj)将对象添加到 AutoreleasePoolPage 的栈中。
要弄清线程-runloop-autoreleasepool之间的关系。
主线程的runloop是默认创建的,系统会监听runloop两种事件(自动释放池的创建和释放,销毁的时机):
(1)kCFRunLoopEntry,即将进入runloop时,会创建一个autoreleasepool。
(2)kCFRunLoopBeforeWaiting,runloop即将休眠时,会释放autoreleasepool并创建一个新的autoreleasepool;
(3)kCFRunLoopExit,runloop即将退出时,会释放autoreleasepool。
(4)而autoreleasepool在释放时,会对插入到pool中的对象发送release消息。
所以,runloop每次迭代结束,autoreleasepool释放,aurelease对象释放。
runloop只可以获取,不可以手动创建。
子线程的runloop是手动获取的,在获取的时候系统会创建一个runloop并返回,所以手动获取到的runloop其实是系统刚创建好的。
子线程的autoreleasepool也需要手动获取,但区分情况,一般系统提供的block如usingBlock和GCD提供的block内部都会自动包裹一个autoreleasepool,不用手动加。但是你自己通过其他方式创建的子线程,在线程内部需要手动获取autoreleasepool,防止局部内存使用峰值过高或发生其他内存问题,最后,autoreleasepool释放时,也会对其管理的对象发送release消息。
四、那些对象会放入自动释放池?
在MRC中,调用 autorelease 的对象会被添加到自动释放池。
在ARC中,以类方法获取的对象(注意不是使用alloc/new/copy/mutablCopy获取的对象)不会马上被销毁,而是要等到超过autoreleasepool的作用域才会真实执行release,这是因为类方法里的编译器帮你返回了一个autoreleasing的对象。
Animal *animal1 = [[Animal alloc] init];
Animal *animal2 = [Animal animal];
_objc_autoreleasePoolPrint();
如上代码打印,alloc创建的animal1并不会加入自动释放池,类方法创建的 animal2 会打印
objc[19507]: ##############
objc[19507]: AUTORELEASE POOLS for thread 0x1056cee00
objc[19507]: 1 releases pending.
objc[19507]: [0x7ff56080c000] ................ PAGE (hot) (cold)
objc[19507]: [0x7ff56080c038] 0x6000026e40f0 Animal
objc[19507]: ##############