iOS 内存管理之 autoreleasePool
一、autoreleasePool是个啥
- 一句话总结自动释放池本质就是把一筐对象延迟release;
- MRC环境下,通过 [obj autorelease] 来延迟内存的释放;
- ARC环境下,是不能手动调用,系统会自动给对象添加autorelease;
二、autoreleasePool原理
先简单总结下(ps:以下解释略显抽象):
- 首先有个筐用来记录要延迟释放的对象,这个筐的结构是由若干个以AutoreleasePoolPage对象为结点的双向链表组成;
- 其次得有个方法往这个筐里装对象,也还得有对应从这个筐里批量释放的对象的方法;那这两个方法可以解析源码发现push和pop方法;
这样,就可以实现延迟释放对象的能力;
三、源码赏析
ps:md写的时候,想从苹果开发开源项目平台找源码看,can’t be found了;
以main文件为例:
int main(int argc, char * argv[]) {@autoreleasepool {NSObject * rpObj = [[NSObject alloc] init];}
}
- 先转换成main.cpp文件查看底层调用方法:
在终端运行以下命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
- 查看main.cpp文件:
int main(int argc, char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; ///调用了objc_autoreleasePoolPushNSObject * 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方法;
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,并初始化线程的页栈。
// 全局的自动释放池页栈(每个线程独立)
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 时对象销毁);
若当前页释放为空,自动切换到前一页(栈底方向),继续释放,直到遍历到目标边界; - 清理空页:释放完成后,若当前页为空且不是栈中唯一页,将其从页栈中移除,优化内存占用。
// 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的概念,也就是这个承载延迟释放对象的筐;
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]; // 柔性数组,实际存储自动释放对象的区域
};
综上,有筐,有进筐方法,出筐方法;
