内存管理:AutoreleasePool
MRC
1.retain/release
1 |
|
在MRC时代,对象的生命周期是通过引用计数来管理的,我们需要通过频繁地手动添加retain
来使引用计数 +1,通过release
来使引用计数 -1,当对象的引用计数为 0 时,对象会被销毁。
#示例1:
1 |
|
这样的管理方式不仅比较繁琐,而且可能会因为疏忽大意而导致内存泄露或过度释放情况的发生。比如示例1中,我们是先通过retain
使newName
引用计数 +1,再通过release
使引用计数 -1。如果这两者顺序颠倒,则就可能出问题。假设newName
和_name 的旧值是同一个对象,那么先release
旧值就会使旧值的引用计数为0,从而导致对象被提前释放,后面再赋值给_name 就无效。
2.自动释放池
与 release 相比,autorelease
算是一种延迟对象释放时间的方式。它需要配合 autorelease pool(自动释放池)使用,我们只需要将对象标记为autorelease
,这样对象就会被自动加入到自动释放池中;自动释放池会在合适的时机自动或手动执行drain
方法进行销毁;销毁前会向其内部的这些对象发送release
消息,从而使对象的引用计数-1;当引用计数为 0 时,对象的内存空间就会被释放。一个对象可以被多次放入到同一个自动释放池内,并且每次放入池内时都会调用一次release
方法。
1 |
|
自动释放池在 MRC 和 ARC 中有不同的形式和使用方法:
- MRC 中的 NSAutoreleasePool
1 |
|
- ARC 中的 @autoreleasepool{}
1 |
|
在ARC环境下,我们不能直接使用 NSAutoreleasePool 对象,而要使用后者。根据 开发文档 的描述,后者的效率更高。
3.pool的创建
3.1.自动
大部分情况下,系统已经为我们创建了自动释放池,并不需要我们自己创建。那么系统是什么时候帮我们创建的自动释放池呢。。。?这就要结合 runloop 来分析,下面这段内容是应用执行到didFinishLaunchingWithOptions时,通过 po [NSRunLoop currentRunLoop] 打印的当前runloop的信息:
1 |
|
以下是摘要,显示的是启动后系统在主线程的 Runloop 中自动添加的观察者:
1 |
|
其中第1和第6个观察者,两者的activities
分别是0x1
和0xa0
。activities
表示的是当前 runloop 所处的状态,下面是 CFRunloop.h 中定义的 activities 枚举值:
1 |
|
activities = 0x1,对应的就是 kCFRunLoopEntry;activities = 0xa0,对应的就是 kCFRunLoopBeforeWaiting | kCFRunLoopExit。
解释一下,kCFRunLoopEntry
表示 runloop 已启动;kCFRunLoopBeforeWaiting
表示 runloop 没事做了即将休眠;kCFRunLoopExit
表示 runloop 已结束;从第1和第6个观察者的 callout 描述可以看到,它们的回调都是_wrapRunLoopWithAutoreleasePoolHandler,关于这个回调,目前我尚未查到其源码,它具体怎么实现自动释放池的创建和销毁暂不能一探究竟。按照网上各大博主的说法:
- 在进入runloop时(kCFRunLoopEntry),观察者回调中会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
- 在runloop即将休眠时(kCFRunLoopBeforeWaiting),观察者回调中会调用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
- 在runloop退出时(kCFRunLoopExit),观察者回调中会调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个观察者的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
虽然_wrapRunLoopWithAutoreleasePoolHandler的源码我暂时不得而知,但_objc_autoreleasePoolPush()和_objc_autoreleasePoolPop()的源码是可追踪的,后面的5.2小节中会具体讲解~
小结:系统提供的自动释放池,其创建发生在两个时机:runloop启动时 和 runloop即将休眠时。
3.2.手动
绝大多数情况下,系统会帮我们自动创建和销毁自动释放池。但是,根据 开发文档 的描述,以下场景下我们需要自己创建自动释放池:
- If you are writing a program that is not based on a UI framework, such as a command-line tool.
如果你的项目不是基于UI framework的,比如命令行工具,则你需要自己创建自动释放池管理对象的生命周期。
- If you write a loop that creates many temporary objects.
You may use an autorelease pool block inside the loop to dispose of those objects before the next iteration. Using an autorelease pool block in the loop helps to reduce the maximum memory footprint of the application.
就是说,当你的循环体内创建了大量的临时对象时,你需要在循环体内创建一个自动释放池,这样临时对象会被标记为 autorelease 并在下次循环之前销毁。 在循环体内使用自动释放池可以降低内存峰值。
#示例2:在一个模态弹出的界面的viewDidLoad
方法中模拟一个循环体内大量创建临时变量的情况:
1 |
|
这是不使用自动释放池,反复弹出界面时的内存峰值状况图:
这是使用自动释放池,反复弹出界面时的内存峰值状况图:
通过对比,可以清楚的看到,使用自动释放池时,即使创建了大量的临时对象,对象都能及时释放,内存的峰值也几乎没变化~
- If you spawn a secondary thread.
You must create your own autorelease pool block as soon as the thread begins executing; otherwise, your application will leak objects.
当你创建了一个子线程时,也需要在线程启动时主动创建一个自动释放池,否则会产生内存泄露。
#示例3:
1 |
|
注意,先将 Build Settings->Objective-C Automotic Reference Counting 选项设置为NO,禁用ARC。运行后,输出日志:
1 |
|
如日志所示,MRC环境下,在子线程中创建对象之后,使用 autorelease pool 并将对象标记为 autorelease,任务执行完成之后对象能正常释放。你也可以试着将 @autorelease{ } 块和 autorelease 去掉,运行之后对象不会销毁。
需要注意的是,子线程中需要自己创建释放池的说法只针对MRC的环境。在ARC的环境下,我们在子线程中创建新的对象后,该对象是能自动释放的。可以将 Build Settings->Objective-C Automotic Reference Counting 选项设置为 YES,启用ARC,修改代码如下:
1 |
|
输出日志:
1 |
|
可以看到,ARC环境下,在子线程中创建了 Person 的实例对象,并且没有主动在子线程中添加释放池,但 person 对象最后确实销毁了。
那么为啥 ARC 环境下子线程中不使用自动释放池,临时对象也能释放呢?StackOverflow 的 这篇帖子 中有一种解释:
The latest version of the runtime (646, which shipped with OS X 10.10 and iOS 8) does indeed add a pool if you perform an autorelease without a pool on the current thread. The previous version of the runtime (551.1, which came with OS X 10.9 and iOS 7), also did this.
从 OSX 10.9 和 iOS7 开始,如果我们在线程中没有使用 autorelease pool,那么苹果会自动帮我们创建一个 pool 来释放对象,避免了内存泄露。我瞄了一眼这份开发文档的更新日志,上次更新时间竟还停留在 2012-07-17!下面是最新的 NSObject.mm 中的源码:
1 |
|
这是 autorelease 方法底层实现的源码,在子线程中不创建自动释放池,直接将对象标记为 autorelease 时,会调用 autoreleaseFast(),参数为当前 autorelease 对象。
1 |
|
因为没有创建释放池,所以 autoreleaseFast() 内会直接进入最后一个判断语句中,接着调用 autoreleaseNoPage(),参数为 autorelease 对象。
1 |
|
进入函数后,因为没设置释放池,所以 haveEmptyPoolPlaceholder() 返回 false;又因为传进来的 obj 参数是 autorelease 对象而非 POOL_BOUNDARY(边界对象),所以会直接创建 page 并将其设置为当前正在使用的 pool;随后将 autorelease 对象加入自动释放池的栈顶;在当前线程结束后 pool 会出栈,其中的 autorelease 对象也会随之释放。
可见,ARC环境下苹果已经更改了实现,子线程中会帮我们创建自动释放池,只是文档未更新。
4.pool的销毁
在 3.1 小节中讲过,自动释放池的销毁时机主要有两个:runloop即将进入休眠时 和 runloop退出时~
- AppKit 与自动释放池
AppKit and UIKit frameworks process each event-loop iteration (such as a mouse down event or a tap) within an autorelease pool block. Therefore you typically do not have to create an autorelease pool block yourself, or even see the code that is used to create one.
在事件循环(event-loop)开始时,AppKit 和 UIKit 会在主线程中创建一个自动释放池,不需要我们手动创建。当事件循环结束时,自动释放池会执行 drain 并销毁。
- 线程与自动释放池
每个线程(包括主线程)都维护着自己的自动释放池栈
,新创建的 pool 会被 push 到栈顶,当 pool 销毁时它会 pop 出栈。当前线程中被标记为 autorelease 的对象会被加入到栈顶的 pool 内。当线程销毁时,与当前线程相关的 pool 也都会通过 drain 自动销毁。
5.结构与源码
1 |
|
这是 NSObject.mm 源码中对 autorelease pool 的描述。它明确指出:
- autorelease pool 是一个保存着指针的栈,此栈由一个双向的 pages(AutoreleasePoolPage) 链表组成,链表会在需要时被添加和删除;
- 栈内指针指向的对象有两种:一种是被标记为 autorelease 而待 release 的对象;另一种是表示 pool 边界的哨兵对象(POOL_BOUNDARY);
- 当 pool 被销毁而出栈时,哨兵对象前面的所有 autorelease 对象都会收到 release 消息而释放。
5.1.栈与链表
这里我们先讲一下上面提到的链表。Autorelease pool 并没有单独的结构体,它由若干个AutoreleasePoolPage
以双向链表的形式组合而成。NSObject.mm 中有关于 AutoreleasePoolPage 的定义,成员变量部分摘要如下:
1 |
|
- thread,表示当前线程,每个线程中都有与之对应的 pool,线程启动时创建pool,线程销毁时 drain pool;
- id *next,游标,指向栈顶最新 add 进来的 autorelease 对象的下一个位置。
- parent,指向父 page;
- child,指向子 page;
每个线程中都有一个自动释放池的栈,栈内可能有一个或多个AutoreleasePoolPage
对象;当将对象标记为autorelease
时,此对象会被 add 到栈顶的 page 内;如果此 page 已满,则会创建一个新的 page 作为 child
,新 page 的next
指针被初始化在栈底;标记为 autorelease 的对象会被 add 到这个新建的 page 内;文字不如图表,这里借用博主 ©sunnyxx 的一张图能更清晰的看到添加 autorelease 对象后 page 的结构:
5.2.pool的创建
上面对 autorelease pool 的栈结构在原理上有了一个简单的描述,那么在代码层上 pool 是如何被创建的呢?
#示例4:使用 @autoreleasepool{ } 来手动
创建一个自动释放池
1 |
|
使用Clang编译后得到的结果如下,这里只截取关键部分的代码:
1 |
|
可以看到,我们声明的 count 和 block 变量及其调用,在编译后的结果中是被 {__AtAutoreleasePool…} 包裹着的。所以实际上,@autoreleasepool{ } 就是通过__AtAutoreleasePool
来实现的,它是一个结构体,其定义中包含了:
- 构造函数,负责pool的创建或入栈;
- 析构函数,负责pool的出栈、销毁;
- 一个通过析构函数创建的 atautoreleasepoolobj 对象的指针。
构造函数和析构函数中分别调用了objc_autoreleasePoolPush()
和objc_autoreleasePoolPop()
,关于这两个函数,我们可以在 NSObject.mm 源码中追踪到其具体的实现:
1 |
|
这里注意,_objc_autoreleasePoolPush 和_objc_autoreleasePoolPop 正是在3.2小节的最后提到的那两个函数,也就是之前说的,主线程会在 Runloop启动
和即将休眠
时通过回调函数中调用_objc_autoreleasePoolPush()创建 pool,在即将休眠
和退出loop
时调用_objc_autoreleasePoolPop()释放 pool。这两个函数的内部也只是调用了 objc_autoreleasePoolPush 和 objc_autoreleasePoolPop(注:函数名的开头不带下划线)。
因为本小节是介绍 pool 入栈,所以先只讲解 objc_autoreleasePoolPush 函数。它跟 pool 的创建和入栈有什么关系呢?Clang文档 中有一段关于此函数的描述:
1 |
|
就是说,此函数就是用来创建 autorelease pool 并将其设置为当前 pool,它的内部的调用了AutoreleasePoolPage::push()
函数:
1 |
|
if 语句第一个条件是针对调试模式,调试模式下每次自动释放池都会调用 autoreleaseNewPage() 方法,因为是第一次调用所以没有page,就创建一个新的 page。
1 |
|
这是调试模式下的实现,我们不用关心它,我们只需要看autoreleaseFast()
函数,其参数为POOL_BOUNDARY
(注意这个参数,后面讲 autorelease 对象入栈时,会有对比)。
1 |
|
autoreleaseFast() 函数中,hotPage()用来获取当前 pool 栈内正在使用的 page,首次调用push()
时没有 page,所以会进入最后一个判断,调用 autoreleaseNoPage()函数来创建一个空的占位池
。
1 |
|
第一次进入此函数时,haveEmptyPoolPlaceholder()会返回 false,所以会进入最后一个判断,调用 setEmptyPoolPlaceholder():
1 |
|
setEmptyPoolPlaceholder() 会通过 TLS 返回一个 EMPTY_POOL_PLACEHOLDER,其定义如下:
1 |
|
EMPTY_POOL_PLACEHOLDER 是一个空的占位池,它存储在 TLS 中,不包含任何对象。
至此,执行 @autorelease{ },第一次调用 push() 函数,最终创建了一个空的占位池~
5.3.对象入栈
如果在 @autorelease block 内创建了对象,那么这些对象就会被自动标记为 autorelease,并加入到自动释放池中:
1 |
|
从函数实现的源码中可以看到,对象被标记为 autorelease 后,会调用 autoreleaseFast()。上面在介绍 pool 的创建时,已经介绍过这个函数,当时 autoreleaseFast 的参数是一个边界对象(哨兵),这里的参数则是被标记为 autorelease 的对象本身:
1 |
|
- 进入函数体之后,会先调用 hotPage() 获取当前在用的page,
1 |
|
当前是第一次调用 autorelease,栈中还没有创建好的page,只在5.2 小节中执行 @autorelease 时创建了一个 EMPTY_POOL_PLACEHOLDER(空的占位池),所以 hotPage() 会返回 nil。因此 autoreleaseFast()函数会继续调用 autoreleaseNoPage()。
1 |
|
因为之前在创建空的占位池时 autoreleaseNoPage()内调用了 setEmptyPoolPlaceholder(),所以这次 haveEmptyPoolPlaceholder() 会返回 true,进入第一个条件语句,pushExtraBoundary 被置为 true,所以最后创建了一个新的page,插入了边界对象,autorelease 对象入栈。
将对象压入栈时,会调用 add() 函数,首先解除保护;随后将对象插入到 page 中,并重新设置 next 指针;最后设置保护,返回值为当前 autorelease 对象下一位的索引。源码如下:
1 |
|
至此,第一个 autorelease 对象完成入栈~
后面再有对象被标记为 autorelease 时,依然走 autoreleaseFast(),但这时 page 已经存在,如果 page 没满,则直接 page->add() 让对象入栈;如果page已满,则调用 autoreleaseFullPage()新建page,再让对象入栈;
1 |
|
因为是栈式结构,所以先入栈的对象会在栈底,后入栈的对象则依次往栈顶的方向添加,具体可以参考 5.1 小节中的示意图。
5.4.对象释放
当 Runloop 即将进入休眠 和 Runloop 退出时,如线程执行完任务销毁时,autorelease pool 会 drain 并销毁,销毁前向其中保存着的 autorelease 对象依次发送 release 消息,从而释放对象。那么自动释放池的底层是如何释放对象的呢?
1 |
|
objc_autoreleasePoolPop()即 Runloop 退出,自动释放池出栈时调用的函数。函数内会判断当前 token 是 EMPTY_POOL_PLACEHOLDER 还是 POOL_BOUNDARY 或者 autorelease 对象。如果是空的占位池,则清空占位池;如果栈底不是边界对象,则直接报错;其他情况,直接调用 releaseUntil(),释放对象。
1 |
|
上面章节中讲到,第一次将对象标记为 autorelease 时,在对象入栈前会往栈内插入一个边界对象(哨兵),这些边界对象可以视为一个 pool 的开始。当 pool 需要释放对象时,会从栈顶开始,依次向栈底边界对象的方向清理掉这中间的所有 autorelease 对象,具体流程为:
- 根据传入的哨兵对象地址找到哨兵对象所处的 page;
- 向当前 page 中晚于哨兵对象插入的所有 autorelease 对象发送 -release 消息;
- 回头移动 next 指针到正确位置;
- 从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page。
以上,就是 MRC 中有关 retain,release,autorelease 和自动释放池相关的原理和具体的实现。
ARC
1.作用
ARC,自动引用计数是苹果在 iOS5 之后推出的新技术。使用ARC时,我们不再需要手动添加retain
、release
、autorelease
这样的内存管理代码,编译器会在编译代码时自动帮我们添加。比如系统会自动帮我们在主线程 Runloop 中创建一个自动释放池,ARC下自动释放池内的对象会被自动标记为 autorelease,等 Runloop 退出时线程结束,pool被销毁,autorelease 对象就会 release。
ARC 不仅减少了开发者的麻烦,还避免了因为疏忽而导致的内存泄露或者过度释放等问题,我们只需要专注于自己的业务。
ARC differs from tracing garbage collection in that there is no background process that deallocates the objects asynchronously at runtime.[3] Unlike garbage collection, ARC does not handle reference cycles automatically. This means that as long as there are “strong” references to an object, it will not be deallocated. Strong cross-references can accordingly create deadlocks and memory leaks. It is up to the developer to break cycles by using weak references.
2.ARC与GC
ARC
!= 垃圾回收机制
!
ARC 发生在编译阶段
,它是LLVM 3.0 编译器中的新特性。ARC 环境中,编译器在代码编译时
帮我们将对象标记为 autorelease、retain、release,我们无须再写这些内存管理的代码,只需要用strong
或者weak
表示你对对象的所有权,或者注意像循环强引用这种问题即可。
JAVA中的垃圾回收机制则是在运行时
检查对象的依赖,如果没有指针指向某个对象,那么这个对象就是垃圾对象,到达一定量级后系统就会自动清除这些垃圾对象,或者由调用者主动调用GC清理~
作为对比来说,OC中的引用计数
更靠近垃圾回收机制一些,即当对象的引用计数=0时,就会自动调用对象的dealloc
函数进行销毁。
3.变量修饰符
ARC下的四个变量修饰符:
- __strong
对应属性修饰符中的strong
,强引用,表示指针指向并拥有某个对象(引用计数+1)。这是声明对象时默认的修饰符,如果想释放强引用的对象,则将指针置为nil即可。ARC下当没有任何一个强引用指向对象时,对象才会销毁。
- __weak
对应属性修饰符中的weak
,弱引用,表示指向但不拥有某个对象(引用计数不变)。__weak 不会影响对象的释放,即只要没有强引用指向对象,即使有N个弱引用指向此对象,那么对象还是会销毁。对象被释放时,__weak 指针会被自动置为nil,不会引发野指针问题。
- __autoreleasing
相当于MRC中的 autorelease,属性不能使用此修饰符。使用示例:
1 |
|
- __unsafe_unretained
对应属性修饰符中的unsafe_unretained
,相当于 MRC 中的assign
,只是将指针指向某对象,不改变其引用计数,不影响其释放。之所以以unsafe
开头,是因为当此对象被释放时,原指针仍会指向此对象所在的内存区域,再次调用此指针时会引发野指针问题,不安全。
Clang文档中有这样一段描述:
Methods in the alloc, copy, init, mutableCopy, and new families are implicitly marked attribute((ns_returns_retained)). This may be suppressed by explicitly marking the method attribute((ns_returns_not_retained)).
与MRC一样,以 alloc
、init
、copy
、mutableCopy
、new
开头的方法返回的对象,会被隐式的标记为ns_returns_retained
,其他情况下创建的对象则会被标记为 autorelease 并加入自动释放池中。
4.注意事项
- 不能再手动向对象发送retain, release, autorelease、retainCount、dealloc消息;
- 可以重写 dealloc 方法,但不能在其内部调用[super dealloc];
- 不能使用 NSAutoreleasePool 对象,而是使用 @autoreleasepool{};
- 属性修饰符 weak 相当于原来的 assign,strong 相当于原来的 retain;
- 注意循环引用问题;
在整个 XCode 中 开关 ARC,可以通过 Build Settings -> Objective-C Automotic Reference Counting 选项来设置。设置单独的某个或某几个文件开关 ARC 时,可到 Build Phases -> Compile Sources 中双击对应的文件,添加-fobjc-arc
或-fno-objc-arc
即可~
相关参考:
#©wiki-Automatic_Reference_Counting