什么是autoreleasepool
顾名思义,autoreleasepool也称为自动释放池,类似于C语言中的自动变量,我们可以将对象加入到autoreleasepool中,当其超出其作用域时,调用release释放该对象。 在之前探究ARC实现时,我们可以发现ARC将非自己持有的对象加入到了autoreleasepool。 下面深入了解一下autoreleasepool的结构
autoreleasepool的结构
在objc中查看源码可以发现最后会走到: 上面的方法看上去是对 AutoreleasePoolPage 对应静态方法 push 和 pop 的封装。下面看看这个AutoreleasePoolPage。
AutoreleasePoolPage
在objc中点开其源码:
class AutoreleasePoolPage
{
# 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;
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE;
#else
PAGE_MAX_SIZE;
#endif
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
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,也非常简单:
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
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 。
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存在且未满,则直接推入hotPage。
- 如果HotPage存在且已经满,则调用autoreleaseFullPage,去初始化一个新的页,并推入
- 如果不存在HotPage,则调用autoreleaseNoPage,创建一个hotPage,推入hotPage。
autoreleaseFullPage(当前 hotPage 已满)
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *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);
}
该方法逻辑如下:
- 查看hotPage是否有后继节点(page),如果有直接使用后继节点。
- 如果没有后继节点,则新建一个AutoreleasePoolPage。
- 将对象加入获取到的page,并将其设置为hotPage,其实就是存入 TLS 中共享。
autoreleaseNoPage(没有 hotPage)
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
assert(!hotPage());
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
pushExtraBoundary = true;
}
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
_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) {
return setEmptyPoolPlaceholder();
}
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
return page->add(obj);
}
autoreleaseNoPage只有在自动释放池还没有page时调用,主要逻辑:
- 如果当前自动释放池推入的是一个哨兵
POOL_BOUNDARY 时,将EmptyPoolPlaceholder 存入 TLS 中。 - 如果 TLS 存储了
EmptyPoolPlaceholder 时,在创建好page之后,会先推入一个POOL_BOUNDARY ,然后再将加入自动释放池的对象推入。 推入都是调用了add函数:
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next;
*next++ = obj;
protect();
return ret;
}
这个很好理解,就是一个压入栈的操作。
objc_autoreleasePoolPop
在自动释放池所在作用域结束时,会调用objc_autoreleasePoolPop,对自动释放池中的对象进行释放。
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
if (hotPage()) {
pop(coldPage()->begin());
} else {
setHotPage(nil);
}
return;
}
page = pageForPointer(token);
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
} else {
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);
if (DebugPoolAllocation && page->empty()) {
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
page->kill();
setHotPage(nil);
}
else if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
}
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;
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 释放对象
实现如下:
void releaseUntil(id *stop)
{
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
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);
在这里通过一个while循环,判断当前page如果被清空,则继续清理链表中的上一个page,release了page里面存储的对象。
kill() 方法
void kill()
{
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()。
|