每个线程(包括主线程),都维护了一个管理 NSAutoreleasePool 的栈。当创先新的 Pool 时,他们会被添加到栈顶。当 Pool 被销毁时,他们会被从栈中移除。
autorelease 的对象会被添加到当前线程的栈顶的 Pool 中。当 Pool 被销毁,其中的对象也会被释放。当线程结束时,所有的 Pool 被销毁释放。
AutoreleasePool
autorelease 本质上就是延迟调用 release ,那 autoreleased 对象究竟会在什么时候释放呢?为了弄清楚这个问题,我们先来做一个小实验。
先看代码:
1 |
|
思考一下 输出结果
———————————–这是分割线———————————
答案:
1 |
|
分析:
3 种场景下,我们都通过 [NSString stringWithFormat:@"leichunfeng"]
创建了一个 autoreleased
对象,这是我们实验的前提。并且,为了能够在 viewWillAppear
和 viewDidAppear
中继续访问这个对象,我们使用了一个全局的 __weak
变量 string_weak_
来指向它。因为 __weak
变量有一个特性就是它不会影响所指向对象的生命周期,这里我们正是利用了这个特性。
场景 1:
当使用 [NSString stringWithFormat:@"leichunfeng"]
创建一个对象时,这个对象的引用计数为 1 ,并且这个对象被系统自动添加到了当前的 autoreleasepool 中。当使用局部变量 string 指向这个对象时,这个对象的引用计数 +1 ,变成了 2
(这里后面会在看一下)。因为在 ARC 下 NSString *string
本质上就是 __strong NSString *string
。所以在 viewDidLoad
方法返回前,这个对象是一直存在的,且引用计数为 2 。而当 viewDidLoad
方法返回时,局部变量 string 被回收,指向了 nil 。因此,其所指向对象的引用计数 -1 ,变成了 1 。
而在 viewWillAppear
方法中,我们仍然可以打印出这个对象的值,说明这个对象并没有被释放。咦,这不科学吧?不是一直都说当函数返回的时候,函数内部产生的对象就会被释放的吗?前面我们提到了,这个对象是一个 autoreleased
对象,autoreleased
对象是被添加到了当前最近的 autoreleasepool
中的,只有当这个 autoreleasepool
自身 drain
的时候,autoreleasepool
中的 autoreleased
对象才会被 release
另外,我们注意到当在viewDidAppear
中再打印这个对象的时候,对象的值变成了 nil ,说明此时对象已经被释放了。因此,我们可以大胆地猜测一下,这个对象一定是在 viewWillAppear 和 viewDidAppear 方法之间的某个时候被释放了,并且是由于它所在的 autoreleasepool 被 drain 的时候释放的。
####### 下面我们要证明这个问题
在开始前,我先简单地说明一下原理,我们可以通过使用 lldb
的 watchpoint
命令来设置观察点,观察全局变量 string_weak_
的值的变化,string_weak_
变量保存的就是我们创建的 autoreleased
对象的地址。在这里,我们再次利用了 __weak
变量的另外一个特性,就是当它所指向的对象被释放时,__weak
变量的值会被置为 nil 。了解了基本原理后,我们开始验证上面的猜测。
我们先在第 35 行打一个断点,当程序运行到这个断点时,我们通过 lldb 命令 watchpoint set v string_weak_
设置观察点,观察 string_weak_
变量的值的变化。如下图所示,我们将在 console 中看到类似的输出,说明我们已经成功地设置了一个观察点:
点击继续执行
我们先看 console 中的输出,注意到 string_weak_ 变量的值由 0x00007f9b886567d0
变成了 0x0000000000000000
,也就是 nil.
说明此时它所指向的对象被释放了。另外,我们也可以注意到一个细节,那就是 console 中打印了两次对象的值,说明此时 viewWillAppear
也已经被调用了,而 viewDidAppear
还没有被调用。
接着,我们来看看左侧的线程堆栈。我们看到了一个非常敏感的方法调用 -[NSAutoreleasePool release]
,这个方法最终通过调用 AutoreleasePoolPage::pop(void *)
函数来负责对 autoreleasepool 中的 autoreleased 对象执行 release 操作。
结合前面的分析,我们知道在 viewDidLoad 中创建的 autoreleased 对象在方法返回后引用计数为 1 ,所以经过这里的 release 操作后,这个对象的引用计数 -1 ,变成了 0 ,该 autoreleased 对象最终被释放,猜测得证。
另外,我们在代码中并没有手动添加 autoreleasepool ,那这个 autoreleasepool 究竟是哪里来的呢?看完后面的章节你就明白了。
场景 2:
当通过 [NSString stringWithFormat:@"leichunfeng"]
创建一个对象时,这个对象的引用计数为 1 。而当使用局部变量 string 指向这个对象时,这个对象的引用计数 +1 ,变成了 2 。而出了当前作用域时,局部变量 string 变成了 nil ,所以其所指向对象的引用计数变成 1 。
另外,我们知道当出了 @autoreleasepool {}
的作用域时,当前 autoreleasepool 被 drain
,其中的 autoreleased 对象被 release 。所以这个对象的引用计数变成了 0 ,对象最终被释放。
场景 3:
当出了 @autoreleasepool {}
的作用域时,其中的 autoreleased 对象被 release ,对象的引用计数变成 1 。当出了局部变量 string 的作用域,即 viewDidLoad 方法返回时,string 指向了 nil ,其所指向对象的引用计数变成 0 ,对象最终被释放。
理解在这 3 种场景下,autoreleased 对象什么时候释放对我们理解 Objective-C 的内存管理机制非常有帮助。其中,场景 1 出现得最多,就是不需要我们手动添加 @autoreleasepool {}
的情况,直接使用系统维护的 autoreleasepool ;场景 2 就是需要我们手动添加 @autoreleasepool {}
的情况,手动干预 autoreleased 对象的释放时机;场景 3 是为了区别场景 2 而引入的,在这种场景下并不能达到出了 @autoreleasepool {}
的作用域时 autoreleased 对象被释放的目的。
AutoreleasePoolPage
细心的读者应该已经有所察觉,我们在上面已经提到了 -[NSAutoreleasePool release]
方法最终是通过调用 AutoreleasePoolPage::pop(void *)
函数来负责对 autoreleasepool 中的 autoreleased 对象执行 release 操作的
那这里的 AutoreleasePoolPage 是什么东西呢?其实,autoreleasepool 是没有单独的内存结构的,它是通过以 AutoreleasePoolPage
为结点的双向链表
来实现的(分别对应结构中的parent指针和child指针)。我们打开 runtime 的源码工程,在 NSObject.mm 文件的第 438-932 行可以找到 autoreleasepool 的实现源码。通过阅读源码,我们可以知道
- 1、每一个线程的 autoreleasepool 其实就是一个指针的堆栈;
- 2、每一个指针代表一个需要 release 的对象或者 POOL_SENTINEL(哨兵对象,代表一个 autoreleasepool 的边界)
- 3、一个 pool token 就是这个 pool 所对应的
POOL_SENTINEL
的内存地址。当这个 pool 被 pop 的时候,所有内存地址在pool token
之后的对象都会被 release - 4、这个堆栈被划分成了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增加或删除
- 5、
Thread-local storage
(线程局部存储)指向hot page
,即最新添加的 autoreleased 对象所在的那个 page - 6、AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程
- 7、AutoreleasePoolPage每个对象会开辟
4096
字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地 - 8、上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
一个空的 AutoreleasePoolPage 的内存结构如下图所示:
字段释义:
magic
用来校验 AutoreleasePoolPage 的结构是否完整;next
指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin()thread
指向当前线程parent
指向父结点,第一个结点的 parent 值为 nilchild
指向子结点,最后一个结点的 child 值为 nildepth
代表深度,从 0 开始,往后递增 1hiwat
代表 high water mark 。
注意
:当 next == begin()
时,表示 AutoreleasePoolPage 为空
;当 next == end()
时,表示 AutoreleasePoolPage 已满
Autorelease Pool Blocks
我们使用 clang -rewrite-objc
命令将下面的 Objective-C 代码重写成 C++ 代码:
1 |
|
将会得到以下输出结果(只保留了相关代码):
1 |
|
不得不说,苹果对 @autoreleasepool {}
的实现真的是非常巧妙,真正可以称得上是代码的艺术。苹果通过声明一个 __AtAutoreleasePool
类型的局部变量 __autoreleasepool
来实现 @autoreleasepool {}
。当声明 __autoreleasepool
变量时,构造函数 __AtAutoreleasePool()
被调用,即执行 atautoreleasepoolobj = objc_autoreleasePoolPush();
;当出了当前作用域时,析构函数 ~__AtAutoreleasePool()
被调用,即执行 objc_autoreleasePoolPop(atautoreleasepoolobj);
。也就是说 @autoreleasepool {}
的实现代码可以进一步简化如下
1 |
|
push 操作
上面提到的 objc_autoreleasePoolPush()
函数本质上就是调用的 AutoreleasePoolPage 的 push 函数
1 |
|
因此,我们接下来看看 AutoreleasePoolPage 的 push 函数的作用和执行过程。一个 push 操作其实就是创建
一个新的 autoreleasepool ,对应 AutoreleasePoolPage 的具体实现就是往 AutoreleasePoolPage 中的 next
位置插入一个 POOL_SENTINEL
,并且返回插入的 POOL_SENTINEL
的内存地址。这个地址也就是我们前面提到的 pool token
,在执行 pop 操作的时候作为函数的入参
1 |
|
push 函数通过调用 autoreleaseFast
函数来执行具体的插入操作。
1 |
|
autoreleaseFast 函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:
- 1、当前 page 存在且没有满时,直接将对象添加到当前 page 中,即 next 指向的位置;
- 2、当前 page 存在且已满时,创建一个新的 page ,并将对象添加到新创建的 page 中;
- 3、当前 page 不存在时,即还没有 page 时,创建第一个 page ,并将对象添加到新创建的 page 中。
每调用一次 push 操作就会创建一个新的 autoreleasepool ,即往 AutoreleasePoolPage
中插入一个 POOL_SENTINEL
,并且返回插入的 POOL_SENTINEL
的内存地址。
autorelease 操作
通过 NSObject.mm
源文件,我们可以找到 -autorelease
方法的实现:
1 |
|
通过查看 ((id)self)->rootAutorelease()
的方法调用,我们发现最终调用的就是 AutoreleasePoolPage 的 autorelease 函数。
1 |
|
AutoreleasePoolPage
的 autorelease 函数的实现对我们来说就比较容量理解了,它跟 push 操作的实现非常相似。只不过 push 操作插入的是一个 POOL_SENTINEL
,而 autorelease 操作插入的是一个具体的 autoreleased 对象。
1 |
|
pop 操作
同理,前面提到的 objc_autoreleasePoolPop(void *)
函数本质上也是调用的 AutoreleasePoolPage 的 pop 函数。
1 |
|
pop 函数的入参就是 push 函数的返回值,也就是 POOL_SENTINEL
的内存地址,即 pool token
。当执行 pop 操作时,内存地址在 pool token
之后的所有 autoreleased 对象都会被 release 。直到 pool token
所在 page 的 next
指向 pool token
为止。
根绝上面的内容我们可以大致猜测 AutoreleasePoolPage类的结构 如下:
AutoreleasePoolPage 类的结构
1 |
|
实例
下面是某个线程的 autoreleasepool 堆栈的内存结构图,在这个 autoreleasepool 堆栈中总共有两个 POOL_SENTINEL ,即有两个 autoreleasepool 。该堆栈由三个 AutoreleasePoolPage 结点组成,第一个 AutoreleasePoolPage 结点为 coldPage() ,最后一个 AutoreleasePoolPage 结点为 hotPage() 。其中,前两个结点已经满了,最后一个结点中保存了最新添加的 autoreleased 对象 objr3 的内存地址。
此时,如果执行 pop(token1)
操作,那么该 autoreleasepool 堆栈的内存结构将会变成如下图所示
Autorelease对象什么时候释放?
这个问题拿来做面试题,问过很多人,没有几个能答对的。很多答案都是“当前作用域大括号结束时释放”,显然木有正确理解Autorelease机制。
在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
嵌套的AutoreleasePool
知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。
其他Autorelease相关知识点
使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 这里被一个局部@autoreleasepool包围着
}];
当然,在普通for循环和for in循环中没有,所以,还是新版的block版本枚举器更加方便。for循环中遍历产生大量autorelease变量时,就需要手加局部AutoreleasePool咯。