Autoreleasepool
int main(int argc, const char * argv[]) {
@autoreleasepool {
//
}
return 0;
}
使用 clang rewrite 命令重写 main.m 文件:
clang -rewrite-objc main.m
最终生成了一个 main.cpp 文件,进文件找到 main() 函数:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */
{
__AtAutoreleasePool __autoreleasepool;
}
return 0;
}
根据注释 /* @autoreleasepool */ 可以得知 @autoreleasepool {} 被转换了一个 __AtAutoreleasePool 类型的变量 __autoreleasepool,在 main.cpp 文件里搜索一下 __AtAutoreleasePool,可以得知 __AtAutoreleasePool 是一个结构体:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
这个结构体在构造方法 __AtAutoreleasePool()
中会调用 objc_autoreleasePoolPush()
方法,在析构方法 ~__AtAutoreleasePool()
中会调用 objc_autoreleasePoolPop()
方法。根据这两个方法的名称以及调用他们的方法类型,可以猜测这是在进行一个入栈出栈的操作,在结构体初始化的时候入栈,在结构体释放的时候出栈,也就是说 main()
函数中包含的编译器帮我们实现的方法 @autoreleasepool {}
实际的工作逻辑大概是这样的:
int main(int argc, const char *argv[]) {
/* @autoreleasepool */
{
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// do something
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
其实在 objc runtime 源码里面就能找到 autoreleasepool 的直接使用例子:
void call_load_methods(void)
{
...
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
...
}
在进行执行类和分类load()方法的时候,手动添加了一个自动释放池,那么 objc_autoreleasePoolPush() 和 objc_autoreleasePoolPop() 内部具体是怎样实现的呢,直接结合 objc 的源码来分析,查看这两个方法的实现源码,发现这两个方法其实就是直接调用了 AutoreleasePoolPage 类的 push() 和 pop() 方法,其实到这里已经知道 AutoreleasePool 是由 AutoreleasePoolPage 类来实现的。
void *objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
AutoreleasePoolPage
AutoreleasePoolPage 的实现在 objc 的 NSObject.mm 文件中,查看源码可以知道 AutoreleasePoolPage 是一个C++类,整个类的实现代码也就几百行,源码中有一段注释对 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. 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. 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.
大致意思就是:
一个线程的自动释放池是一系列指针的堆栈。堆栈中的指针要么是要释放的对象,要么是一个自动释放池的边界对象(POOL_BOUNDARY),一个释放池标识(pool token)就是一个指向自动释放池边界(POOL_BOUNDARY)的指针。当释放池从堆栈中pop时,所有在这个释放池哨兵指针(sentinel)后的对象都会被释放掉,栈被分成一个双向链接的页面链表(doubly-linked list of pages),页面(page)根据需要进行增加和删除,线程本地存储(TLS)指向存储了最新release对象的热页面(hot page)。
基础属性
简单来说,AutoreleasePool 就是由多个 AutoreleasePoolPage 组成的双向链表。还是先看 AutoreleasePoolPage 的源码,下面的代码简化了许多细节:
class AutoreleasePoolPage
{
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3;// 0xA3A3A3A3 after releasing
static size_t const SIZE = PAGE_MAX_SIZE;
static size_t const COUNT = SIZE / sizeof(id);
/* 下列字段共占用 56 字节 */
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
...
}
#define PAGE_MAX_SIZE PAGE_SIZE
#define PAGE_SIZE I386_PGBYTES
#define I386_PGBYTES 4096 /* bytes per 80386 page */
/*
当刚好只有一个pool入栈并且它从未包含任何对象时,
EMPTY_POOL_PLACEHOLDER存储在TLS中。
当顶级(即libdispatch)入栈和出栈pool但从没使用它时,
这可以节省内存。
*/
#define EMPTY_POOL_PLACEHOLDER ((id*)1)
#define POOL_BOUNDARY nil
每个 AutoreleasePoolPage 大小是 4096 字节,为什么是4096?因为虚拟内存扇区大小就是 4096,也就是 4k 对齐,可以提升读写效率。AutoreleasePoolPage 结构中除去固定字段占用的 56 字节,剩余的内存空间可以用来存放 autorelease 对象的地址。
字段说明:
- magic:这个字段是用来验证 AutoreleasePoolPage 结构完整性的,对 AutoreleasePool 的实现过程并无关系,这里暂时略过。
- thread:当前 AutoreleasePoolPage 所在的线程,AutoreleasePool 是和线程一一对应的。
- next:指向栈顶,也就是下个 autorelease 对象入栈的目标位置。
- parent:指向父节点(上一个节点)
- child:指向子节点(下一个节点)
- depth:双向链表的深度(链表的节点个数)
- hiwat:high water mark,最高水位标记,也就是记录双向链表深度达到过的最大值
宏定义:
- POOL_BOUNDARY:这个宏表示释放池的边界
- EMPTY_POOL_PLACEHOLDER:空 pool 的占位符
Tips:
以 alloc、copy、new、mutableCopy 开头的方法名创建的对象是自己生成并持有的。其它方法生成的对象会自动加入自动释放池中。
POOL_BOUNDARY
POOL_BOUNDARY 是 autoreleasepool 的边界标识符, autoreleasepool 是可以嵌套的,如下所示:
@autoreleasepool {
// do something
@autoreleasepool {
// do something
}
}
每开始一个新的 autoreleasepool,都会调用 push() 函数入栈一个 POOL_BOUNDARY 标识符到栈上,表示接下来开启了一个新的 autoreleasepool。
hotPage
其实就是通过 TLS 存储一下当前使用的 AutoreleasePoolPage 的地址,方面下次快速获取。为什么要使用 TLS,因为
objc_autoreleasePoolPush
步骤
前面我们已经知道了 objc_autoreleasePoolPush() 函数就是直接调用了 AutoreleasePoolPage 类的 push() 方法:
void *objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
接下来再看看 push() 函数的实现,简化后的代码如下:
static inline void *push()
{
id *dest = autoreleaseFast(POOL_BOUNDARY);
return dest;
}
内部也是直接调用了 autoreleaseFast() 函数,删掉一部分对逻辑无关的代码,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 并且 page 中还有可用空间:
这个时候直接调用 add() 方法将 autorelease 对象添加到 AutoreleasePoolPage 的可用栈里面; -
有 hotPage 并且 page 中已经没有可用空间:
当 hotPage 没有可用的栈空间了,这个时候就用 autoreleaseFullPage() 创建一个新 page;
新创建的 page 设置为 hotPage,然后将 autorelease 对象添加到 page 的栈中; -
没有 hotPage
如果还没有 hotPage 这个时候通过 autoreleaseNoPage() 方法创建一个 page。
接下来再说说上面三种情况中用到的 add()
、autoreleaseFullPage()
、autoreleaseNoPage()
这几个函数。
add
add()
方法其实就是一个入栈的操作,将对象加入到 AutoreleasePoolPage 的栈中,然后将栈顶指针 next 移动到下一个空闲位置,简化后的代码如下:
id *add(id obj)
{
...
id *ret = next;
*next++ = obj;
...
return ret;
}
autoreleaseNoPage
autoreleaseNoPage() 其实可以作为一个初始化的方法,当内存中还没有 AutoreleasePoolPage 存在的时候,通过这个方法创建一个新的 page,也可以理解为 page 双向链表的第一个节点,初始化完成后将 page 标记为 hotPage :
id *autoreleaseNoPage(id obj)
{
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
// 正在空占位pool后将第二个pool入栈,
// 或者正在将第一个objec入栈到空占位pool里面。
// 并且在此操作之前,入栈一个POOL_BOUNDARY表示当前空池占位符呈现的pool
pushExtraBoundary = true;
}
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// 入栈一个没有pool的对象,环境要求将进行无pool调试
objc_autoreleaseNoPool(obj);
return nil;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
// 正在入栈一个没有池的池,并且没有请求alloc-per-pool调试,
// 安装并返回空池占位符。
return setEmptyPoolPlaceholder();
}
// 新page并设置为hotPage
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// 入栈 pool 边界对象
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// 入栈 autorelease 对象
return page->add(obj);
}
在忽略特殊调试模式的情况下,这个过程可以这样理解:
- 当添加第一个 autoreleasePool,这个时候会入栈一个 POOL_BOUNDARY 标识,这个时候并不会马上去创建一个新的 AutoreleasePoolPage, 而是通过 TLS 设置一个 EMPTY_POOL_PLACEHOLDER(空池占位符);
- 接下来如果入栈的是一个 autorelease 对象,那么这个时候就创建一个 AutoreleasePoolPage 并且设置为 hotPage,然后将 POOL_BOUNDARY 标识添加到栈上,然后再把 autorelease 对象地址添加到栈上。
Tips:
为什么上面不在第一步中直接就创建一个 AutoreleasePoolPage,而只是通过 TLS 设置一个空池占位符呢?空池占位符,顾名思义,有可能这个入栈的 autoreleasePool 是一个空池,它有可能一个 autorelease 对象都不会有,这个时候花费 4096 Bytes 内存空间去创建一个并不会使用的 AutoreleasePoolPage,就会带来性能和内存空间上的浪费,所以新添加一个 autoreleasePool 的时候,仅用 EMPTY_POOL_PLACEHOLDER 标识一下这是一个空池,等后续实际真正需要的时候再创建。
autoreleaseFullPage()
这个方法的逻辑比较简单,就是当前 hotPage 栈空间已满的时候,需要创建一个新 page 链接到链表的末端,并将新创建的 page 设置为 hotPage,再往 hotPage 里面添加 autorelease 对象。
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
do {
if (page->child) {
page = page->child;
} else {
page = new AutoreleasePoolPage(page);
}
} while (page->full());
setHotPage(page);
return page->add(obj);
}
流程
整个过程的流程大致就是:
- 添加新的 autoreleasePool,TLS 设置一个空池占位符;
- 如果接下来这个 autoreleasePool 并没有入栈 autorelease 对象就结束了,那么这个时候 TLS 清空空池占位符,一切恢复初始状态;如果接下来这个 autoreleasePool 入栈了一个 autorelease 对象(如果没有 hotPage,那么就创建一个新的 AutoreleasePoolPage,并设置为 hotPage),在 hotPage 里入栈一个 POOL_BOUNDARY 和这个 autorelease 对象;
- 后续还有 autorelease 对象。如果 hotPage 还有内存空间可用,那么就直接将 autorelease 对象地址添加到栈顶;如果 hotPage 没有可用内存空间了,那么就创建一个新的 AutoreleasePoolPage,并且作为上一个 page 的子节点,创建完以后将新的 page 设置为 hotPage,然后进行后续的 autorelease 对象入栈操作。
- 如果又添加了新的 autoreleasePool,那么就重复前面的 1、2 步骤。
用伪代码表示这个流程:
push()
├── autoreleaseFast(POOL_BOUNDARY) // 添加新 pool,没有 page,设置空池占位符
│ └── autoreleaseNoPage(POOL_BOUNDARY)
│ └── setEmptyPoolPlaceholder
├── autoreleaseFast(obj) // 添加 autorealease 对象,创建新 page,入栈 POOL_BOUNDARY 和 对象
│ └── autoreleaseNoPage(obj)
│ ├── AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
│ ├── setHotPage(page);
│ ├── page->add(POOL_BOUNDARY);
│ └── page->add(obj);
├── autoreleaseFast(obj1) // 添加 autorealease 对象
│ ├── AutoreleasePoolPage *page = hotPage();
│ └── page->add(obj1)
├── ... // 添加 autorealease 对象
├── ...
├── autoreleaseFast(objn) // 添加 autorealease 对象,page 空间满了,创建新 page
│ └── autoreleaseFullPage()
│ ├── AutoreleasePoolPage *page = new AutoreleasePoolPage(lastPage);
│ ├── setHotPage(page);
│ └── page->add(objn)
├── autoreleaseFast(objn1) // 添加 autorealease 对象
│ ├── AutoreleasePoolPage *page = hotPage();
│ └── page->add(objn1)
│
...
objc_autoreleasePoolPop()
步骤
前面我们知道,objc_autoreleasePoolPush() 就是入栈了一个 POOL_BOUNDARY 在 autoreleasePool 的栈顶。再看一看 main() 函数里面,objc_autoreleasePoolPop() 就是把入栈的 POOL_BOUNDARY 出栈:
int main(int argc, const char *argv[]) {
/* @autoreleasepool */
{
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// do something
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
看看 objc_autoreleasePoolPop() 的具体实现,调用了 AutoreleasePoolPage 类的 pop() 方法:
void *objc_autoreleasePoolPop(void)
{
return AutoreleasePoolPage::pop();
}
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
// 1
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
if (hotPage()) {
pop(coldPage()->begin());
} else {
setHotPage(nil);
}
return;
}
// 2
page = pageForPointer(token);
stop = (id *)token;
// 3
page->releaseUntil(stop);
// 4
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
}
pop() 方法中删除了一些正常逻辑无关的代码,这个过程大致可以分为4个步骤:
- 空占位池相关的处理;
- pageForPointer() 找到 token 所在的 AutoreleasePoolPage;
- releaseUntil() 释放 autorelease 对象;
- kill() 杀掉 child page,清理内存,避免线程堆栈中太多垃圾导致线程堆栈溢出;
pageForPointer
根据指针取 page 的地址。前面说了 AutoreleasePoolPage 的地址都是 4k 对齐的,所以这里将指针地址与 4096 进行模运算就得到了指针在 page 中的偏移地址,然后用指针地址减去这个偏移地址就得到了 page 地址。
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE; // SIZE == 4096
result = (AutoreleasePoolPage *)(p - offset);
return result;
}
releaseUntil()
这个过程是释放栈中 autorelease 对象,首先是找到链表尾部不为空的 page, 然后从栈顶 next 一直向栈低释放 autorelease 对象,直到找到 *stop 指针所在的位置。
void releaseUntil(id *stop)
{
while (this->next != stop) {
// 1
AutoreleasePoolPage *page = hotPage();
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
// 2
page->unprotect();
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
// 3
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
}
kill()
这个方法比较简单,就是删除当前的 AutoreleasePoolPage 以及当前 AutoreleasePoolPage 的子页面,释放掉内存:
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);
}
顺便提一下 pop() 方法中第 4 步中杀掉当前页面的 child 页面的逻辑:
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
-
这里为什么会有删除子页面这样一个逻辑?
大致是这样的:如果当前页面的栈空间使用未超过一半,那么认为还有足够的空间使用,如果当前页面有子页面,那么一时半会儿还用不到后面的子页面,就直接 kill 掉把内存释放了;如果当前页面的栈空间使用超过一半了,那么就认为很快就会出现内存不够用的情况,如果当前页面有子页面的话,就保留这个一个子页面,子页面如果还有子页面,那么是要清理掉的,不用保留太多个页面, -
这些子页面是哪里来的呢?
这些页面都是前面 autorelease 用到的页面, autorelease 对象在 release 后,但是用到的页面是还未被释放掉的,也就是页面还在双向链表中,所有这里要清理掉过多的页面来避免占用了过多的线程堆栈空间,避免线程堆栈溢出。