第五部分开始将讨论OC的核心技术,Reference Counting,也就是使用引用计数来进行内存管理,这一部分涉及到底层内存管理机制,ARC相关技术细节和在开发中涉及到内存管理需要注意的常见问题。
Item29 Understand Reference Counting
- Reference Counting是OC管理内存的方式,当一个对象的RC为0时,将被释放,iOS没有垃圾回收机制。
How Reference Counting Works
- 在NSObject Protocol中有三个方法可以改变RC,retain,release,autorelease。
- retainCount这个方法可以查看对象当前的RC值,但是并不推荐使用,Item36会讨论。
- 对象之间往往是互相持有的,当该持有关系是strong时,被持有对象的RC加1,而持有链的最顶端是根对象,Mac OSX是NSApplication,iOS是UIApplication,都是应用创建的单例。
- 举例一个NSMutableArray添加一个NSNumber,虽然在array添加number后,释放number,number的RC还是1,调用number理论上是可以的,但是并不推荐这么做,因为如果任何其他原因使number的RC为0,这一做法会导致崩溃。
- 对象被释放后,它的内存将进入可用内存池,如果调用发生在内存被复写之前,不会发生错误,所以之类bug有时会很难复现,所以在MRC中往往在调用release后会赋值nil。
Memory Management in Property Accessors
- strong命名的property的setter方法,是新值retain,然后旧值release,然后赋值,这一顺序不能错,因为如果先release再retain,且正好两个对象是同一个,可能会导致对象提前释放,RC为0,成为野指针,再调用retain则会出错。想按照这个顺序来,必须判断新旧两值是否是同一对象。
Autorelease Pools
- 借助autorelease pool替代release操作的autorelease,常用于需要返回新建对象的方法,具体释放时间在下一次事件循环(Item34将讨论)。
- 在直接调用该方法时不用额外的内存空间,直接调用即可。
- 但是如果返回对象需要持有时,比如赋值给一个实例变量,它需要retain一次,然后使用完后手动release,所以可以将autorelease理解为延长对象的生命周期,可以至少保证到方法调用的边界。
Retain Cycles
- 循环引用一般是指两个或多个对象直接互相存在强引用,而导致RC都不能为0,所有对象都不能释放。
- 在垃圾回收机制下,retain cycle会被定义为孤岛,而直接被全部释放,而在RC机制下,只能通过定义weak引用或者依靠外部帮助来使其中某个对象交出对其他对象的引用。
Item30 Use ARC to Make Reference Counting Easier
- Clang编译器带来了一个静态分析器,可以定位出现RC问题的位置,例如没有添加release,除此之外,该分析器可以为你自动添加retain,release这些操作,这也就是ARC技术的基础。
- 在ARC机制下,retain、release、autorelease、dealloc这些操作都是不允许的,因为这回影响编译器判断添加语句的位置。
- 事实上,ARC并没有直接调用上述这些方法,而是调用了他们的C的替代方法,例如objc_retain,这么做的好处是,因为这些操作会频繁调用,使用C方法可以提高效率。这也是为什么不允许直接重写retain,release这些方法,因为这方法并不是直接调用的。
Method-Naming Rules Applied by ARC
- 内存管理指定方法名在OC里一直是惯例,而ARC将其加强了,含有以下名称的方法:alloc、new、copy、mutableCopy,所返回的对象的所有者为方法的调用者,而其他方法返回的对象为autorelease,会保持到方法调用边界。
- 而ARC会依据方法名的开头添加响应的语句,例如上述四个关键字开头的,会直接返回,而一般方法,ARC会在返回对象前加上autorelease。
- 而在调用这些方法时,第一类方法返回的对象,ARC会在方法结束前添加release方法,而第二类方法因为有autorelease,所以不会添加操作。
- ARC通过命名规范来规范内存管理,再加上之前的命名空间,OC是少有的如此强调命名的语言之一。
- ARC可以做一些无法手动完成的优化,比如,它将在编译期间取消多余的retain和release操作。
- ARC在runtime也有进行优化,举例:EOCPerson的一般初始化方法返回的值(添加了autorelease),被赋值给一个对象的strong属性实例,按照之前的原则,需要在返回的对象加retain,这里的autorelease和retain看起来是多余的,ARC确实可以为了性能,直接去掉autorelease这个方法,所有返回的对象都为RC+1,但为了兼容MRC,ARC还是需要特殊处理。
- ARC确实对这种现象做了处理,在返回对象之前它调用了objc_autoreleaseReturnValue,如果被发现该对象是需要retain的,则会添加一个flag,而不是调用autorelease。同样的,调用者也会调用objc_retainAutoreleasedReturnValue,而不是retain,该方法也会先检测flag,如果存在,则不会retain,这样提高了效率:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Memory-Management Semantics of Variables
- ARC也同时管理着本地变量和实例变量的内存,默认每个变量对于对象是strong引用。
- 在setter方法中,ARC中直接对旧值赋值即可,ARC会自动添加正确的代码。
- 声明实例变量时,也可以改变内存管理方式,strong(默认,赋值将被retain)、unsafe_retained(赋值同assign,但指针不会自动置空,可能出现野指针)、weak(赋值同assign,对象被释放时,指针会自动置为nil,所以是安全的,iOS5后可用)、autoreleasing(多用与方法的返回值)。
- __weak用于本地变量时,常用于避免循环引用,比如在block中:
1 2 3 4 5 6 |
|
ARC Handling of Instance Variables
- 在ARC中,你一般不需要再重写dealloc方法,ARC借用Objective-C++的特性,Objective-C++对象在释放时会调用所有持有对象的析构方法,当编译器发现对象包含C++对象时,会生成.cxx_destruct方法,ARC借助这个方法,在其中执行清除内存的代码。
- 但有时你仍需要重写dealloc方法,像CoreFoundation对象和堆上开辟的内存(如malloc),以及KVO、的解除,都需要手动释放,但注意在ARC中,不需要在dealloc中写[super dealloc],因为ARC在.cxx_destruct中已经调用了这一方法。
Overriding the Memory-Management Methods
- 在MRC中,重写内存相关方法是允许的,比如单例常常重写release方法为一个空操作,这样单例就不会被释放。
- 但在ARC中是不允许的,一是会导致ARC对对象周期的误判,二是ARC对内存管理做了深度的优化,当需要执行retain、release、autorelease时,ARC在OC的message dispatch做了优化处理,不能重写或调用这些方法则是该优化的前提。
Item31 Release References and Clean Up Observation State Only in dealloc
1.dealloc方法会在对象的引用计数为0时自动调用,但什么时候调用并不能保证,即使是在MRC中,手动控制release也一样,因为很多库会在你不知道的情况下修改对象,这会导致调用dealloc的时间发生变化。所以你千万不要手动去调用dealloc,runtime会在合适的时间调用。
2.那么在ARC下,重写dealloc的话,一是用于释放CoreFoundation的对象,二是取消NSNotificationCenter中注册该对象的监听或KVO。
3.如果你的类中使用到了文件描述集,sockets、或者开辟了大块儿内存,由于dealloc的调用时间不明,你可能在你不需要使用的时候即可释放这些内存,而不用等到dealloc触发,这样需要自定义一个清除方法,该方法必须在dealloc之前调用,不然就算异常了。
4.清除资源需要另一个方法的原因是创建的对象并不是都会被调用dealloc,因为一部分对象在应用退出后台时并不会释放,它们只有在应用彻底被系统回收后才会释放,这是一种优化措施,但也会导致大量的资源被无故占用,所以在-(void)applicationWillTerminate:(UIApplication*)application中调用一些对象的clean方法是必要的。
5.有时可以在dealloc中也可以去调用clean方法,这可以避免忘记调用clean,但最好还是手动去先执行clean,所以还是要提示下或者严重的话直接直接抛出异常:
1 2 3 4 5 6 7 8 9 10 11 |
|
6.除了上述特例,一般是不允许在dealloc中调用类的其他实例方法,因为有可能导致方法执行前,该对象可能已经释放了。而且,dealloc方法是在导致对象最终释放的线程上执行的,所以需要在特定线程执行的方法在此调用,不能保证线程正确,即使是通过代码强制在某线程执行,也是不安全的,因为对象处于释放状态。 7.另外dealloc中也不可调用property的setter、getter方法,尤其是被重写的accessor,也有可能触发KVO的回调,导致未知的错误。
Item32 Beware of Memory Management with Exception-Safe Code
- Exception是OC和C++中用于处理严重异常的对象,但有时你也需要通过代码处理这些异常,例如去注销一个KVO,但之前并没有注册过的情况。
- 在try/catch中创建对象,并需要自己释放时,需要将释放代码写到finally中,这样才能保证无论是否异常都能保证对象释放。
- 但在ARC中可以自动添加额外的处理代码,使用-fobjc-arc-exceptions这个flag来控制,但默认是关闭的,因为exception出现时application直接crash,资源也会回收,所以没必要再做处理,而且会带来性能问题,只有编译器处于Objective-C++时才会开启,因为OC++添加代码带来性能损耗没有ARC添加时那么大,另外OC++中Exception是被大量使用的。
- 如果在ARC下需要单独处理exception,那么可以开启flag,但如果你有很多exception处理,那么你该考虑NSError了,如Item21所讲。
Item33 Use Weak References to Avoid Retain Cycles
- 循环引用带来的问题主要是,引用环中的对象将不能再被调用,但也不能释放,从而导致内存泄露。
- Java会有垃圾回收来解决这类问题,但iOS和Mac OS X 10.8之后是没有垃圾回收的,所以只能开发者自己去避免。
- 使用unsafe_unretained可以避免这一问题,它类似assign,但assign一般用于数值型,而unsafe_unretained用于对象,但如字面意思一样,它不会因为所指向的对象被释放而置为空值,所以调用unsafe_unretained的对象,可能会因为所指对象不存在而崩溃,所以是不安全的。
- 在ARC中我们常用的是weak字段,该字段与unsafe_unretained功能一致,但是它会在所指对象释放后自动指向nil,所以是安全的。
- 关于循环引用,总的原则就是,如果你不持有一个对象,那么你就不该retain它(数组,集合不直接持有包含的对象,但是会retain它们,是个例外)。一般场景有,controller的UI控件(一般weak属性),一个对象的delegate属性(一般为weak属性)。
Item34 Use Autorelease Pool Blocks to Reduce High-Memory Waterline
1.@autoreleasepool{}这是OC中建立autorelease pool的方法,但我们一般不必去手动创建。
2.main函数中的autorelease pool并不是必须的,只是UIApplicationMain()函数中需要autorelease的对象没有对应的pool,但它们在程序终止时时肯定会被释放的。
3.autorelease pool可以嵌套,autorelease的对象总是被添加最里面的pool中。
4.利用上述特性,我们可以对一些大量循环执行一个可能产生很多autorelease对象的方法做优化,如下EOCPerson的创建可能产生大量autorelease对象,这样产生的autorelease对象会在自己建的autorelease pool结束时释放,而不是长期存在于线程自己的autorelease pool,避免了出现应用内存陡升陡降这种“瀑布现象”:
1 2 3 4 5 6 7 8 |
|
5.autorelease pool可以被理解为放入了一个栈中,新建的pool会在最顶端,当它释放后会被移出栈,当一个对象调用了autorelease,它将被添加到最顶端的autorelease pool。
6.使用autorelease pool来优化瀑布现象并不是必要的,这取决你的应用,如果确实导致了问题,那么去使用它,如果不必要使用,那么就不要添加多于的autorelease pool。
7.ARC之前使用autorelease pool是使用NSAutoreleasePool,因为它属于重量级对象,所以一般是隔段时间进行释放:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
8.推荐使用新语法@autoreleasepool,更加轻量,而且一个重要特性,NSAutoreleasePool中创建的autoreleased对象在执行drain之后还能使用,这可能造成崩溃,且很多时候难以发现,而使用@autoreleasepool,这类代码不会被编过,也就及早避免了这一问题。
Item35 Use Zombies to Help Debug Memory-Management Problems
1.内存问题一般很难处理,原因是被释放的那块内存不一定就很快被重写,或者正好被一个同类的对象重写,这样有时不会导致崩溃,有时却会,所以开发者有时会无从下手。
2.Cocoa的Zombies特性会帮助我们解决这一问题,当该模式启用,所有被释放的对象会转化为NSZombie对象,其占用过的内存也不会被重用,当该对象收到消息时,会抛出异常,告知开发者所收到的消息,原来的对象类型这些信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
3.通过上述手段我们得知obj在dealloc后变为了NSZombie_EOCClass,但并没有它的父类,实际上,它是通过对原类型的类名修改,然后对NSZombie类型执行objc_duplicateClass(),完全拷贝zombie类并使用新类名(也可使用继承,但不如copy效率),制造出obj对应的zombie类,然后用objc_setClass()修改obj的isa指针,改变其类型,这一切都是利用runtime完成的(通过method swizzles对dealloc方法替换)。
4.由于NSZombie没有实现任何方法,所以向它或者它的copy类型发送任何消息,会直接进入forwarding mechanism,在寻求转发时如果发现类型名以NSZombie_开头,那么直接抛出异常,并打印出message、原类型这些信息。
Item36 Avoid Using retainCount
- retainCount是NSObject Protocol的一个方法,用于返回对象目前的引用计数值,在ARC中已经弃用,但即使在MRC中,任然应该避免使用它。
- 原因一是retainCount返回的是实时的count值,也就是说像autorelease这样将要发生的count减少的情况,不会在该方法反映出来,所以依据该值去执行一些改变count的方法,往往会出问题。
- 有时retainCount会返回一个极大的值,这是NSString或NSNumber直接设置常量时,系统会将其作为一个单例的常量,而不是去创建一个对应的对象,这些对象的ratainCount是不会改变的,但只是对一些特例的优化。