移动app开发中,由于移动设备内存的限制,内存管理是一个非常重要的话题。objective-c的内存管理,不仅是面试当中老生常谈的一个必问话题,也是日常项目开发中,特别需要重视的环节。对于笔者这种以java语言入门编程世界的开发者来说,习惯了垃圾收集器的自动化管理,对于oc的引用计数器管理方式,还是需要花功夫来学习和运用。
1. ARC 和 非ARC
oc的内存管理方式,分为ARC(automatic reference counting自动引用计数)和非ARC模式。Apple 在 Xcode 4.2 中发布了 Automatic Reference Counting (ARC),该功能为开发人员消除了手动执行引用计数的负担。
目前xcode新建项目,都会推荐默认的ARC方式(ARC的确有很大的优势)。当然,如果必须要使用非ARC,可以在build setting中修改automatic reference counting选项为NO。如果在ARC项目中引入非ARC的代码或者静态库,需要在build phases设置相应资源为-fno-objc-ar;相反,非arc项目设置arc使用-fobjc-arc。
ARC与非ARC,从字面上来看,在于是否auto,即是由编译器来自动实现reference counting,还是由开发者手动完成引用计数的加减。作为现在经常使用arc模式来开发的我们来说,ARC大大减少了我们对内存管理的工作,甚至,很多入门开发者完全像开发java应用一样,没有管object的释放。然而对oc来说,从mac os x10.8开始,garbage collector也已废弃,iOS上压根就没有出现过这个概念。iOS上oc的内存管理,本质上是引用计数的管理,理解引用计数,是iOS内存管理的核心。
2. 引用计数
如果一个对象没有被其他对象所引用,则表明该对象已不需要,即可以释放。这就好比人们在一张饭桌上吃饭,如果饭桌上还有人,就表明该饭桌还不可以收拾;只有当所有人都吃好饭离开,使用人数小于1,才表明这张饭桌已使用完,可以清理收拾。 所以对象释放的时机,即是该内存的引用计数小于1. oc中的内存管理方式,就是基于对引用计数的管理。
ARC模式下,编译器完成了引用计数的管理,开发者不需要手动添加引用计数管理的代码(实际上也不允许),所以以下主要通过非ARC模式的代码方式,解释引用计数管理方式。
1》对象的初始化和释放
oc中的object需要继承自统一的父类NSObject。对于一个NSObject的生命周期来说,关注以下几个方法:
- alloc
- new
- copy
- mutableCopy
- dealloc
当调用上述前四种方法时,都会生成新的对象,object的引用计数都会+1,归调用者所有(即需要调用者释放)。系统对象以及库中所有的对象都会遵循这个规则,即所有以上述四种方法名开头的方法,都会使引用计数+1;反之,所有不以该命名开头的返回对象方法,都不会使引用计数+1.对于ARC来说,这种规则被确立为硬性规定。当我们自己自定义对象时,也应遵循这种规范,虽然通过命名规则来体现内存管理方式有些让人奇怪。
dealloc是对象内部的实现方法,当object的引用计数为0,即没有被其他对象引用时,该方法会调用,释放object。需要注意的是,在每个对象的生命周期内,dealloc只会调用一次。然而当引用计数为0时,并不能保证系统何时执行该方法。运行期系统会在何时时机调用dealloc,所以决不应该手动调用dealloc,一旦调用后对象就不可用。
在非ARC对象的dealloc方法中,主要就是释放对象所拥有的引用,除此之外,通常还需要把原来配置的观测行为清理掉,如remove掉kvo和notification的观察者。对于ARC模式来说,并不允许我们直接复写dealloc这些内存管理相关的操作。编译器会利用Objective-c++的特性,为C++对象的.cxx_destruct方法生成代码,释放对象。但是,对于那些通过malloc()生成的内存或者比如CoreFoundation中的对象,并非属于oc对象,开发者需要按照需要自己释放。
在非ARC模式中,我们可以手动控制引用计数的增减。通过以下两个方法:
- retain
- release
当调用[object retain],引用计数+1;调用[object release]时,引用计数-1.当object不在使用,需要释放时,调用release使引用计数清0,而不要直接调用dealloc。
NSObject协议中,存在retainCount这个方法,用于查询对象的当前保留计数:
- (NSUInteger)retainCount
然而它并没有卵用。然而它并没有卵用(重要的事要说两次)。在ARC模式中,内存管理相关的方法都无法通过编译,而在非ARC中,retainCount很多时候并不能反映出对象正常的保留计数。甚至当对象已被释放的情况下,再次调用到retainCount会直接导致crash。
NSString *str = @"12456"; NSLog(@"str retainCount: %lu",(unsigned long)[str retainCount]); NSNumber *num = @1; NSLog(@"num retainCount: %lu",(unsigned long)[num retainCount]); NSNumber *numF = @3.1415f; NSLog(@"numF retainCount: %lu",(unsigned long)[numF retainCount]);
以上代码的结果可能会让你大跌眼镜:
2015-07-24 14:37:19.238 TestMemory[10987:3418823] str retainCount: 4294967295 2015-07-24 14:37:19.241 TestMemory[10987:3418823] num retainCount: 18 2015-07-24 14:37:19.241 TestMemory[10987:3418823] numF retainCount: 1
str和num对象的retainCount出现了让人不解的数字。实际上编译器对这些对象都做了优化,这种优化只在某些场合才会发生,所以numF对象的retainCount是正常的。所以再次强调,不要试图利用retainCount来做任何操作。
iOS的内存管理,说白了也就是对于引用计数,调用retain和release来告知系统对内存的释放。我们举个栗子,以下代码在非ARC模式中运行:
/*! * 饭桌 */ @interface Table : NSObject @end @implementation Table - (void)dealloc{ NSLog(@"%s",__func__); [super dealloc]; } @end /*! * 客人 */ @interface Guest : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, retain) Table *table; - (instancetype)initWithName:(NSString *)name; @end @implementation Guest - (instancetype)initWithName:(NSString *)name{ self = [self init]; if (self) { self.name = name; } return self; } - (void)dealloc{ NSLog(@"%@ dealloc",self.name); [self.name release]; [self.table release]; [super dealloc]; } - (void)setTable:(Table *)table{ [table retain]; [_table release]; _table = table; } @end
//两位客人到一张饭桌上吃饭的过程再现 Table *table = [[Table alloc] init]; NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); Guest *guest_1 = [[Guest alloc] initWithName:@"guest_1"]; NSLog(@"%@ retain count: %lu",guest_1.name,(unsigned long)[guest_1 retainCount]); guest_1.table = table; NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); Guest *guest_2 = [[Guest alloc] initWithName:@"guest_2"]; NSLog(@"%@ retain count: %lu",guest_2.name,(unsigned long)[guest_2 retainCount]); guest_2.table = table; NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); [guest_1 release]; NSLog(@"%@ retain count: %lu",guest_1.name,(unsigned long)[guest_1 retainCount]); NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); [guest_2 release]; NSLog(@"%@ retain count: %lu",guest_2.name,(unsigned long)[guest_2 retainCount]); NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]); [table release]; NSLog(@"table retain count: %lu",(unsigned long)[table retainCount]);
以上实现了一个简单的客人到饭桌吃饭的场景:首先有准备出一张饭桌,来了一位客人1,做到饭桌吃饭,又来了一位客人2,到饭桌吃饭,客人1吃好饭走人,客人2走人,饭桌使用完毕,收拾饭桌。
首先看两个类,Table很简单,类中没有保留的其他对象,dealloc方法调用[super dealloc]保证父类对象的释放。Guest类中,添加了两个属性(@property),属性的修饰符是copy和retain。iOS会对property自动创建get和set方法,而copy/retain/assign等这一类修饰符,表明了set方法中对属性值不同内存管理的方式,这一点后文详述。这里只要知道,copy和retain都会导致set方法中将属性值保留,即retainCount+1。Guest类中加入了自定义的init方法,注意init开头的方法,必须按照oc的代码规范要求才能实现init时调用的目的,如例子中initWithName:这样的camel-case。dealloc方法中实现了对保留对象的释放,最后调用[super dealloc]。setTable:方法复写了property的set方法,实际上只是将retain修饰符属性值的默认set方法用代码再现了一下。可以看到,对于retain的属性值,会先保留新值,后释放旧值,然后将新值赋给属性,实现对象的保留。而set方法的调用者,需要管理所赋值的释放。
以上代码的log如下:
2015-07-24 10:14:07.623 TestMemory[10956:3397055] table retain count: 1 2015-07-24 10:14:07.628 TestMemory[10956:3397055] guest_1 retain count: 1 2015-07-24 10:14:07.629 TestMemory[10956:3397055] table retain count: 2 -> table被guest对象保留,引用计数+1 2015-07-24 10:14:07.630 TestMemory[10956:3397055] guest_2 retain count: 1 2015-07-24 10:14:07.631 TestMemory[10956:3397055] table retain count: 3 -> table被guest对象保留,引用计数+1 2015-07-24 10:14:07.632 TestMemory[10956:3397055] guest_1 dealloc -> guest_1 引用计数为为0,触发dealloc 2015-07-24 10:14:07.632 TestMemory[10956:3397055] guest_1 retain count: 1 2015-07-24 10:14:07.633 TestMemory[10956:3397055] table retain count: 2 2015-07-24 10:14:07.634 TestMemory[10956:3397055] guest_2 dealloc -> guest_2 引用计数为为0,触发dealloc 2015-07-24 10:14:07.634 TestMemory[10956:3397055] guest_2 retain count: 1 2015-07-24 10:14:07.635 TestMemory[10956:3397055] table retain count: 1 2015-07-24 10:14:07.636 TestMemory[10956:3397055] -[Table dealloc] -> table 引用计数为0,触发dealloc 2015-07-24 10:14:07.636 TestMemory[10956:3397055] table retain count: 1
可以看到,对象通过alloc方法create后,retainCount都会+1。table对象在alloc,又分别被guest_1和guest_2保留,导致retainCount为3。guest调用release方法后,引用计数为0,都会触发guest对象的dealloc方法,导致table对象引用计数-1.最后table调用release,引用计数为0,触发table对象dealloc。
然而,正如上所说,retainCount在这里并没有很好地起到说明引用计数的作用。虽然引用计数都以为0,最后的retainCount都没能显示为0.这可能是运行期内部对retainCount方法做出了处理,或者是对象内存并没有被立即释放,否则对象dealloc后再次调用已释放对象的方法都会导致EXC_BAD_ACCESS的crash发生。
2》属性的所有权语义(ownership semantic)
待续...