在前面一篇文章中简单归纳总结了一下OC对象的内存管理相关的内容, 还剩余一个内容没有整理到, 就是autorelease.
我们前面说到OC对象中很多非allocXXX, copyXXX 的方法创建的对象, 在该方法返回时候一般会调用autorelease方法, 将对象添加到AutoReleasePool中, 它会在特定的时间给添加到自动释放池的对象都发送release消息, 从而能够使对象延迟释放.
1. AutoReleasePool的启动并在Runloop注册Observer
AutoReleasePool依赖Runloop事件循环, App在启动时, 会在主线程中自动创建一个自动释放池, 然后在整个Runloop运行过程中, 如果产生一些autorelease对象, 会自动添加到这个自动释放池中.
这个自动释放池, 会在Runloop中注册几个Observer, 分别如下:
- Runloop 进入
Entry进入循环时时, 调用AutoReleasePoolPush() - Runloop 进入
Exit退出时, 调用AutoReleasePoolPop() - Runloop进入
beforeWaiting即将休眠时, 先调用AutoReleasePool::Pop()再调用AutoReleasePool::Push(), 此时休眠以前主线程产生的autorelease对象就会被发送release消息.
2. AutoReleasePoolPage的内存结构
网上有很多总结AutoReleasePoolPush()与AutoReleasePoolPop()的方法, 他们直接调用的一个底层类AutoReleasePoolPage的两个类方法 -- AutoReleasePoolPage::Push()和AutoReleasePoolPage::Pop(). 对于AutoReleasePoolPage的结构这里简单贴一下源码:
// 双向链表的节点!!!
class AutoReleasePoolPage {
...
pthread_t const thread; // 每个自动释放池会关联唯一个线程!
static size_t const SIZE = ... // 每个节点的内存大小, 目前是4096字节
static size_t const COUNT = ... // 整个链表的大小
// 节点的数据区是: 栈式结构
id *next; // 栈顶指针!!! 指向下一个可存放autorelease对象地址的位置
// pool_boundary 是哨兵对象, 目前直接存储 nil 表示
// 双向链表的前驱和后继节点
AutoreleasePoolPage *const parent;
AutoreleasePoolPage *child;
...
}
上面伪代码和注释写的非常清楚了, 这里再归纳一下:
- 自动释放池实际是一个
双向链表, 每个node节点的size是 4096字节!!! - 每个链表的节点拥有数据存储区, 使用类似
栈结构存储autorelease对象的地址!!! - 除了存储
autorelease对象地址, 还可能存储pool_boundary作为哨兵, 帮助解决autoReleasePool嵌套问题. - 一个
AutoReleasePool仅关联一个thread线程!!!
简单解释一下对于嵌套问题:
对于上面的pool_boundary可以简单理解, 在调用AutoReleasePoolPush时, 会插入一个nil到栈中. 当调用AutoReleasePoolPop时, 将栈顶到最近的nil之间的autorelease对象pop出栈, 并逐个发送release消息. 即使有多层嵌套也比较好理解!
另外, 为了防止单个节点数据爆表, Apple在设计的时, 限制了每个节点存储的autorelease对象的数量. 因此只要单个node满了. 就会创建新的节点.
此外, 还有一个函数比如hotPage(), 就是最新autorelease对象插入的node page页!!!
3. 相关问题
Q: ARC环境下, autorelease在什么时候释放?
- 如果手动添加了
@autorelease{}, 在作用域结束时释放 - 如果在主线程, 那么Runloop在
beforeWaiting通知发送时, 给该对象release
Q: 子线程默认不会开启Runloop, autorelease对象是如何处理的?
- 如果我们自己在子线程创建时包装了
@autorelease{}, 会由这个自动释放池释放 - 如果没有创建, 但是产生了autorelease对象, 那么会调用一个
autoreleaseNoPage方法. 这个方法会自动创建一个hotpage, 也就是一个AutoreleasePoolPage, 然后将autorelease对象交给它管理
static id *autoreleaseNoPage(id obj) {
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
ASSERT(!hotPage());
bool pushExtraBoundary = false;
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
return page->add(obj);
}
这里贴一个源码文档的注释
Autorelease pool implementation
A thread's autorelease pool is a stack of pointers.
Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.
A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.
The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.
thread-local storage points to the hot page, where newly autoreleased objects are stored.