本文将会阐述AutoReleasePool和ARC所有权修饰符的实现原理和自己的一些理解,并解决如下几个问题:
- AutoReleasePool和ARC是怎么配合完成内存管理的;
- __weak修饰的变量引用和释放问题;
- ARC的实现原理;
- BAD_ACCESS的出现原因;
- 局部变量的释放问题;
- …
AutoReleasePool运行原理
AutoRelease顾名思义会在某个时机自动释放管理在池内的变量,看上去很像ARC,甚至新手会将这两个概念混淆。但其实它更像C语言局部变量的特性。
首先我们来看下面的代码段:
1 | { |
这段代码在ARC环境下,是没有问题的,但是在MRC环境下obj
变量持有的对象不会正确释放从而造成内存泄露,而i
会被正确释放。AutoRelease会像C语言一样,用类似管理局部变量的方式来完成堆上对象的释放操作。但与C语言不同的是,我们可以设定池的作用域。
自释放池的运行原理如下图:
NSAutoReleasePool对象会在每次Runloop开始前生成,期间我们可以往自释放池内添加对象,当自释放池对象声明周期结束时会对池内的注册对象进行了一次Release操作。
利用Autorelease达到延时释放
内存管理的规则是,谁持有谁释放,但是Foundation中有类似[NSMutableArray array]
类方法取得对象存在,但自己不持有,这是怎么做到的呢
1 | - (id)instance { |
-instance
方法将会返回一个NSObject类型的对象,同时该对象的内存分配和实例化都在方法中实现,在-instance
方法中实例化的对象会被放入自动释放池,若在一个生命周期内无人持有该对象,那该对象在生命周期后会被回收,这样的延时释放机制是很有用的
我们来看下面的代码:
1 | - (id)instance { |
以上代码,我们用两种方式生成了NSObject对象,都无变量持有,都会被释放,但是两种生成方式生成的对象在内存管理上的差别在哪里呢。NSObject对象1,在实例化时由于无变量持有,引用计数不会+1,所以他会被立即释放;而NSObject对象2,实际上在-instance
方法内,被局部变量obj
持有引用计数+1,不会被立即释放,所以他会在一次Runloop后被自释放池释放
Autorelease在不同编译环境的使用
1 |
|
自动释放池的使用场景
目前常用的自动释放池使用场景有两处:
- 当产生大量Autorelease局部变量时
1 | NSArray *urls = < |
这些对象在Runloop结束之前都不会被释放,那么必然会导致这个Runloop周期内的内存峰值过大,我们可以使用自释放池包裹每次循环的内容,那么当一次循环结束,池内的临时对象都将被销毁
- 需要自己管理一个辅助线程时
1 | - (void)subThread { |
关于自动释放池的官方文档中有提到:
1 | If you spawn a secondary thread. |
在Cocoa应用程序中,每个线程独立维护自己的自释放堆栈。如果需要自己生成管理一个辅助线程,必须创建自己的自释放池,否则一旦线程执行,该线程将会产生内存泄露对象。
Autorelease是怎么配合ARC完成内存管理
所以我们可以来回答文章开始提出的问题,既然有了ARC,为什么还需要Autorelease呢。正常情况下,设置了ARC编译期会自动插入内存管理代码,这是编译期特性,但我们依然需要一个更灵活的管理对象生命周期的方式。
例如延时释放的场景,非alloc/new/copy/mutableCopy生成对象在超出代码域时对对象进行保活;
例如为了降低内存峰值,提前释放因为循环语句生成的大量临时变量;
例如对赋有__weak
修饰符变量的引用。
其实ARC也可以舍弃Autorelease这个概念,并且规定所有从方法中返回的对象保留引用计数比期望值多1,但是这么做破坏了向后的兼容性,需要考虑到不适用ARC的代码。
所以笔者的理解,Autorelease是对ARC内存管理一种兼容性优化方案。并且由于ARC下我们无法对对象的引用计数进行操作,Autorelease灵活补充了ARC对对象生命周期的控制,使开发者可以在一个Runloop的范围内对对象进行延迟或提前释放控制。
ARC的实现
XCode4.2默认设置ARC有效。ARC环境下编译期LLVM会自动添加内存管理代码,并且id类型必须要加上所有权修饰
__strong
__weak
__unsafe_unretained
__autoreleasing
苹果的官方说明中称,ARC是“由编译期进行内存管理”的技术,其本质和JAVA的垃圾回收机制有区别。并且编译器并不能完全胜任内存管理,在此基础上还需要Objective-C运行时库的协助。所以ARC的实现条件是:
- clang-3.0以上
- objc4-493.9以上
__strong修饰符
__strong
修饰符是id类型默认的所有权修饰符,当声明变量时未指定对象所有权修饰,那么__strong
会被默认添加
1 | { |
那么我们来看下声明强引用的编译期代码,当代码作用域结束时,编译器会自动插入release
1 | { |
非alloc/new/copy/mutableCopy方法生成对象
1 | { |
中间使用objc_retainAutoreleasedReturnValue()
方法是什么意思呢。我们知道,将内存管理语义在方法名中表示出来是Objective-C的惯例,而ARC将之确立为硬性规定,若方法名为下列词语开头alloc/new/copy/mutableCopy,则返回对象归调用者所有。而array
方法生成的NSMutableArray对象明显不符合这个规定,我们来看看如果不使用objc_retainAutoreleasedReturnValue()
方法会变成什么样:
1 | /** NSMutableArray.m */ |
+array
方法返回的是一个autorelease对象,如果我们要引用这个对象,编译器在设置其值得时候还需要进行一次retain操作。可以看出,+array
方法中的autorelease和main()
中的retain都是多余的,也就是autorelease紧跟其后就retain。为了优化代码,可以省略autorelease的注册,改为调用objc_autoreleaseReturnValue
,它要比调用autorelease和retain更快。
代码改写为如下:
1 | /** NSMutableArray.m */ |
__weak修饰符
__weak
修饰符提供了两个特性:
- 若附有
__weak
修饰符的变量所引用的对象被废弃时,则将该变量赋值为nil - 使用附有
__weak
修饰符的变量,就是使用注册到autoreleasepool中的对象
1 | { |
这段模拟代码表明,编译器使用objc_initWeak()
函数初始化weak变量,在作用域结束时使用objc_destroyWeak()
释放该变量。以上代码可以再进行下面的改写
1 | { |
__weak
修饰的实质,就是以被引用对象为键,引用指针地址为值插入Weak散列表,这样一个对象就可以同时赋值给多个__weak
修饰符变量。当被引用对象释放时,又可以根据散列表的结构特性快速找到所有的变量地址,将其置为nil并移除该记录。
现在来讲下__weak
修饰符带来的另外一个特性,当使用__weak
修饰符变量时,即是使用注册到autoreleasepool中的对象。
1 | { |
当使用弱引用变量时,会调用objc_loadWeakRetained()
方法取出__weak
变量所引用的对象并retain,并将该对象加入autoreleasepool。所以很多论坛上说__weak
不会使引用对象计数+1是不规范和完整的说法。
这个特性的好处是,在@autoreleasepool块结束前,都可以放心使用该变量,而不用担心被引用对象被释放。坏处是,如果多处使用该变量,那么被应用对象也会被重复注册到autoreleasepool中。
例如如下代码:
1 | id _weak o = obj; |
在使用变量o时,0x6719e40
会被注册到autoreleasepool内5次
所以将附有__weak
修饰符的变量o再赋值给附有__strong
修饰符的变量,可以避免这个问题
1 | id __weak o = obj; |
在tmp = o;
时,对象仅会被注册到autoreleasepool中1次
__autoreleasing修饰符
使用__autoreleasing
,等同于将对象加入autoreleasepool延迟释放
1 | { |
在非alloc/new/copy/mutableCopy生成对象用__autoreleasing
修饰:
1 | { |
unsafe_unretained修饰符
__unsafe_unretained
和名字一样,是不安全的所有权修饰符,它的用法和__weak
类似。当对象销毁时,会依然指向之前的内存空间(野指针问题)。
那么有同学会问,既然是unsafe的,为什么还要使用__ unsafe_unretained
修饰符呢。原因有两点:
__weak
只支持iOS5.0以上的系统(当然对于现在,这个原因已经可以无视了)__weak
对性能会有一定的消耗,在上一个section我们介绍了其实现,知道它是通过一个散列表来管理弱引用关系的。所以__unsafe_unretained
比__weak
要快,而且一个对象有大量的__weak
引用变量的时候,当对象被废弃那么此时就要遍历Weak表,把对应链表的所有节点地址置空,消耗cpu资源。
所以我们在使用__unsafe_unretained
修饰符时,要保证被引用对象附有__strong
修饰符并确实存在,否则会出现EXC_BAD_ACCESS
野指针问题。不过笔者认为,使用__unsafe_unretained
修饰符带来的性能提升几乎可以忽略,而且野指针问题调试可能会非常棘手,所以还是使用__weak
吧 ☺️