AutoreleasePool(自动释放池)是 OC 中一种内存自动回收的机制。在 MRC 中,可以通过 [obj autorelease]
来延迟内存的释放;而 ARC 中的 autorelease
方法是被禁用的,无法主动调用,但对象的内存任在我们不知情的情况下被很好的管理。这就是依赖于背后的 Autorelease 机制,那么是如何管理的呢?
AutoreleasePool
在 main.m
中我们会看到下面这段代码:
int main(int argc, char * argv[]) { |
我们通过编译:
xcrun -sdk iphonesimulator clang -rewrite-objc main.m |
得到如下编译后的代码:
int main(int argc, char * argv[]) { |
看到 @aotoreleasepool
被编译器转换成了 __AtAutoreleasePool __autoreleasepool
。查找得到 __AtAutoreleasePool
对应的结构如下:
struct __AtAutoreleasePool { |
结构体内实际上是两个 objc 方法:objc_autoreleasePoolPush()
和 objc_autoreleasePoolPop()
。
打开 objc 源码 - DeveloperErenLiu/RuntimeAnalyze/objc4-750,对应查找到它们在 NSObject.mm
的实际实现:
void * |
这两个函数实际上都是对 AutoreleasePoolPage
的封装。所以自动释放机制的核心也在这个类。
AutoreleasePoolPage
AutoreleasePoolPage
的结构图如下:
我们会注意到两个字段:child
和 parent
。AutoreleasePool
是由若干 AutoreleasePoolPage
以双向链表的形式组合而成。
需要注意的几个点:
- 每个
AutoreleasePoolPage
对象会开启 4096字节(4kb)内存(4kb 对齐的原因,虚拟内存每个扇区 4096 字节)。 depth
表示链表深度,即节点个数。id *next
指针作为游标指向当前页栈顶(即最新加入的 autorelease 对象的下一个位置)。
单个 AutoreleasePoolpage
的结构如下:
objc_autoreleasePoolPush()
每个 AutoreleasePoolPage
对象会开启 4096字节(4kb)内存,除了自身实例变量所占空间,剩下的空间全部拿来存储 autorelease 对象的地址。
每当进行一次 objc_autoreleasePoolPush
调用时,runtime 都会向当前的 AutoreleasePoolPage 中添加一个哨兵对象
,值为 nil,
哨兵对象的定义为:
注:图中的 obj1
、obj2
表示 AutoreleasePoolPage
自身除了 autorelease 对象的实例。
到这里会有个疑问:为什么需要插入一个哨兵对象?放到后面解释。
添加完哨兵对象后,将 next
指针指向下一个添加 Autorelease 对象的位置。当当前 AutoreleasePoolPage 满了,开启一个新的 AutoreleasePoolPage,并更新 child
和 parent
指针,以组成双向链表。
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_autoreleaseReturnValue
和objc_retainAutoreleasedReturnValue
来判断是否需要加入 autoreleasePool,这是编译器的优化。
// 自己生成并持有对象,不需要加入 autoreleasePool |
iOS5 及之前的编译器,关键字
__weak
修饰的对象,会自动加入autoreleasePool
;iOS5 及之后的编译器,则直接调用的 release,不会加入 autoreleasePool。id 指针(id *)
和对象指针(NSError **)
,会自动加上关键字__autorealeasing
,加入 autoreleasePool。
子线程中使用 autorelease 对象会内存泄漏吗?
子线程的 runloop 默认是不开启的,如果产生了 Autorelease 对象,会调用 autoreleaseNoPage
方法。这个方法里会自动创建一个 hotpage,默认生成一个 AutoreleasePoolPage 来添加 autorelease 对象。
参考内容: