OC归纳总结 -- (4)OC内存管理之autorelease

925 阅读4分钟

在前面一篇文章中简单归纳总结了一下OC对象的内存管理相关的内容, 还剩余一个内容没有整理到, 就是autorelease.

我们前面说到OC对象中很多非allocXXX, copyXXX 的方法创建的对象, 在该方法返回时候一般会调用autorelease方法, 将对象添加到AutoReleasePool中, 它会在特定的时间给添加到自动释放池的对象都发送release消息, 从而能够使对象延迟释放.

1. AutoReleasePool的启动并在Runloop注册Observer

AutoReleasePool依赖Runloop事件循环, App在启动时, 会在主线程中自动创建一个自动释放池, 然后在整个Runloop运行过程中, 如果产生一些autorelease对象, 会自动添加到这个自动释放池中.

这个自动释放池, 会在Runloop中注册几个Observer, 分别如下:

  1. Runloop 进入Entry进入循环时时, 调用AutoReleasePoolPush()
  2. Runloop 进入Exit退出时, 调用AutoReleasePoolPop()
  3. 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;
    ...
}

上面伪代码和注释写的非常清楚了, 这里再归纳一下:

  1. 自动释放池实际是一个双向链表, 每个node节点的size是 4096字节!!!
  2. 每个链表的节点拥有数据存储区, 使用类似结构存储autorelease对象的地址!!!
  3. 除了存储autorelease对象地址, 还可能存储pool_boundary作为哨兵, 帮助解决autoReleasePool嵌套问题.
  4. 一个AutoReleasePool仅关联一个thread线程!!!

简单解释一下对于嵌套问题:

对于上面的pool_boundary可以简单理解, 在调用AutoReleasePoolPush时, 会插入一个nil到栈中. 当调用AutoReleasePoolPop时, 将栈顶到最近的nil之间的autorelease对象pop出栈, 并逐个发送release消息. 即使有多层嵌套也比较好理解!

另外, 为了防止单个节点数据爆表, Apple在设计的时, 限制了每个节点存储的autorelease对象的数量. 因此只要单个node满了. 就会创建新的节点.

此外, 还有一个函数比如hotPage(), 就是最新autorelease对象插入的node page页!!!

3. 相关问题

Q: ARC环境下, autorelease在什么时候释放?

  1. 如果手动添加了@autorelease{}, 在作用域结束时释放
  2. 如果在主线程, 那么Runloop在beforeWaiting通知发送时, 给该对象release

Q: 子线程默认不会开启Runloop, autorelease对象是如何处理的?

  1. 如果我们自己在子线程创建时包装了@autorelease{}, 会由这个自动释放池释放
  2. 如果没有创建, 但是产生了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.