文章也同时在简书更新
引言
OC对象的生命周期取决于引用计数,我们有两种方式可以释放对象:一种是直接调用release释放;另一种是调用autorelease将对象加入自动释放池中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。
本文将介绍自动释放池的原理和使用场景,并结合一道据说是优酷iOS的笔试题来举例说明自动释放池的妙用。
更多关于iOS内存管理的文章已经汇总至:深入总结iOS内存管理。
自动释放池的创建
如果没有自动释放池而给对象发送autorelease消息,将会收到控制台报错。但一般我们无需担心自动释放池的创建问题。
我们的Mac以及iOS系统会自动创建一些线程,例如主线程和GCD中的线程,都默认拥有自动释放池。每次执行 “事件循环”(event loop)时,就会将其清空,这一点非常重要,请务必牢记! 关于事件循环,其涉及到runloop,可以看这篇文章:深入理解RunLoop。
因此我们一般不需要手动创建自动释放池,通常只有一个地方需要它,那就是在main()函数里,如下:
|
|
这个main()函数里面的池并非必需。因为块的末尾是应用程序的终止处,即便没有这个自动释放池,也会由操作系统来释放。但是这些由UIApplicationMain函数所自动释放的对象就没有池可以容纳了,系统会发出警告。因此,这里的池可以理解成最外围捕捉全部自动释放对象所用的池。
@autoreleasepool的作用
大家可以先看一下下面的iOS笔试题的第5题(修改代码的错误),如下图:
这段代码问题在哪里呢?题目的解答请继续阅读。笔者先给一个提示:与内存的释放有关。
现考虑如下代码:
|
|
这段代码和笔试题关键部分大同小异。如果“doSthWith:”方法要创建一个临时对象,那么这个对象很可能会放在自动释放池里。笔试题中最后stringByAppendingString方法很有可能属于上述的方法。因此如果涉及到了自动释放池,那么问题也应该就出在上面。
注意:即便临时对象在调用完方法后就不再使用了,它们也依然处于存活状态,因为目前它们都在自动释放池里,等待系统稍后进行回收。但自动释放池却要等到该线程执行下一次事件循环时才会清空,这就意味着在执行for循环时,会有持续不断的新的临时对象被创建出来,并加入自动释放池。要等到结束for循环才会释放。在for循环中内存用量会持续上涨,而等到结束循环后,内存用量又会突然下降。
而如果把循环内的代码包裹在“自动释放池”中,那么在循环中自动释放的对象就会放在这个池,而不是在线程的主池里面。如下:
|
|
新增的自动释放池可以减少内存用量,因为系统会在块的末尾把这些对象回收掉。而上述这些临时对象,正在回收之列。
自动释放池的机制就像“栈”。系统创建好池之后,将其压入栈中,而清空自动释放池相当于将池从栈中弹出。在对象上执行自动释放操作,就等于将其放入位于栈顶的那个池。
实验验证
我们可以通过实验进行验证。新建工程加入上述代码,并关闭ARC(不然是看不到区别的)。
在未添加autoreleasepool时,我们的堆内存实时分配情况如下图:
大家可以看到Persistent Bytes不断增加,到达100W次的创建峰值后(出for循环)开始逐步释放。因此图像是一个向上凸的曲线。
而在加入autoreleasepool后,我们看到如下的曲线:
可以发现尽管字符串在不断地创建,但由于得到了及时的释放,堆内存始终保持在一个很低的水平。
其他注意点
@autoreleasepool语法还有一个好处,就是可以避免无意间误用那些在清空池之后已被系统回收的对象,例如:
|
|
上述代码在编译时就会基于错误警告,因为obj出了自动释放池就不可用了。
@autoreleasepool小结
- 自动释放池排布在栈中,对象受到autorelease消息后,系统将其放入栈顶的池里。
- 合理运用自动释放池,可以降低程序的内存峰值。
微信公众号
第一时间获取最新内容,欢迎关注微信公众号:「洛斯里克的大书库」。