iOS学习
- AutoReleasePool底层原理及总结
-
- 自动释放池
-
- 生命周期
- 结构
- 实现
-
- 入栈
-
- autoreleaseFast()
- [page->add 添加对象](#page->add 添加对象)
- [autoreleaseFullPage(当前 hotPage 已满)](#autoreleaseFullPage(当前 hotPage 已满))
- [autoreleaseNoPage(没有 hotPage)](#autoreleaseNoPage(没有 hotPage))
- 出栈
-
- objc_autoreleasePoolPop
- AutoreleasePoolPage::pop
- [pageForPointer 获取 AutoreleasePoolPage](#pageForPointer 获取 AutoreleasePoolPage)
- [releaseUntil 释放对象](#releaseUntil 释放对象)
- [kill() 方法](#kill() 方法)
- 总结
- 冷页(coldPage)和热页(hotPage)
AutoReleasePool底层原理及总结
自动释放池
AutoreleasePool自动释放池用来延迟对象的释放时机,将对象加入到自动释放池后,这个对象不会立即释放,等到自动释放池销毁后才会将里面的对象释放

生命周期
- 从程序启动到加载完成,主线程对应的runloop会处于休眠状态,等待用户来唤醒runloop
- 用户的每一次交互都会开启一次runloop,用于处理用户的所有点击、触摸事件
- runloop在监听到交互事件后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中
- 在一次完整的runloop结束之前,会将自动释放池中所有对象发送releae消息,然后销毁自动释放池。
总结一下:首先一个Observer监视Entry,在即将进入Loop时,创建自动释放池,并且这件事优先级最高,确保创建自动释放池发生在其他所有回调之前。然后另一个Observer监视:BeforeWaiting(准备进入休眠)和Exit(即将退出Loop)。
BeforeWaiting调用_objc_autoreleasePoolPop()
Exit调用 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
结构
自动释放池其本质也是一个对象
,其类型__AtAutoreleasePool
是一个结构体,有构造函数 + 析构函数
,结构体定义的对象在作用域结束后,会自动调用析构函数。在创建时,一般也会调用构造函数
每一个AutorealeasePool都是由一系列的 AutoreleasePoolPage
组成的,并且一个page的大小是4096字节。而在其构造函数中对象的压栈位置
,是从首地址+56
开始的,所以可以一页中实际可以存储 4096-56 = 4040字节
,转换成对象是 4040 / 8 = 505
个,即一页最多可以存储505个对象
,其中第一页有哨兵对象
只能存储504
个。
AutorealeasePool
就是由AutoreleasePoolPage
构成的双向链表,AutoreleasePoolPage
是双向链表的节点

AutoreleasePoolPage
的定义如下:
objectivec
class AutoreleasePoolPage
{
//magic用来校验AutoreleasePoolPage的结构是否完整
magic_t const magic; // 16字节
//指向最新添加的autoreleased对象的下一个位置,初始化时指向begin();
id *next; // 8字节
//thread指向当前线程
pthread_t const thread; // 8字节
//parent指向父节点,第一个节点的parent指向nil;
AutoreleasePoolPage * const parent; // 8字节
//child 指向子节点,第一个节点的child指向nil;
AutoreleasePoolPage *child; // 8字节
//depth 代表深度,从0开始往后递增1;
uint32_t const depth; // 4字节
//hiwat 代表high water mark;
uint32_t hiwat; // 4字节
...
}
- magic 检查校验完整性的变量
- next 指向新加入的autorelease对象
- thread page当前所在的线程,AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
- parent 父节点 指向前一个page
- child 子节点 指向下一个page
- depth 链表的深度,节点个数
- hiwat high water mark 数据容纳的一个上限
- EMPTY_POOL_PLACEHOLDER 空池占位
- POOL_BOUNDARY 是一个边界对象 nil,之前的源代码变量名是 POOL_SENTINEL哨兵对象,用来区别每个page即每个 AutoreleasePoolPage 边界
- PAGE_MAX_SIZE = 4096, 为什么是4096呢?其实就是虚拟内存每个扇区4096个字节,4K对齐的说法。
- COUNT 一个page里对象数
自动释放池中的栈
如果我们的一个 AutoreleasePoolPage
被初始化在内存的 0x100816000 ~ 0x100817000
中,它在内存中的结构如下:

其中有 56 bit 用于存储 AutoreleasePoolPage
的成员变量,剩下的 0x100816038 ~ 0x100817000
都是用来存储加入到自动释放池中的对象。
begin()
和 end()
这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000
这一范围的边界地址。
next
指向了下一个为空的内存地址,如果 next
指向的地址加入一个 object
,它就会如下图所示移动到下一个为空的内存地址中:
POOL_SENTINEL(哨兵对象)
POOL_SENTINEL就是一个哨兵对象,它是一个宏,值为nil,标志着一个自动释放池的边界。
在每个自动释放池初始化调用 objc_autoreleasePoolPush
的时候,都会把一个 POOL_SENTINEL
push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL
哨兵对象。
而当方法 objc_autoreleasePoolPop
调用时,就会向自动释放池中的对象发送 release
消息,直到第一个 POOL_SENTINEL
:

objective-c
@autoreleasepool { // 外层池
id a = ...; // A
id b = ...; // B
@autoreleasepool { // 内层池
id c = ...; // C
id d = ...; // D
} // 内层池销毁
id e = ...; // E
id f = ...; // F
} // 外层池销毁
对于以上代码,在内层池销毁前,链表的存储类似D->C->S2->B->A->S1。其中S为哨兵节点,销毁时外层池时,遇到第一个哨兵节点停止
注意: 每个自动释放池只有一个哨兵对象,并且哨兵对象在第一页。
实现
在我们的编程中,每一个main文件都有以下代码:
objectivec
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
在整个main函数中,只有一个autoreleasepool
块,在块中之包含了一行代码,这行代码将所有的事件、消息全部交给了UIApplication
来处理也就是说整个 iOS 的应用都是包含在一个autoreleasepool的 block 中的
上述的aotuoreleasepool
通过源码编译发现,被转换为为一个 __AtAutoreleasePool
结构体:
objectivec
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
这个结构体会在初始化时调用 objc_autoreleasePoolPush()
方法,会在析构时调用 objc_autoreleasePoolPop
方法。
至此,我们可以分析出,单个自动释放池的执行过程就是objc_autoreleasePoolPush()
---> [object autorelease]
---> objc_autoreleasePoolPop(void *)
。
因此得到这个main函数的实际工作逻辑:
objectivec
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
所以autoreleasepool的实现主要靠objc_autoreleasePoolPush()
和objc_autoreleasePoolPop()
来实现
objectivec
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
而在objc_autoreleasePoolPush()
和objc_autoreleasePoolPop()
中又分别调用了AutoreleasePoolPage
类的push和pop方法。
入栈

objc_autoreleasePoolPush()
首先调用objc_autoreleasePoolPush()
objectivec
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
AutoreleasePoolPage::push()
objectivec
static inline void *push() {
return autoreleaseFast(POOL_SENTINEL);
}
该函数就是调用了关键的方法 autoreleaseFast
,并传入哨兵对象POOL_SENTINEL
autoreleaseFast()
objectivec
static inline id *autoreleaseFast(id obj)
{
//1. 获取当前操作页
AutoreleasePoolPage *page = hotPage();
//2. 判断当前操作页是否满了
if (page && !page->full()) {
//如果未满,则压桟
return page->add(obj);
} else if (page) {
//如果满了,则安排新的页面
return autoreleaseFullPage(obj, page);
} else {//页面不存在,则新建页面
return autoreleaseNoPage(obj);
}
}
hotPage 可以为当前正在使用的 AutoreleasePoolPage
上述代码一共分为三种情况
- 有
hotPage
并且当前page
不满
调用 page->add(obj)
方法将对象添加至 AutoreleasePoolPage
的栈中
- 有
hotPage
并且当前page
已满
调用 autoreleaseFullPage
初始化一个新的页接着调用 page->add(obj)
方法将对象添加至 AutoreleasePoolPage
的栈中
- 无hotPage
调用 autoreleaseNoPage
创建一个 hotPage
,接着调用 page->add(obj)
方法将对象添加至 AutoreleasePoolPage
的栈中
总而言之就是最后调用 page->add(obj)
将对象添加到自动释放池中
page->add 添加对象
objectivec
//入桟对象
id *add(id obj)
{
ASSERT(!full());
unprotect();
//传入对象存储的位置(比' return next-1 '更快,因为有别名)
id *ret = next;
//将obj压桟到next指针位置,然后next进行++,即下一个对象存储的位置
*next++ = obj;
protect();
return ret;
}
压栈操作,将对象加入 AutoreleasePoolPage
然后移动栈顶的指针
autoreleaseFullPage(当前 hotPage 已满)
objectivec
static 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);
}
从传入的 page
开始遍历整个双向链表,直到查找到一个未满的 AutoreleasePoolPage
如果找到最后还是没找到创建一个新的 AutoreleasePoolPage
将找到的或者构建的page
标记成 hotPage
,然后调动上面分析过的 page->add
方法添加对象。
autoreleaseNoPage(没有 hotPage)
objectivec
static id *autoreleaseNoPage(id obj) {
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
if (obj != POOL_SENTINEL) {
page->add(POOL_SENTINEL);
}
return page->add(obj);
}
创建一个新的page
,并且将新的page
设置为hotpage
。接着调用page->add
方法添加POOL_SENTINEL
对象,来确保在 pop
调用的时候,不会出现异常。最后,将 obj
添加到autoreleasepool中
既然当前内存中不存在 AutoreleasePoolPage
,就要从头开始构建这个自动释放池的双向链表 ,也就是说,新的 AutoreleasePoolPage
是没有 parent
指针的。
出栈

objc_autoreleasePoolPop
objectivec
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
该方法传入的参数是push压栈后返回的哨兵对象
,即ctxt
,其目的是避免出栈混乱,防止将别的对象出栈
但是传入不是哨兵对象而是传入其它的指针也是可行的,会将自动释放池释放到相应的位置。
pop
源码实现,主要由以下几步- 空页面的处理,并
根据token获取page
- 容错处理
- 通过
popPage
出栈页
- 空页面的处理,并
objectivec
//出栈
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
//判断对象是否是空占位符
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
//如果是空占位符
// Popping the top-level placeholder pool.
//获取当前页
page = hotPage();
if (!page) {
// Pool was never used. Clear the placeholder.
//如果当前页不存在,则清除空占位符
return setHotPage(nil);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
//如果当前页存在,则将当前页设置为coldPage,token设置为coldPage的开始位置
page = coldPage();
token = page->begin();
} else {
//获取token所在的页,可以传入非哨兵对象
page = pageForPointer(token);
}
stop = (id *)token;
//判断最后一个位置,是否是哨兵
if (*stop != POOL_BOUNDARY) {
//最后一个位置不是哨兵,即最后一个位置是一个对象
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 {
//如果是第一个位置,且有父节点,则出现了混乱
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
return popPageDebug(token, page, stop);
}
//出栈页
return popPage<false>(token, page, stop);
}
-
判断 token 是否为"空占位符"):
-
是:处理 hotPage/coldPage 逻辑。
-
否:找到 token 所在的 page。
-
-
检查 stop 是否为哨兵(POOL_BOUNDARY):
- 不是:如果是最冷页起始位置且无父节点,正常;否则报错。
-
.是否调试模式:
-
是:走调试分支。
-
否:走正常分支。
-
-
调用 popPage 释放对象。
空占位符(EMPTY_POOL_PLACEHOLDER)
-
作用:标记"最外层 pool"的存在(即你还没有真正的 autorelease pool,只是做了个占位)。
-
存储位置:通常存储在 TLS(线程本地存储)中,作为 pool stack 的初始值。
-
什么时候用:当你还没有创建任何 autorelease pool 时,系统会用 EMPTY_POOL_PLACEHOLDER 作为 pool stack 的初始标记。这样 pop 操作时能区分"真的 pool"还是"只是个占位符"。
-
不是实际存储在 page 里的对象,而是 pool stack 的特殊标记。
AutoreleasePoolPage::pop
objectivec
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
if (allowDebug && PrintPoolHiwat) printHiwat();
//出桟当前操作页面对象
page->releaseUntil(stop);
// 删除空子项
if (allowDebug && DebugPoolAllocation && page->empty()) {
//特殊情况:在每个页面池调试期间删除所有内容
//获取当前页面
AutoreleasePoolPage *parent = page->parent;
//将当前页面杀掉
page->kill();
//设置父节点页面为当前操作页
setHotPage(parent);
} else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) {
//特殊情况:当调试丢失的自动释放池时,删除所有pop(top)
page->kill();
setHotPage(nil);
} else if (page->child) {
//滞后:如果页面超过一半是满的,则保留一个空的子节点
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
传入的allowDebug
为false则通过releaseUntil
出栈当前页stop
位置之前的所有对象,即向栈中的对象发送release消息
,直到遇到传入的哨兵对象
。就是将这整个池释放掉。
该静态方法总共做了三件事情:
- 使用
pageForPointer
获取当前token
所在的AutoreleasePoolPage
- 调用
releaseUntil
方法释放栈中的 对象,直到stop
- 调用
child
的kill
方法
注意:该方法是一个析构函数,即在释放的时候使用。因此:
- pop之后,所有
child page
肯定都为空了,且当前page
一定是hotPa
- 系统为了节约内存,判断,如果当前
page
空间使用少于一半,就释放掉所有的child page
,如果当前page
空间使用大于一半,就从孙子page
开始释放,预留一个child page
。
pageForPointer 获取 AutoreleasePoolPage
pageForPointer
方法主要是通过内存地址的操作,获取当前指针所在页的首地址:
objectivec
static AutoreleasePoolPage *pageForPointer(const void *p) {
return pageForPointer((uintptr_t)p);
}
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(SIZE为4096, 0x1000)
result = 0x100816000 :通过 p - (p % SIZE) 得到当前 page 的起始地址(基址)。
而最后调用的方法 fastCheck()
用来检查当前的 result
是不是一个 AutoreleasePoolPage
。
通过检查
magic_t
结构体中的某个成员是否为0xA1A1A1A1
。
releaseUntil 释放对象
objectivec
void releaseUntil(id *stop)
{
// 这里没有使用递归, 防止发生栈溢出
while (this->next != stop) { // 一直循环到 next 指针指向 stop 为止
// Restart from hotPage() every time, in case -release
// autoreleased more objects
AutoreleasePoolPage *page = hotPage(); // 取出 hotPage
while (page->empty()) { // 从节点 page 开始, 向前找到第一个非空节点
page = page->parent; // page 非空的话, 就向 page 的 parent 节点查找
setHotPage(page); // 把新的 page 节点设置为 HotPage
}
page->unprotect(); // 如果需要的话, 解除 page 的内存锁定
id obj = *--page->next; // 先将 next 指针向前移位, 然后再取出移位后地址中的值
memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); // 将 next 指向的内存清空为SCRIBBLE
page->protect(); // 如果需要的话, 设置内存锁定
if (obj != POOL_BOUNDARY) { // 如果取出的对象不是边界符
objc_release(obj); // 给取出来的对象进行一次 release 操作
}
}
setHotPage(this); // 将本节点设置为 hotPage
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}
主要是通过循环遍历
,判断对象是否等于stop,其目的是释放stop之前
的所有的对象,
调用者是用 pageForPointer() 找到的
, token 所在的 page 节点, 参数为 token. 这个函数主要操作流程就是, 从 hotPage 开始, 使用 next 指针遍历
存储在节点里的 autorelease 对象列表
, 对每个对象进行一次 release 操作
, 并且把 next 指向的指针清空, 如果 hotPage 里面的对象全部清空, 则继续循环向前取 parent 并继续用 next 指针遍历 parent, 一直到 next 指针指向的地址为 token 为止. 因为 token 就在 this 里面, 所以这个时候的 hotPage 应该是 this.
kill() 方法
objectivec
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; // child 置空
page->protect(); // 如果需要的话, 设置内存锁定
}
delete deathptr; // 回收刚刚保留的节点, 重载 delete, 内部调用 free
} while (deathptr != this);
}
自动释放池中需要 release 的对象都已操作完成, 此时 hotPage 之后的 page 节点都已经清空了, 需要把这些节点的内存都回收, 操作方案就是从最后一个节点, 遍历到调用者节点, 挨个回收
总结
结构上:
- 自动释放池是由
AutoreleasePoolPage
以双向链表的方式实现的,每一个AutoreleasePoolPage
所占内存大小为4096字节,其中56字节用于存储结构体中的成员变量。 - autoreleasepool在初始化时,内部是调用
objc_autoreleasePoolPush
方法 - autoreleasepool在调用析构函数释放时,内部是调用
objc_autoreleasePoolPop
方法
入栈
在页中压栈普通对象主要是通过next指针递增进行的
-
当没有pool,即只有空占位符(存储在tls中)时,则创建页,压栈哨兵对象
-
当页未满,将autorelease对象插入到栈顶next指针指向的位置(向一个对象发送autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置)
-
当页满了(next指针马上指向栈顶),建立下一页page对象,设置页的child对象为新建页,新page的next指针被初始化在栈底(begin的位置),下次可以继续向栈顶添加新对象。

出桟(pop)
在页中出栈普通对象主要是通过next指针递减进行的
-
根据传入的哨兵对象地址找到哨兵对象所处的page
-
在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置.(从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page(在一个page中,是从高地址向低地址清理))
-
当页空了时,需要赋值页的parent对象为当前页

冷页(coldPage)和热页(hotPage)
AutoreleasePoolPage 的结构
AutoreleasePool 的实现是一个双向链表结构,每个 page(页)可以存放一批 autorelease 的对象。多个 page 连接起来,形成一个"池栈"。
-
hotPage:当前正在使用的、最顶层的 page。新 autorelease 的对象会被加到 hotPage 上。
-
coldPage:最底层的 page(链表的头部),也就是最早分配的 page,通常在整个进程生命周期内都不会被释放,作为池的"根"。