一、autoreleasePool是个啥
- 一句话总结自动释放池本质就是把一筐对象延迟release;
- MRC环境下,通过 [obj autorelease] 来延迟内存的释放;
- ARC环境下,是不能手动调用,系统会自动给对象添加autorelease;
二、autoreleasePool原理
先简单总结下(ps:以下解释略显抽象):
- 首先有个筐用来记录要延迟释放的对象,这个筐的结构是由若干个以AutoreleasePoolPage对象为结点的双向链表组成;
- 其次得有个方法往这个筐里装对象,也还得有对应从这个筐里批量释放的对象的方法;那这两个方法可以解析源码发现push和pop方法;
这样,就可以实现延迟释放对象的能力;
三、源码赏析
ps:md写的时候,想从苹果开发开源项目平台找源码看,can't be found了;
以main文件为例:
objectivec
int main(int argc, char * argv[]) {
@autoreleasepool {
NSObject * rpObj = [[NSObject alloc] init];
}
}
- 先转换成main.cpp文件查看底层调用方法:
objectivec
在终端运行以下命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
- 查看main.cpp文件:
objectivec
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; ///调用了objc_autoreleasePoolPush
NSObject * rpObj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
}
}
- 有这么一个叫做
__AtAutoreleasePool的结构体,这里可以看到装筐和出筐的push和pop方法;
objectivec
struct __AtAutoreleasePool {
//构造函数
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
//析构函数
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
- 简单看下push和pop方法
先看push方法,核心逻辑有三块:
- 获取当前线程的 "热页"(hotPage):自动释放池由多个 AutoreleasePoolPage 组成栈结构(每个页大小为 4096 字节),"热页" 是当前正在使用的页(栈顶页)。
- 推入 "池边界"(POOL_BOUNDARY):每个自动释放池对应一个特殊标记 POOL_BOUNDARY(本质是 nil),push() 方法会将该标记添加到热页中,作为当前池的 "起点"。后续添加的自动释放对象会依次存入页中,直到调用 objc_autoreleasePoolPop 时,释放从 "起点" 到当前位置的所有对象。
- 无页时创建新页栈:若线程首次使用自动释放池(无页),autoreleaseNoPage() 会创建第一个 AutoreleasePoolPage,并初始化线程的页栈。
objectivec
// 全局的自动释放池页栈(每个线程独立)
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
void *objc_autoreleasePoolPush(void) {
// 获取当前线程的自动释放池页栈
AutoreleasePoolPage *page = hotPage();
if (page) {
// 若存在当前页,直接在页中添加一个"池边界"(POOL_BOUNDARY)
return page->push();
} else {
// 若不存在页,创建新的页栈,并添加池边界
return autoreleaseNoPage();
}
}
再看pop方法,核心逻辑有四块:
- 获取热页:先拿到当前线程的 AutoreleasePoolPage 栈顶页(hotPage),确保操作的是当前正在使用的页;
- 验证令牌:传入的 ctxt 必须是 push 时返回的 POOL_BOUNDARY(池边界标记),避免释放错误范围的对象(如传入无效指针会直接报错);
- 反向释放对象:调用 releaseUntil(stop) 从栈顶反向遍历,直到遇到 stop(即 POOL_BOUNDARY):
每次取出栈顶的自动释放对象,调用 objc_release 减少其引用计数(计数为 0 时对象销毁);
若当前页释放为空,自动切换到前一页(栈底方向),继续释放,直到遍历到目标边界; - 清理空页:释放完成后,若当前页为空且不是栈中唯一页,将其从页栈中移除,优化内存占用。
objectivec
// pop 函数:传入 push 返回的"池令牌"(POOL_BOUNDARY),释放池内对象
OBJC_EXPORT void objc_autoreleasePoolPop(void *ctxt) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
void objc_autoreleasePoolPop(void *ctxt) {
// 1. 获取当前线程的热页(正在使用的 AutoreleasePoolPage 栈顶页)
AutoreleasePoolPage *page = hotPage();
if (!page) {
// 无有效页,直接返回(异常场景)
return;
}
// 2. 验证传入的"池令牌"是 POOL_BOUNDARY(push 时存入的边界标记)
if (ctxt != POOL_BOUNDARY) {
// 若令牌无效(如传入错误指针),释放到栈底并报错
page->releaseUntil(POOL_BOUNDARY);
_objc_fatal("invalid autorelease pool token");
}
// 3. 释放从"当前池边界"到"栈顶"的所有自动释放对象
page->releaseUntil(ctxt);
// 4. 清理空页:若当前页释放后无对象,且不是唯一页,将其从栈中移除
if (page->empty() && !page->isSinglePage()) {
page->kill();
}
}
// 核心辅助方法:释放从当前位置到目标边界(ctxt)的所有对象
void AutoreleasePoolPage::releaseUntil(void *stop) {
// 从栈顶开始,反向遍历对象
while (this->next != stop) {
// 获取当前栈顶对象
AutoreleasePoolPage *page = hotPage();
while (page->empty()) {
// 若当前页空,切换到前一页(栈底方向)
page = page->parent;
setHotPage(page);
}
// 取出栈顶对象,指针下移
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); // 擦除痕迹,避免野指针
// 释放对象(调用 objc_release,触发引用计数-1)
objc_release(obj);
}
// 更新热页(确保后续操作指向正确页)
setHotPage(this);
}
- 从上面看源码引出了
AutoreleasePoolPage的概念,也就是这个承载延迟释放对象的筐;
objectivec
class AutoreleasePoolPage {
// 1. 常量定义
static const size_t SIZE = 4096; // 每页大小(4KB,固定)
static const size_t COUNT = SIZE / sizeof(id); // 每页可存储的对象数(约 1024 个,因头部占部分空间)
static const id POOL_BOUNDARY = nil; // 池边界标记(push/pop 的核心标记)
// 2. 成员变量(页的元数据 + 存储区)
magic_t magic; // 校验页完整性的魔术值(调试用)
id *next; // 指向栈顶的下一个空闲位置(即下一个对象的存储地址)
pthread_t thread; // 所属线程(线程隔离,每个线程的页栈独立)
AutoreleasePoolPage *parent; // 前一页(栈底方向)
AutoreleasePoolPage *child; // 后一页(栈顶方向)
uint32_t depth; // 页在栈中的深度(用于调试)
uint32_t hiwat; // 历史最高存储量(用于调试)
// 3. 存储区(紧跟在成员变量后,占 SIZE - 元数据大小的空间)
id objects[0]; // 柔性数组,实际存储自动释放对象的区域
};
综上,有筐,有进筐方法,出筐方法;