AutoreleasePool(自动释放池)是 OC 中一种内存自动回收的机制。在 MRC 中,可以通过 [obj autorelease] 来延迟内存的释放;而 ARC 中的 autorelease 方法是被禁用的,无法主动调用,但对象的内存任在我们不知情的情况下被很好的管理。这就是依赖于背后的 Autorelease 机制,那么是如何管理的呢?

AutoreleasePool

main.m 中我们会看到下面这段代码:

int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

我们通过编译:

xcrun -sdk iphonesimulator clang -rewrite-objc main.m

得到如下编译后的代码:

int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

看到 @aotoreleasepool 被编译器转换成了 __AtAutoreleasePool __autoreleasepool。查找得到 __AtAutoreleasePool 对应的结构如下:

struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};

结构体内实际上是两个 objc 方法:objc_autoreleasePoolPush()objc_autoreleasePoolPop()

打开 objc 源码 - DeveloperErenLiu/RuntimeAnalyze/objc4-750,对应查找到它们在 NSObject.mm 的实际实现:

void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}

这两个函数实际上都是对 AutoreleasePoolPage 的封装。所以自动释放机制的核心也在这个类。

AutoreleasePoolPage

AutoreleasePoolPage 的结构图如下:

我们会注意到两个字段:childparentAutoreleasePool 是由若干 AutoreleasePoolPage 以双向链表的形式组合而成。

需要注意的几个点:

  • 每个 AutoreleasePoolPage 对象会开启 4096字节(4kb)内存(4kb 对齐的原因,虚拟内存每个扇区 4096 字节)。
  • depth 表示链表深度,即节点个数。
  • id *next 指针作为游标指向当前页栈顶(即最新加入的 autorelease 对象的下一个位置)。

单个 AutoreleasePoolpage 的结构如下:

objc_autoreleasePoolPush()

每个 AutoreleasePoolPage 对象会开启 4096字节(4kb)内存,除了自身实例变量所占空间,剩下的空间全部拿来存储 autorelease 对象的地址。

每当进行一次 objc_autoreleasePoolPush 调用时,runtime 都会向当前的 AutoreleasePoolPage 中添加一个哨兵对象,值为 nil,

哨兵对象的定义为:

#   define POOL_BOUNDARY nil

注:图中的 obj1obj2 表示 AutoreleasePoolPage 自身除了 autorelease 对象的实例。

到这里会有个疑问:为什么需要插入一个哨兵对象?放到后面解释。

添加完哨兵对象后,将 next 指针指向下一个添加 Autorelease 对象的位置。当当前 AutoreleasePoolPage 满了,开启一个新的 AutoreleasePoolPage,并更新 childparent 指针,以组成双向链表。

objc_autoreleasePoolPop()

objc_autoreleasePoolPush 会有个返回值,这个返回值正是前面提到的哨兵对象。objc_autoreleasePoolPop() 调用时会把哨兵对象作为入参。之后根据传入的哨兵对象地址找到哨兵对象对应的 AutoreleasePoolPage;在当前 page 中,对所有晚于哨兵对象插入的 Autorelease 对象发送 release 消息,到哨兵对象后,销毁当前 page;再根据 parent 向前继续进行 pop,知道第一个哨兵对象所在 page 释放完成。

关于 AutoreleasePool 的几个问题

AutoreleasePool 是怎么释放的?

第一种是 main.m 里的 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,优先级最低,保证其释放池子发生在其他所有回调之后。

第二种是手动创建的局部 AutoreleasePool。

根据当前 loop 的情况,进行创建和释放。

什么对象会加入 autoreleasePool?

  • alloc/new/copy/mutableCopy等持有对象的方法,不会加入 autoreleasePool;其他不持有对象的方法通过 objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue 来判断是否需要加入 autoreleasePool,这是编译器的优化。
// 自己生成并持有对象,不需要加入 autoreleasePool
id object = [[NSObject alloc] init];
// MRC:不是自己生成并且不持有对象,需要加入autoreleasePool
id object2 = [NSMutableArray array];
// MRC:不是自己生成,但持有对象,不需要加入autoreleasePool
id object3 = [NSMutableArray array];
[object3 retain];
  • iOS5 及之前的编译器,关键字 __weak 修饰的对象,会自动加入 autoreleasePool;iOS5 及之后的编译器,则直接调用的 release,不会加入 autoreleasePool。

  • id 指针(id *)对象指针(NSError **),会自动加上关键字 __autorealeasing,加入 autoreleasePool。

子线程中使用 autorelease 对象会内存泄漏吗?

子线程的 runloop 默认是不开启的,如果产生了 Autorelease 对象,会调用 autoreleaseNoPage 方法。这个方法里会自动创建一个 hotpage,默认生成一个 AutoreleasePoolPage 来添加 autorelease 对象。

参考内容:

评论