自动释放池
AutoreleasePool自动释放池用来延迟对象的释放时机,将对象加入到自动释放池后这个对象不会立即释放,等到自动释放池被销毁后才将里边的对象释放。
自动释放池的生命周期
- 从程序启动到加载完成,主线程对应的runloop会处于休眠状态,等待用户交互来唤醒runloop
- 用户的每一次交互都会启动一次runloop,用于处理用户的所有点击、触摸事件等
- runloop在监听到交互事件后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中
- 在一次完整的runloop结束之前,会向自动释放池中所有对象发送release消息,然后销毁自动释放池
AutorealeasePool结构
每一个AutorealeasePool都是由一系列的 AutoreleasePoolPage
组成的,并且每一个 AutoreleasePoolPage
的大小都是 4096
字节 。
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字节
...
}
自动释放池中的栈
如果我们的一个 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实现
在每个文件中的main.m文件中都有下面这段代码:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
在整个main函数中只有一个autoreleasepool
块,在块中只包含了一行代码,这行代码将所有的事件、消息全部交给了 UIApplication
来处理,也就是说整个 iOS 的应用都是包含在一个autoreleasepool的 block 中的
将main.m文件通过$ clang -rewrite-objc main.m
重新编译生成发现aotuoreleasepool
被转换为为一个 __AtAutoreleasePool
结构体:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
这个结构体会在初始化时调用 objc_autoreleasePoolPush()
方法,会在析构时调用 objc_autoreleasePoolPop
方法。
所以实际上 main
函数在实际工作时其实是这样的:
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()
来实现
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);
}
在这个方法中传入不是哨兵对象而是传入其它的指针也是可行的,会将自动释放池释放到相应的位置。
AutoreleasePoolPage::pop
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();
}
}
}
该静态方法总共做了三件事情:
- 使用
pageForPointer
获取当前token
所在的AutoreleasePoolPage
- 调用
releaseUntil
方法释放栈中的对象,直到stop
- 调用
child
的kill
方法
- pop之后,所有
child page
肯定都为空了,且当前page
一定是hotPage
- 系统为了节约内存,判断,如果当前
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
result = 0x100816000
而最后调用的方法 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 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
}
调用者是用 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
方法
入栈(push)
在页中压栈普通对象主要是通过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对象为当前页
Autorelease对象什么时候释放
当创建了局部释放池时,会在@autoreleasepool{}的右大括号结束时释放,及时释放对象大幅度降低程序的内存占用。在没有手动加@autoreleasepool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的(每个线程对应一个runloop),而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池。
AutoreleasePool能否嵌套使用
可以嵌套使用,其目的是可以控制应用程序的内存峰值,使其不要太高 可以嵌套的原因是因为自动释放池是以栈为节点,通过双向链表的形式连接的,且是和线程一一对应的 自动释放池的多层嵌套其实就是不停的push哨兵对象,在pop时,会先释放里面的,在释放外面的
哪些对象可以加入AutoreleasePool?alloc创建可以吗?
使用new、alloc、copy关键字生成的对象和retain了的对象需要手动释放,不会被添加到自动释放池中 设置为autorelease的对象不需要手动释放,会直接进入自动释放池 所有 autorelease 的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中
AutoreleasePool的释放时机是什么时候?
App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()
。
第一个 Observer 监视一个事件:
监听 Entry(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush()
创建自动释放池。其 order 是 -2147483647,优先级最高,保证创 建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件:
BeforeWaiting(准备进入休眠) 时调用 _objc_autoreleasePoolPop()
和 _objc_autoreleasePoolPush()
释放旧的池并创建新池;
Exit(即 将退出 Loop) 时调用_objc_autoreleasePoolPop()
来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
thread 和 AutoreleasePool的关系
每个线程都有与之关联的自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈,对于当前线程来说,释放对象会被压栈到栈顶,线程停止时,会自动释放与之关联的自动释放池
RunLoop 和 AutoreleasePool的关系
主程序的RunLoop在每次事件循环之前之前,会自动创建一个 autoreleasePool 并且会在事件循环结束时,执行drain操作,释放其中的对象