「iOS」————自动释放池底层原理
iOS学习
- AutoReleasePool底层原理及总结
- 自动释放池
- 生命周期
- 结构
- 自动释放池中的栈
- POOL_SENTINEL(哨兵对象)
- 实现
- 入栈
- autoreleaseFast()
- page->add 添加对象
- autoreleaseFullPage(当前 hotPage 已满)
- autoreleaseNoPage(没有 hotPage)
- 出栈
- objc_autoreleasePoolPop
- AutoreleasePoolPage::pop
- pageForPointer 获取 AutoreleasePoolPage
- releaseUntil 释放对象
- 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
的定义如下:
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
:
@autoreleasepool { // 外层池id a = ...; // Aid b = ...; // B@autoreleasepool { // 内层池id c = ...; // Cid d = ...; // D} // 内层池销毁id e = ...; // Eid f = ...; // F
} // 外层池销毁
对于以上代码,在内层池销毁前,链表的存储类似D->C->S2->B->A->S1。其中S为哨兵节点,销毁时外层池时,遇到第一个哨兵节点停止
注意: 每个自动释放池只有一个哨兵对象,并且哨兵对象在第一页。
实现
在我们的编程中,每一个main文件都有以下代码:
int main(int argc, char * argv[]) {@autoreleasepool {return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));}
}
在整个main函数中,只有一个autoreleasepool
块,在块中之包含了一行代码,这行代码将所有的事件、消息全部交给了UIApplication
来处理也就是说整个 iOS 的应用都是包含在一个autoreleasepool的 block 中的
上述的aotuoreleasepool
通过源码编译发现,被转换为为一个 __AtAutoreleasePool
结构体:
struct __AtAutoreleasePool {__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}void * atautoreleasepoolobj;
};
这个结构体会在初始化时调用 objc_autoreleasePoolPush()
方法,会在析构时调用 objc_autoreleasePoolPop
方法。
至此,我们可以分析出,单个自动释放池的执行过程就是objc_autoreleasePoolPush()
—> [object autorelease]
—> objc_autoreleasePoolPop(void *)
。
因此得到这个main函数的实际工作逻辑:
int main(int argc, const char * argv[]) {{void * atautoreleasepoolobj = objc_autoreleasePoolPush();// do whatever you wantobjc_autoreleasePoolPop(atautoreleasepoolobj);}return 0;
}
所以autoreleasepool的实现主要靠objc_autoreleasePoolPush()
和objc_autoreleasePoolPop()
来实现
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()
void *objc_autoreleasePoolPush(void) {return AutoreleasePoolPage::push();
}
AutoreleasePoolPage::push()
static inline void *push() {return autoreleaseFast(POOL_SENTINEL);
}
该函数就是调用了关键的方法 autoreleaseFast
,并传入哨兵对象POOL_SENTINEL
autoreleaseFast()
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 添加对象
//入桟对象
id *add(id obj){ASSERT(!full());unprotect();//传入对象存储的位置(比' return next-1 '更快,因为有别名)id *ret = next; //将obj压桟到next指针位置,然后next进行++,即下一个对象存储的位置*next++ = obj;protect();return ret;}
压栈操作,将对象加入 AutoreleasePoolPage
然后移动栈顶的指针
autoreleaseFullPage(当前 hotPage 已满)
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)
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
void objc_autoreleasePoolPop(void *ctxt) {AutoreleasePoolPage::pop(ctxt);
}
该方法传入的参数是push压栈后返回的哨兵对象
,即ctxt
,其目的是避免出栈混乱,防止将别的对象出栈
但是传入不是哨兵对象而是传入其它的指针也是可行的,会将自动释放池释放到相应的位置。
pop
源码实现,主要由以下几步- 空页面的处理,并
根据token获取page
- 容错处理
- 通过
popPage
出栈页
- 空页面的处理,并
//出栈
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
template<bool allowDebug>static voidpopPage(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
方法主要是通过内存地址的操作,获取当前指针所在页的首地址:
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 释放对象
void releaseUntil(id *stop)
{// 这里没有使用递归, 防止发生栈溢出while (this->next != stop) { // 一直循环到 next 指针指向 stop 为止// Restart from hotPage() every time, in case -release // autoreleased more objectsAutoreleasePoolPage *page = hotPage(); // 取出 hotPagewhile (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 指向的内存清空为SCRIBBLEpage->protect(); // 如果需要的话, 设置内存锁定if (obj != POOL_BOUNDARY) { // 如果取出的对象不是边界符objc_release(obj); // 给取出来的对象进行一次 release 操作}}setHotPage(this); // 将本节点设置为 hotPage#if DEBUG// we expect any children to be completely emptyfor (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() 方法
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,通常在整个进程生命周期内都不会被释放,作为池的“根”。