什么是autoreleasepool
顾名思义,autoreleasepool也称为自动释放池,类似于C语言中的自动变量,我们可以将对象加入到autoreleasepool中,当其超出其作用域时,调用release释放该对象。
在之前探究ARC实现时,我们可以发现ARC将非自己持有的对象加入到了autoreleasepool。
下面深入了解一下autoreleasepool的结构
autoreleasepool的结构
在objc中查看源码可以发现最后会走到:
上面的方法看上去是对 AutoreleasePoolPage 对应静态方法 push 和 pop 的封装。下面看看这个AutoreleasePoolPage。
AutoreleasePoolPage
在objc中点开其源码:
// MARK: - AutoReleasepool class
class AutoreleasePoolPage
{
// 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
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id);
// 校验完整性
magic_t const magic;
// next指针指向最新添加的autoreleased对象的下一个位置,初始化时指向begin()
id *next;
// 指向当前线程
pthread_t const thread;
// 父节点
AutoreleasePoolPage * const parent;
// 子节点
AutoreleasePoolPage *child;
// 节点深度
uint32_t const depth;
uint32_t hiwat;
// SIZE-sizeof(*this) bytes of contents follow
static void * operator new(size_t size) {
return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
}
static void operator delete(void * p) {
return free(p);
}
这个数据结构很熟悉,原来Auto releasePool的底层是由一系列的 AutoreleasePoolPage 组成的
(并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000))
#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES
结构如下所示:
是一个双向链表,parent 和 child 就是用来构造双向链表的指针。
在官方文档对于AutoreleasePool的解释中,我们可以看到下面几个概念,在下面的源码用得上。
Autorelease pool implementation
- A thread's autorelease pool is a stack of pointers.
线程的自动释放池是指针的堆栈
- Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.
每个指针都是要释放的对象,或者是POOL_BOUNDARY,它是自动释放池的边界。
- A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.
池令牌是指向该池的POOL_BOUNDARY的指针。弹出池后,将释放比哨点更热的每个对象。
- The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.
堆栈分为两个双向链接的页面列表。根据需要添加和删除页面。
- Thread-local storage points to the hot page, where newly autoreleased objects are stored.
线程本地存储指向热页面,该页面存储新自动释放的对象。
1、自动释放池 是一个 关于指针的栈结构
2、其中的指针是指要释放的对象或者 pool_boundary 哨兵(现在经常被称为 边界)
pool_boundary:POOL_BOUNDARY:nil的宏定义,替代之前的哨兵对象POOL_SENTINEL,在自动释放池创建时,在objc_autoreleasePoolPush中将其推入自动释放池中。在调用objc_autoreleasePoolPop时,会将池中对象按顺序释放,直至遇到最近一个POOL_BOUNDARY时停止。
3、自动释放池是一个页的结构(虚拟内存中提及过) ,而且这个页是一个双向链表(表示有父节点 和 子节点,在类中提及过,即类的继承链)
4、一些名词
-
hotPage
:是当前正在使用的page
,操作都是在hotPage
上完成,一般处于链表末端或者倒数第二个位置。存储在TLS
中,可以理解为一个每个线程共享一个自动释放池链表。 -
coldPage
:位于链表头部的page
,可能同时为hotPage
。 -
EMPTY_POOL_PLACEHOLDER
:当自动释放池中没有推入过任何对象时,这个时候推入一个POOL_BOUNDARY
,会先将EMPTY_POOL_PLACEHOLDER
存储在 TLS 中作为标识符,并且此次并不推入POOL_BOUNDARY
。等再次有对象被推入自动释放池时,检查在 TLS 中取出该标识符,这个时候再推入POOL_BOUNDARY
。 -
next
:指向AutoreleasePoolPage
指向栈顶空位的指针,每次加入新的元素都会往上移动。
autoreleasePool中的栈
如果我们的一个 AutoreleasePoolPage 被初始化在内存的 0x100816000 ~ 0x100817000 中,它在内存中的结构如下:
其中有 56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的 0x100816038 ~ 0x100817000 都是用来存储加入到自动释放池中的对象。
begin() 和 end() 这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。
next 指向了下一个为空的内存地址,如果 next 指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中:
在栈中release加入的元素时候,就会根据哨兵也就是pool_boundary来作为边界Pop。
当方法 objc_autoreleasePoolPop 调用时,就会向自动释放池中的对象发送 release 消息,直到第一个 POOL_SENTINEL:
autoreleasePool的实现源码
objc_autoreleasePoolPush
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
它调用 AutoreleasePoolPage 的类方法 push,也非常简单:
// push 返回的是next指针指向的obj(是一个released obj)
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// 每一个autoreleasepool对象开始于一个新的autoreleasepoolpage
// 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;
}
objc_autoreleasePoolPush
方法其实就是向自动释放池推入一个POOL_BOUNDARY
,作为该autoreleasepool
的起点。autoreleaseFast方法的具体逻辑将在后面分析autorelease
方法时再进行分析。
autorelease
下面看加入自动释放池方法autorelease的代码实现:
static inline id autorelease(id obj) {
id *dest __unused = autoreleaseFast(obj);
return obj;
}
可以看到这里把对象作为参数调用了autoreleaseFast
。
// 创建一个page逻辑
static inline id *autoreleaseFast(id obj)
{
// hotPage可以理解为正在使用的AutoreleasePoolPage
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
// 有 hotPage 且不满 直接加入栈中
return page->add(obj);
} else if (page) {
// hotPage 已满 先创建一个 Page 后,加入新 Page 中
return autoreleaseFullPage(obj, page);
} else {
// 没有 hotPage 直接新建一个 Page,并加入 Page 中
return autoreleaseNoPage(obj);
}
}
这段代码的逻辑:
- 如果HotPage存在且未满,则直接推入hotPage。
- 如果HotPage存在且已经满,则调用autoreleaseFullPage,去初始化一个新的页,并推入
- 如果不存在HotPage,则调用autoreleaseNoPage,创建一个hotPage,推入hotPage。
autoreleaseFullPage(当前 hotPage 已满)
// MARK: - 创建一个new 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);
// 找到一个未满的 page , 未找到则新建一个 page ,设置成 hotPage
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
该方法逻辑如下:
- 查看hotPage是否有后继节点(page),如果有直接使用后继节点。
- 如果没有后继节点,则新建一个AutoreleasePoolPage。
- 将对象加入获取到的page,并将其设置为hotPage,其实就是存入 TLS 中共享。
autoreleaseNoPage(没有 hotPage)
// autoreleasepool中没有page
static __attribute__((noinline))
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
// 执行 No page 表示目前还没有释放池,或者有一个空占位符池,但是还没有加入对象
assert(!hotPage());
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that, push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
// 如果是空占位符池,需要加入一个释放池边界
pushExtraBoundary = true;
}
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
//如果没有创建一个autoreleasepool,则直接抛异常
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
pthread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
// 如果传入 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();
}
// We are pushing an object or a non-placeholder'd pool.
// 初始化一个 page 并设置 hotPage
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// 插入释放池边界
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// obj添加到page中
return page->add(obj);
}
autoreleaseNoPage只有在自动释放池还没有page时调用,主要逻辑:
- 如果当前自动释放池推入的是一个哨兵
POOL_BOUNDARY
时,将EmptyPoolPlaceholder
存入 TLS 中。 - 如果 TLS 存储了
EmptyPoolPlaceholder
时,在创建好page之后,会先推入一个POOL_BOUNDARY
,然后再将加入自动释放池的对象推入。
推入都是调用了add函数:
// 向page中加入对象
id *add(id obj)
{
assert(!full());
unprotect();
// 将next节点赋值给ret
id *ret = next; // faster than `return next-1` because of aliasing
// 将obj赋值给next
*next++ = obj;
protect();
return ret;
}
这个很好理解,就是一个压入栈的操作。
objc_autoreleasePoolPop
在自动释放池所在作用域结束时,会调用objc_autoreleasePoolPop,对自动释放池中的对象进行释放。
// pop
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
// 如果是空池占位符,要清空整个自动释放池
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
if (hotPage()) {
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
// 如果存在 hotPage ,则找到 coldPage 的起点 重新 pop
pop(coldPage()->begin());
} else {
// 未使用过的释放池,置空 TLS 中存放的 hotPage.
setHotPage(nil);
}
return;
}
// 根据token获取page
page = pageForPointer(token);
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
// 在 stop 不为 POOL_BOUNDARY 的情况下 只可能是 coldPage()->begin()
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 {
// 如果不是 POOL_BOUNDARY 也不是 coldPage()->begin() 则报错
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
/// 释放 stop 后面的所有对象
page->releaseUntil(stop);
// 清除空节点
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
}
// 清除后续节点 page
else if (page->child) {
// 如果当前 page 没有达到半满,则干掉所有后续 page
if (page->lessThanHalfFull()) {
page->child->kill();
}
// 如果当前 page 达到半满以上,则保留下一页
else if (page->child->child) {
page->child->child->kill();
}
}
}
这段代码逻辑:
- 检查入参是否为空池占位符
EMPTY_POOL_PLACEHOLDER
,如果是则继续判断是否hotPage
存在,如果hotPage
存在则将释放的终点改成coldPage()->begin()
,如果hotPage
不存在,则置空 TLS 存储中的hotPage
。 - 检查stop既不是
POOL_BOUNDARY
也不是coldPage()->begin()
的情况将报错。 - 清空自动释放池中stop之后的所有对象。
- 判断当前page如果没有达到半满,则干掉所有后续所有 page,如果超过半满则只保留下一个page。
这里用到了几个函数,首先是通过token获取page的pageForPointer:
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE;
// 将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为所有的 AutoreleasePoolPage 在内存中都是对齐的:
assert(offset >= sizeof(AutoreleasePoolPage));
result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();
return result;
}
将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为所有的 AutoreleasePoolPage 在内存中都是对齐的:
p = 0x100816048
p % SIZE = 0x48
result = 0x100816000
而最后调用的方法 fastCheck() 用来检查当前的 result 是不是一个 AutoreleasePoolPage。
通过检查 magic_t 结构体中的某个成员是否为 0xA1A1A1A1。
releaseUntil 释放对象
实现如下:
// MARK: - 释放obj
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
// 获取page的父指针
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
// 根据next指针缩小一个位置获取obj
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
// 发送release 消息
objc_release(obj);
}
}
setHotPage(this);
在这里通过一个while循环,判断当前page如果被清空,则继续清理链表中的上一个page,release了page里面存储的对象。
kill() 方法
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,就是从autoreleasePoolPush开始到Pop结束。
- [NSObject autorelease]
└── id objc_object::rootAutorelease()
└── id objc_object::rootAutorelease2()
└── static id AutoreleasePoolPage::autorelease(id obj)
└── static id AutoreleasePoolPage::autoreleaseFast(id obj)
├── id *add(id obj)
├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
│ ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
│ └── id *add(id obj)
└── static id *autoreleaseNoPage(id obj)
├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
└── id *add(id obj)
在autorelease
方法的调用栈中,最终都会调用上面提到的 autoreleaseFast
方法,将当前对象加到 AutoreleasePoolPage
中。
RunLoop和autoreleasePool
iOS在主线程的RunLoop中注册了两个Observer:
- 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush();
- 第2个Observer:
① 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush();
② 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()。