OC语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数;用完了之后,就递减其引用计数。计数变为0,就表示没人关注此对象了,于是就可以把它销毁。
NSObject协议声明了下面三个方法用于操作计数器,以递增或递减其值:
- Retain 递增保留计数
- release 递减保留计数
- autorelease 待稍后清理“自动释放池”时,再递减保留计数
查看保留计数的方法是retainCount,此方法不太有用,不推荐。
下图演示了对象从创建出来经过一次“保留”两次“释放”的过程。
对象如果持有指向其他对象的强引用,那么“前者”就拥有“后者”。
也就是说,对象想令其所引用的那些对象继续存活,就可以将其“保留”。等用完了之后,再释放。
在上图所示的对象图中,ObjectB和ObjectC都引用了ObjectA。若ObjectB和ObjectC都不再使用ObjectA,则其保留计数降为0,于是便可以摧毁了。 还有其他对象想令ObjectB和ObjectC继续存活,而应用程序里又有另外一些对象想令那些对象存活。如果按“引用树”回溯,那么最终会发现一个“根对象”。再Mac OS X应用程序中,此对象就是NSApplication对象;而在iOS程序中,则是UIApplication对象。两者都是应用程序启动时所创建的单例。
下面这段代码有助于理解:
NSMutableArray* array = [[NSMutableArray alloc] init];
NSNumber* number = [[NSNumber alloc] initWithInt:1337];
[array addObject: number];
[number release];
// do something with 'array'
[array release];
如前所述,由于代码中直接调用了release方法,所以在ARC下无法编译。再Objective-C中,调用alloc的方法所返回的对象由调用者持有。也就是说,调用者已通过alloc方法表达了想令该对象继续存活下去的意愿。不过请注意,这并不是说对象的保留计数此刻一定是1,能够肯定的是:保留计数至少为一。因为alloc或initWithInt:方法的实现代码中,也许还有其他对象 保留了此对象。
创建完数组后,把number对象加入其中,调用数组的“addObject:”方法时,数组也会在number上调用retain方法,以期保留此对象。这时保留计数至少为2。接下来代码不再需要number对象了,于是release。此时保留计数至少为1。这样就不能照常使用number变量了。调用release后,已经无法保证所指的对象仍然存活。当然,根据本例的代码我们显然知道number在调用了release后存活,因为数组还在引用它,但是不能假定它一定存活,也就是不能像下面这样写代码:
NSNumber* number = [[NSNumber alloc] initWithInt:1337];
[array addObject: number];
[number release];
NSLog(@"number = %@", number);
即使上述代码在本例中可以正常运行,也仍然不是个好办法。如果调用release之后,基于某些原因,其引用计数降至0,那么number对象所占内存也许会回收。再调用NSLog可能就会导致崩溃。但也不一定,因为对象所占的内存在“解除分配”之后,只是放回“可用内存池”。如果执行NSLog时,尚未覆写对象内存,那么该对象仍然有效,程序不会崩溃。由此可见:
因过早释放对象而产生的bug很难调试。
为避免无意间使用了无效对象,一般调用完release后都会清空指针。这样就会保证不会出现可能只想无效对象的指针,这种指针被称为“悬挂指针”。比方说,可以这样编写代码来防止此情况发生:
NSNumber* number = [[NSNumber alloc] initWithInt:1337];
[array addObject: number];
[number release];
number = nil;
如前所述,对象图由互相关联的对象所组成 。刚才那个例子中的数组通过在其元素上调用retain方法来保留那些对象。不光是数组,其他对象也可以保留别的对象,这一般通过访问“属性”来实现,而访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为“strong关系”,则设置的属性值会保留。比方说,有个名为foo的属性由名为_foo的实例变量所实现,那么该属性的设置方法会是这样:
- (void) setFoo:(id)foo {
[foo retain];
[_foo release];
_foo = foo;
}
**此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。**顺序很重要。假如还未保留新值就先把旧值释放了,而且两个值又是指向同一个对象,那么先执行的release操作就可能导致系统将此对象永久回收。而后续的retain操作则无法令这个已经回收彻底的对象复生,于是实例变量就成了悬挂指针。
在OC的引用计数架构中,自动释放池时一项重要特性,调用release会立刻递减对象的保留计数(而且还有可能令系统回收此对象),然而有时候可以不调用它,改为调用autorelease,此方法会在稍后递减计数,通常是在下一次“事件循环”时递减,不过也可能更早。
此特性很有用,尤其是在方法中返回对象 时更应该用它。在这种情况下,我们并不总是想令方法调用者手工保留其值。比方说,有下面这个方法:
- (NSString*) stringValue {
NSString* str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
return str;
}
此时返回的str对象其保留计数比期望值要多1,因为调用alloc会令保留计数加1,而又没有与之对应的释放操作。保留计数多1,就意味着调用者要负责处理多出来的这一次保留操作。必须设法将其抵消。这并不是说保留计数本身一定就是1,它可能大于1,不过那取决于“initWithFormat:”内的实现细节。你要考虑的是如何将多出来的这一次保留操作给抵消掉。
但是,不能在方法内释放str,否则还没等方法返回,系统就把该对象回收了。这里应该用autorelease,它会在稍后释放对象,从而给调用者留下了足够长的时间,使其可以在需要时先保留返回值。换句话说,此方法可以保证对象在跨越“方法调用边界”后一定存活。实际上,释放操作会在清空最外层的自动释放池时进行,除非你有自己的自动释放池。否则这个时机指的就是当前线程的下一次事件循环。改写stringValue方法,使用autorelease释放对象:
- (NSString*) stringValue {
NSString* str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
return [str autorelease];
}
修改之后,stringValue方法把NSString对象返回给调用者时,此对象必然存活。所以我们能像下面这样使用它:
NSString* str = [self stringValue];
NSLog(@"The string is: %@", str);
由于返回的对象将于稍后自动释放,所以多出来的那一次保留操作到时候自然就会抵消,无须再执行内存管理操作。因为自动释放池中的释放操作要等到下一次事件循环时才会执行,所以使用NSLog语句在使用str对象前不需要手工执行保留操作。但是,假如要持有此对象(比如将其设置给实例变量),那就需要保留,并于稍后释放:
_instanceVariable = [[self stringValue] retain];
//...
[_instanceVariable release];
由此可见,autorelease 能延长对象生命期,使其在跨越方法调用边界后依然可以存活一段时间。
在使用引用计数机制时,需要注意的一个问题是保留环。
也就是呈环状相互引用的多个对象。这将导致内存泄露。
因为在循环中的对象保留计数不会降为0。对于循环中的每个对象来说,至少还有另外一个对象引用着它。如图,在图中,每个对象的保留计数都是1。
在垃圾收集环境中,通常将这种情况认定为“孤岛”。此时,垃圾收集器会将三个对象全部回收走。而在OC的引用计数架构中,则享受不到这个便利。通常采用“弱引用”来解决这个问题 。或是从外界命令循环中的某个对象不再保留另外一个对象。这两种方法都能打破保留环,从而防止内存泄露。
在ARC下不能调用下列方法:
- retain
- release
- autolease
- dealloc
这是因为ARC——自动引用计数,会自动执行reatin、release、autorelease等方法。
直接调用上述方法就会产生编译错误。
实际上,ARC在调用这些方法时,并不通过普通的OC消息转发机制,而是直接调用其底层C语言版本。这样做性能更好。比如,ARC会调用与retain等价的 底层函数objc_retain。这也是不能覆写retain、release、autorelease的理由,因为这些方法从来不会被直接调用。
将内存管理语义在方法名中表示出来早已成为OC的惯例。而ARC将之确立为硬性规定。这些规则简单的体现在方法名上。若方法名以下列词语开头,则其返回的对象归调用者所有。
- alloc
- new
- copy
- mutableCopy
归调用者所有的意思是:调用上述四种方法的那段代码要负责释放方法所返回的对象。
也就是说,这些对象的保留计数是正值。
若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。在这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后仍然有效。要想使对象多存活一段时间,必须令调用者保留它才行。
维系这些规则所需要的全部管理事宜均由ARC自动处理,其中也包括在将要返回的对象上调用autorelease,下列代码展示了ARC的用法。
ARC通过命名约定将内存管理规则标准化,初学此语言的人通常觉得这有些奇怪,其他编程语言很少像OC这样强调命名。但是,想成为优秀的 OC程序员就必须适应这套理念。在编码过程中,ARC能帮程序员做很多事情。
除了自动调用“保留”“释放”方法外,使用ARC还有很多其他的好处,他可以执行一些手工很难操作甚至无法完成的优化。例如,在编译期,ARC会把能够相互抵消的retain、release、autorelease操作约简,如果发现在一个对象上执行了多次“释放”“保留”操作,那么ARC有时可以成对的移除这两个操作。
ARC也包含运行期组件,此时所执行的优化很有意义。前面说到,某些方法在返回对象前,为其执行了autorelease操作,而调用方法的代码可能需要将返回的对象保留,比如下面的情况:
//From a class where _myPerson is a strong instance variable
_myPerson = [EOCPerson personWithName:@"Bob Smith"];
此时应该可以看出来,“personWithName:”方法里的autorelease与上一段代码里的retain都是多余的。为提升性能,可将二者删去。但是,ARC需要考虑“向后兼容性”(本来ARC也可以直接舍弃autorelease这个概念,并且规定,所有从方法中返回的对象其保留计数都比期望值多1。但这样会破坏向后兼容性)。
不过ARC可以在运行期检测到这一对多余的操作,也就是autorelease以及紧随其后的retain。为优化代码,此时ARC不直接调用autorelease而是调用objc_autoreleaseReturnValue。此函数会检视当前方法返回之后即将要执行的那段代码。如果发现那段代码要在返回的对象上执行retain操作,则设置全局数据结构中的一个标志位,而不执行autorelease操作。与之相似,如果方法返回了一个自动释放的对象,而调用方法的代码要保留此对象,那么此时不直接执行retain,而改为执行objc_retainAutoreleaseedReturnValue函数。此函数要检测刚才提到的标志位,如果已经置位,则不执行retain操作。并设置检测标志位,要比autorelease和retain快。
下面这段代码演示了ARC如何通过这些特殊的函数来优化程序的:
// Within EOCPerson class
+ (EOCPerson*) personWithName:(NSString*)name {
EOCPerson* person = [[EOCPerson alloc] init];
person.name = name;
objc_autoreleaseReturnValue(person);
}
//Code using EOCPerson class
EOCPerson* tmp = [EOCPerson personWithName:@"Matt Galloway"];
_myPerson = objc_retainAutoreleasedReturnValue(tmp);
为了取得最佳效率,这些特殊函数的实现代码都因处理器而异。下面这段为代码描述了其中的步骤:
在应用程序中可以用下列修饰符来改变局部变量与实例变量的语义:
比方说,想令实例变量的语义和不适应ARC时相同,可以运用__weak或 __unsafe_unretained修饰符。
不论采用上面哪种写法,在设置实例变量时都不会保留其值。只有使用新版运行期程序库时,加了__weak修饰符的weak引用才会自动清空,因为实现自动清空操作,要用到新版所添加的一些功能。
我们经常会给局部变量 加上修饰符,用以打破由“块”所引入的“保留环”。块会自动保留所引入的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致“保留环”。可以用__weak局部变量来打破这种“保留环”:
ARC也负责对实例变量进行内存管理。要管理其内存,ARC就必须在“回收分配给对象的内存”时生成必要的清理代码。凡是具备强引用的变量,都必须释放,ARC会在dealloc方法里插入这些代码。当手动管理引用计数时,你可能会这样写:
- (void) dealloc {
[_foo release];
[_bar release];
[super dealloc];
}
用了ARC之后,就不需要再这样编写了。因为ARC会借助OC的一项特性来生成清理例程。回收OC对象时,待回收的对象会调用所有C++对象的析构函数。编译器如果发现某个对象里含有C++对象,就会生成后缀名为.cxx_destruct的方法。而ARC借助此特性,会在该方法中生成清理内存所需的代码。
不过,如果有非OC的对象,比如CoreFoundation中的对象或是有malloc()分配在堆中的内存,那么仍然需要清理。在ARC下,不能直接调用dealloc方法,dealloc的方法可以这样写:
- (void)dealloc {
CFRelease ( _coreFoundationObject);
free ( _heapAllocatedMemoryBlob);
}
在不使用ARC时,可以覆写内存管理方法,但是当我们使用ARC后,不能随意的对这些方法进行覆写了。
对象在经历生命期之后,会被系统所回收,这时候就要调用dealloc方法了。
在每个对象的生命期内,此方法仅执行一次,也就是当保留计数降为0时。具体何时执行无法保证。
需要注意的是,系统会自行调用dealloc方法,所以你绝不能自己调用dealloc方法。
我们应该在dealloc方法中:
如果用NSNotificationCenter给此对象注册过某种通知,那么一般就会在dealloc中注销。
如果还向着此对象发通知,就会崩溃。
dealloc的方法可以这样写:
- (void) dealloc {
CFRelease(coreFoundationObject);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
如果使用的是手动管理而不是ARC,还需要加上:
[super dealloc];
当然ARC是会自动执行此操作。更加安全方便
在dealloc中也不要调用属性的存取方法,因为有人可能会覆写这些方法。并于其中做一些无法在回收安全阶段安全执行的操作。此外,属性可能还会处于“键值观测机制(KVO)”的监控下,该属性的观测着可能会在属性值改变时“保留”或使用这个即将回收的对象,导致出现错误。
“异常”是一种特性,C语言中没有异常,OC和C++都支持异常且可以相互兼容。
当你使用OC与C++混编或者使用第三方程序库而且此程序库索抛出的异常又不受你控制时,就需要捕获异常以及处理异常。
发生异常时应如何管理内存是个值得研究的问题。在try块中,如果先保留了某个对象,然后在释放前又抛出了异常,那么,除非此catch块能处理此问题,否则该对象所占内存将泄露。
C++的析构函数由OC的异常处理例程来运行。这对于C++对象很重要,由于抛出异常会缩短其生命期 ,所以发生异常时,必须析构,不然就会泄露,而文件句柄等系统资源因为没有正确清理,所以就会更容易因此泄露。
异常处理例程将自动销毁对象。
对象图里经常会出现一种情况,几个对象都以某种方式相互引用。从而形成“环”。由于OC内存管理模型使用引用计数架构,所以这种情况通常会导致泄露内存。
最简单的保留环有两个对象相互引用组成。如图
也就是说,如果在A实例中的other属性设置成B实例,B实例中的other属性又设置成A实例,就会出现保留环。
此时内存还没有泄露。
当其他对象对环内对象的引用移除之后,现在保留环包含的对象就已经泄露了。
避免保留环的最佳方式就是弱引用。
将属性声明为unsafe_unretained即可。上面那段代码经过修改如下:
修改之后, EOCClassB就不再通过other属性来拥有EOCClassA实例了。
unsafe_unretained表明,属性值可能不安全,且不归此实例所拥有。
用unsafe_unretained来修饰的属性特质,其语义同assign等价。然而,assign通常指用于“整体类型”(int,float,结构体等),unsafe_unretained通常用于对象类型。这个词本身就表明其所修饰的属性无法安全使用。
OC中还有一项与ARC相伴的运行期特性,可以令开发者安全使用弱引用:这就是weak属性特质,它与unsafe_unretained作用完全相同。然而,只要把属性回收,属性值就会自动设为nil。
下图是weak与unsafe_unretained具体区别;
当指向EOCClassA实例的引用移除后,unsafe_unretained仍然指向那个已经回收的实例,而weak指向nil。
总之,使用weak比unsafe_unretained通常会更好。因为程序也许会显示出错误的数据,但不会直接崩溃,所以从用户的体验来说,更推荐weak。
OC对象的生命周期取决于引用计数。OC的引用计数架构中,有一项特性叫“自动释放池”。释放对象有两种方式,一种是调用release方法,使其保留计数立即递减;另一种是调用autorelease方法,将其加入自动释放池中。自动释放池用于存放哪些需要在稍后某个时刻释放的对象。清空自动释放池时,系统会向其中的对象发送release消息。
@autoreleasepool {
//...
}
如果在没有创建自动释放池的情况下向对象发送autorelease消息那么控制台会输出:
然而,一般情况下无须担心自动释放池的创建问题。Mac OS X与 iOS 应用程序分别运行于Cocoa 及 Cocoa Touch 环境中。系统会自动创建一些线程,比如说主线程或是"大中枢派发"(Grand Central Dispatch,GCD)机制中的线程,这些线程默认都有自动释放池,每次执行"事件循环"时,就会将其清空。因此,不需要自己来创建"自动释放池块"。通常只有一个地方需要创建自动释放池,那就是在 main 函数里,我们用自动释放池来包裹应用程序的主入口点。比方说,iOS程序的 main 函数经常这样写∶
int main(int argc,char *argv[]) {
@autoreleasepool {
return UIApplicationMain (argc, argV, ni1, @"EOCAppDelegate");
}
}
从技术角度看,不是非得有个"自动释放池块"才行。因为块的末尾恰好就是应用程序的终止处,而此时操作系统会把程序所占的全部内存都释放掉。虽说如此,但是如果不写这个块的话,那么由 UIApplicationMain 函数所自动释放的那些对象,就没有自动释放池可以容纳了,于是系统会发出警告信息来表明这一情况。所以说,这个池可以理解成最外围捕捉全部自动释放对象所用的池。
下面这段代码中的花括号定义了自动释放池的范围。自动释放池于左花括号处创建,并干对应的右花括号处自动清空。位于自动释放池范围内的对象。将在此范围末尾处收到release消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的池里。比方说∶
@autoreleasepool {
NSString *string = [NSString stringwithFormat:@"1 = %i",1];
@autoreleasepool {
NSNumber *number = [NSNumber numberWithInt:1];
}
}
本例中有两个对象,它们都由类的工厂方法所创建,这样创建出来的对象会自动释放(参见第30条)。NSString 对象放在外围的自动释放池中,而 NSNumber 对象则放在里层的自动释放池中。将自动释放池嵌套用的好处是,可以借此控制应用程序的内存峰值,使其不致过高。
看下面这段示例:
for (int i = 0; i < 100000; i++) {
[self doSomethingWithInt:i];
}
如果"doSomethingWithInt∶"方法要创建临时对象,那么这些对象很可能会放在自动释放池里。比方说,它们可能是一些临时字符串。但是,即便这些对象在调用完方法之后就不再使用了,它们也依然处于存活状态,因为目前还在自动释放池里,等待系统稍后将其释放并回收。然而,自动释放池要等线程执行下一次事件循环时才会清空。这就意味着在执行for 循环时。会持续有新对象创建出来。并加入自动释放池中。所有这种对象都要等 for 循环执行完才会释放。这样一来,在执行 for 循环时,应用程序所占内存量就会持续上涨,而等到所有临时对象都释放后,内存用量又会突然下降。
这种情况不甚理想,尤其当循环长度无法预知,必须取决于用户输入时更是如此。比方说,要从数据库中读出许多对象。代码可能会这么写∶
NSArray*databaseRecords =/* ...*/;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
EOCPerson *person = [ [EOCPerson alloc] initWithRecord: record];
[people addObject:person];
}
EOCPerson的初始化函数也许会像上例那样,再创建出一些临时对象。若记录有很多条,则内存中也会有很多不必要的临时对象,它们本来应该提早回收的。增加一个自动释放池即可解决此问题。如果把循环内的代码包裹在"自动释放池块"中,那么在循环中自动释放的对象就会放在这个池,而不是线程的主池里面。例如∶
NSArray *databaseRecords = /*...*/;
NSMutableArray *people = [NSMutableArray new];
for(NSDictionary *record in databaseRecords)[
@autoreleasepool {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
}
}
加上这个自动释放池之后,应用程序在执行循环时的内存峰值就会降低,不再像原来那么高了。内存峰值是指应用程序在某个特定时段内的最大内存用量。新增的自动释放池块可以减少这个峰值,因为系统会在块的末尾把某些对象回收掉。而刚才提到的那种临时对象,就在回收之列。
自动释放池机制就像"栈"(stack)一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。
是否应该用池来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没完成这一步。那就别急着优化。尽管自动释放池块的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。
如果在ARC 出现之前就写过Obiective-C程序.那么可能还记得有种老式写法。就是使用NSAutoreleasePool对象。这个特殊的对象与普通对象不同,它专门用来表示自动释放池,就像新语法中的自动释放池块一样。但是这种写法并不会在每次执行 for循环时都清空池,此对象更为"重量级",通常用来创建那种偶尔需要清空的池。比方说∶
NSArray *databaseRecords = /* ... */;
NSMutableArray *people =[NSMutableArray new];
int i = 0;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
for (NSDictionary*record in databaseRecords) {
EOCPerson *person = [[EOCPerson alloc] initWithRecord: record];
[people addObject:person];
// Drain the pool only every 10 cycles
if (++i == 10){
[pool drain];
i= 0;
}
}
[pool drain];
现在不需要再这样写代码了。采用随着 ARC 所引入的新语法,可以创建出更为"轻量级"(lightweight)的自动释放池。原来所写的代码可能会每执行n 次循环清空一次自动释放池,现在可以改用自动释放池块把 for 循环中的语句包起来,这样的话,每次执行循环时都会建立并清空自动释放池。
@autoreleasepool语法还有个好处;每个自动释放池均有其范围,可以避免无意间误用了那些在清空池后已为系统所回收的对象。比方说,考虑下面这段采用旧式写法的代码∶
NSAutoreleasePool *pool =[ [NSAutoreleasePool alloc] init];
id object = [self createObject];
[pool drain];
[self useObject:object];
这样写虽然稍显夸张,但却能说明问题。调用“useObejct:”方法时所传入的对象,可能已经被系统回收了。同样的代码改用新式写法:
@autoreleasepool {
id Object = [self createObject];
[self useObject:object];
}
这次根本就无法编译,因为object变量出了自动释放池块的外围后就不可用了。所以在调用“useObject:”方法时不能用它做参数。
调试内存管理问题很让人头疼。所幸Cocoa提供了“僵尸对象”这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化成特殊的"僵尸对象"、而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。
将NSZombieEnabled环境变量设为 YES,即可开启此功能。比方说,在 Mac OS X系统中用 bash运行应用程序时,可以这么做∶
export NSZombieEnabled = "YES"
./app
或是在Xcode中打开:
系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤。这一步就是把对象转化为僵尸对象,而不彻底回收。
NSObject 协议中定义了下列方法,用于查询对象当前的保留计数∶
-(NSUInteger) retainCount
然而 ARC已经将此方法废弃了。实际上,如果在 ARC中调用,编译器就会报错,这和在 ARC中调用retain、release、autorelease 方法时的情况一样。虽然此方法已经正式废弃了,但还是经常有人误解它,其实这个方法根本就不应该调用。