第五章 内存管理

29.理解引用计数

Objective-C语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数;用完了之后,就递减其计数。计数变为0,就表示没人关注此对象了,于是,就可以把它销毁。

从Mac OS X 10.8开始,“垃圾回收集器”(garbage collector)已经正式废弃了,以Objective-C代码编写Mac OS X程序时不应再使用它,而iOS则从未支持过垃圾收集。

1. 引用计数工作原理

在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在Objective-C中叫做“保留计数”(retain count),也叫“引用计数”(reference count)。NSObject协议声明了下面三个方法用于操作计数器,以递增或递减其值:

  • retain:递增保留计数。
  • release:递减保留计数。
  • autorelease:待稍后清理“自动释放池”(autorelease pool)时,再递减保留计数。

查看保留计数的方法叫做retainCount,此方法不太有用,即便在调试时也如此,所以笔者(与苹果公司)并不推荐大家使用这个方法。

对象创建出来时,其保留计数至少为1。若想令其继续存活,则调用retain方法。要是某部分代码不再使用此对象,不想令其继续存活,那就调用release或autorelease方法。最终当保留计数归零时,对象就回收了(deallocated),也就是说,系统会将其占用的内存标记为“可重用”(reuse)。此时,所有指向该对象的引用也都变得无效了。

下图演示了对象自创建出来之后历经一次“保留”及两次“释放”操作的过程。

在对象声明周期中,其保留计数时而递增,时而递减,最终归零

应用程序在其声明期中会创建很多对象,这些对象都相互联系着。例如,表示个人信息的对象会引用另一个表示人名的字符串对象,而且可能还会引用其他个人信息对象,比如存放朋友的set中就是如此,于是,这些相互关联的对象就构成了一张“对象图”(object graph)。对象如果持有指向其他对象的强引用,那么前者就“拥有”后者。也就是说,对象想令其所引用的那些对象继续存活,就可将其“保留”。等用完了之后,再释放。

下图中,ObjectB与ObjectC都引用了ObjectA。若ObjectB与ObjectC都不再使用ObjectA,则其保留计数降为0,于是便可摧毁了。还有其他对象想令ObjectB与ObjectC继续存活,而应用程序里又有另外一些对象想令那些对象继续存活。如果按“引用树”回溯,那么最终会发现一个“根对象”(root object)。在Mac OS X应用程序中,此对象就是NSApplication对象;而在iOS应用程序中,则是UIApplication对象。两者都是应用程序启动时创建的单例。

对象图

下面这段代码有助于理解这些方法的用法:

NSMutableArray *array = [[NSMutableArray alloc]init];
    
NSNumber *number = [[NSNumber alloc]initWithInt:111];
[array addObject:number];
[number release];
    
//do something with 'array'
    
[array release];

由于代码中直接调用了release方法,所以在ARC下无法编译。在Objective-C中,调用alloc方法所返回的对象由调用者所拥有。也就是说,调用者已通过alloc方法表达了想令该对象继续存活下去的意愿。不过请注意,这并不是说对象此时的保留计数必定是1。在alloc或“initWithInt:”方法的实现代码中,也许还有其他对象也保留了此对象,所以,其保留计数可能会大于1。能够肯定的是:保留计数至少为1。保留计数这个概念就应该这样理解才对。绝不应该说保留计数一定是某个值,只能说所执行的操作是递增了该计数还是递减了该计数。

创建完数组后,把number对象加入其中。调用数组的“addObject:”方法时,数组也会在number上调用retain方法,以期继续保留此对象。这时,保留计数至少为2。接下来,代码不再需要number对象了,于是将其释放。现在的保留计数至少为1。这样就不能照常使用number变量了。调用release之后,已经无法保证所指的对象仍然存活。当然,根据本例中的代码,我们显然知道number对象在调用了release之后仍然存活,因为数组还在引用着它。然而绝不应假设此对象一定存活,也就是说,不要像下面这样编写代码:

NSNumber *number = [[NSNumber alloc]initWithInt:111];
[array addObject:number];
[number release];
NSLog(@"number=5@",number);

即便上述代码在本例中可以正常执行,也仍然不是个好办法。如果调用release之后,基于某些原因,其保留计数降至0,那么number对象所占内存也许会回收,这样的话,再调用NSLog可能就将使应用程序崩溃了。这里说“可能”,是因为对象所占的内存在“解除分配”(deallocated)之后,只是放回“可用内存池”(avaliable pool)。如果执行NSLog时尚未覆写对象内存,那么该对象仍然有效,这时程序不会崩溃。由此可见:因过早释放对象而导致的bug很难调试。

为避免在不经意间使用了无效对象,一般调用完release之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常称为“悬挂指针”(dangling pointer)。比方说,可以这样编写代码来防止此情况发生:

NSNumber *number = [[NSNumber alloc]initWithInt:111];
[array addObject:number];
[number release];
number = nil;

2. 属性存取方法中的内存管理

数组通过在其元素上调用retain方法来保留那些对象。不光是数组,其他对象也可以保留别的对象,这一般通过访问“属性”来实现,而访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为“strong关系”,则设置的属性值会保留。比方说,有个名叫foo的属性由名为_foo的实例变量所实现。那么该属性的设置方法会是这样:

-(void)setFoo:(id)foo
{
    [foo retain];
    [_foo release];
    _foo = foo;
}

此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要。假如还未保留新值就先把旧值释放了,而且两个值又指向同一个对象,那么,先执行的release操作就可能导致系统将此对象永久回收。而后续的retain操作则无法令这个已经彻底回收的对象复生,于是实例变量就成了悬挂指针。

3. 自动释放池

调用release会立刻递减对象的保留计数(而且还有可能令系统回收此对象),然后有时候可以不调用它,改为调用autorelease,此方法会在稍后递减计数,通常是在下一次“事件循环”(event loop)时递减,不过也可能执行得更早些。

此特性很有用,尤其是在方法中返回对象时更应该用它。在这种情况下,我们并不总是想令方法调用者手工保留其值。比方说,有下面这个方法:

-(NSString *)stringValue
{
    NSString *str = [[NSString alloc]initWithFormat:@"I am this: %@",self];
    return str;
}

此时返回的str对象其保留计数比期望值要多1(+1 retain count),因为调用alloc会令保留计数加1,而又没有与之对应的释放操作。保留计数多1,就意味着调用者要负责处理多出来的这一次保留操作。必须设法将其抵消。这并不是说保留计数本身就一定是1,它可能大于1,不过那取决于“initWithFromat:”方法内的实现细节。你要考虑的是如何将多出来的这一次保留操作抵消掉。

但是,不能在方法内释放str,否则还没等方法返回,系统就把该对象回收了。这里应该用autorelease,它会在稍后释放对象,从而给调用者留下了足够长的时间,使其可以在需要时先保留返回值。换句话说,此方法可以保证对象在跨越“方法调用边界”(method call boundary)后一定存活。实际上,释放操作会在清空最外层的自动释放池时执行,除非你有自己的自动释放池,否则这个时机指的就是当前线程的下一次事件循环。改写stringValue方法,使用autorelease来释放对象:

-(NSString *)stringValue
{
    NSString *str = [[NSString alloc]initWithFormat:@"I am this: %@",self];
    return [str autorelease];
}

修改之后,stringValue方法把NSString对象返回给的调用者时,此对象必然存活。所以我们能够像下面这样使用它:

NSString *str = [self stringValue];
NSLog(@“This string is: %@”,str);

由于返回的str对象将于稍后自动释放,所以多出来的那一次保留操作到时自然就会抵消,无须再执行内存管理操作。因为自动释放池中的释放操作要等到下一次事件循环时才会执行,所以NSLog语句在使用str对象前不需要手工执行保留操作。但是,假如要持有此对象的话(比如将其设置给实例变量),那就需要保留,并于稍后释放:

_instanceVariable = [[self stringValue]retain];
//…
[]

由此可见,autorelease能延长对象生命期,使其在跨越方法调用边界后依然可以存活一段时间。

4. 循环引用

使用引用计数机制时,经常要注意的一个问题就是“循环引用”(retain cycle),也就是呈环状相互引用的多个对象。这将导致内存泄露,因为循环中的对象其保留计数不会降为0。对于循环中的每个对象来说,至少还有另外一个对象引用着它。下图中的每个对象都引用了另外两个对象之中的一个。在这个循环里,所有对象的保留计数都是1。

循环引用

在垃圾收集环境中,通常将这种情况认定为“孤岛”(island of isoland)。此时,垃圾收集器会把三个对象全部回收走。而在Objective-C的引用计数架构中,则享受不到这一便利。通常采用“弱引用”来解决此问题,或是从外界命令循环中的某个对象不再保留另外一个对象。这两种办法都能打破循环引用,从而避免内存泄露。

要点:

  • 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。
  • 在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。

30.使用ARC简化引用计数

使用ARC时一定要记住,引用计数实际上还是要执行的,只不过保留与释放操作现在是由ARC自动为你添加。由于ARC会自动执行retain、release、autorelease等操作,所以直接在ARC下调用这些内存管理方法是非法的。具体来说,不能调用下列方法:

retain
release
autorelease
dealloc

实际上,ARC在调用这些方法事,并不通过普通的Objective-C消息派发机制,而是直接调用其底层C语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多CPU周期。比方说,ARC会调用与retain等价的底层函数objc_retain。这也是不能覆写retain、release或autorelease的缘由,因为这些方法从来不会直接被调用。

1. 使用ARC时必须遵守的方法命名规则

将内存管理语义在方法名中表示出来早已成为Objective-C的惯例,而ARC则将之确立为硬性规定。这些规则简单地体现在方法名上。若方法名以下列词语开头,则其返回的对象归调用者所有:
alloc
new
copy
mutableCopy

归调用者所有的意思是:调用上述四种方法的那段代码要负责释放方法所返回的对象。

若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。在这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后依然有效。要想使对象多存活一段时间,必须令调用者保留它才行。

维系这些规则所需的全部内存管理事宜均有ARC自动处理,其中也包括在将其返回的对象上调用autorelease,下列代码演示了ARC的用法:

+(EOCPerson *)newPerson{
    EOCPerson *person = [[EOCPerson alloc]init];
    return person;
    //这个方法用new开头的,不需要在返回的时候retain、release或autorelease
}

+(EOCPerson *)somePerson{
    EOCPerson *person = [[EOCPerson alloc]init];
    return person;
    //这个方法不是以拥有关系关键字开头的,所以ARC会自动在返回的时候加上autorelease
}

-(void)doSomething{
    EOCPerson *personOne = [EOCPerson newPerson];
    EOCPerson *personTwo = [EOCPerson somePerson];
    //personOne是作为被这段代码拥有的关系返回的,所以需要release,
    //personTwo不被这段代码拥有,不需要release
}

除了会自动调用“保留”与“释放”方法外,使用ARC还可以执行一些手工操作很难甚至无法完成的优化。例如,在编译器,ARC会把能够互相抵消的retain、release、autorelease操作约简。如果发现在同一个对象上执行多次“保留”与“释放”操作,那么ARC有时可以成对地移除这两个操作。

ARC可以在运行期监测到这一对多余的操作,也就是autorelease及紧跟其后的retain。为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊函数。此时不直接调用对象的autorelease方法,而是改为调用objc_autoreleaseReturnValue。此函数会检视当前方法返回之后即将要执行的那段代码。若发现那段代码在返回的对象上执行retain操作,则设置全局数据结构(此数据结构的具体内容因处理器而已)中的一个标志位而不执行autorelease操作。与之相似,如果方法返回了一个自动释放的对象,而调用方法的代码要保留此对象,那么此时不直接执行retain,而是改为执行objc_retainAutoreleaseReturnValue函数。此函数要检测刚才提到的那个标志位,若已经置位,则不执行retain操作。设置并检测标志位,要比调用autorelease和retain更快。

下面这段代码演示了ARC是如何通过这些特殊函数来优化程序的:

+(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_retainAutoreleaseReturnValue(tmp);

为了求得最佳效果,这些特殊函数的实现代码都因处理器而异。下面这段伪代码描述了其中的步骤:

id objc_autoreleaseReturnValue(id object){
    if(/*caller will retain object*/){
        set_flag(object);
        return object;///< no autorelease
    }else{
        return [object autorelease];
    }
}

id objc_retainAutoreleaseReturnValue(id object){
    if(get_flag(object)){
        clear_flag(object);
        return object;///< no retain
    }else{
        return [object retain];
    }
}

2. 变量的内存管理语义

ARC也会处理局部变量与实例变量的内存管理。默认情况下,每个变量都是指向对象的强引用。

@interface EOCClass : NSObject
{
    id _object;
}

@implementation EOCClass
-(void)setup{
    _object = [EOCOtherClass new];
}
@end

在手动管理引用计数时,实例变量_object并不会自动保留其值,而在ARC环境下则会这样做。也就是说,若在ARC下编译setup方法,则其代码会变为:

-(void)setup{
    id tmp = [EOCOtherClass new];
    _object = [tmp retain];
    [tmp release];
}

当然,在此情况下,retain和release可以消去。所以,ARC会将这两个操作化简掉,于是,实际执行的代码还是和原来一样。不过,在编写设置方法时,使用ARC会简单一些。如果不用ARC,那么需要像下面这样来写:

-(void)setObject:(id)object{
    [_object release];
    _object = [object retain];
}

但是这样写会出问题。加入新值和实例变量已有的值相同,如果只有当前对象还在引用这个值,那么设置方法中的释放操作会使该值的保留计数降为0,从而导致系统将其回收。接下来再执行保留操作,就会令应用程序崩溃。使用ARC之后,就不可能发生这种疏失了。在ARC环境下,与刚才等效的设置函数可以这么写:

-(void)setObject:(id)object{
    _object = object;
}

ARC会用一种安全的方式来设置:先保留新值,再释放旧值,最后设置实例变量。

在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:

__stong:默认语义,保留此值

__unsafe_unretained:不保留此值,这么做可能不安全,因为等到再次使用变量时,其对象可能已经回收了。

__weak:不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空。

__autoreleasing:把对象“按引用传递”给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。

比方说,想令实例变量的语义与不使用ARC时相同,可以运用__weak或__unsafe_unretained修饰符:

@interface EOCClass : NSObject
{
    id __weak _weakObject;
    id __unsafe_unretained _unsafeUnretainedObject;
}

不论采用上面哪种写法,在设置实例变量时都不会保留其值。
我们经常用__weak局部变量来打破循环引用。

3.ARC如何清理实例变量

使用ARC之后,不需要再编写像不使用ARC是的那种dealloc方法了,因为ARC借用Objective-C++的一项特性来生成清理例程(cleanup routime)。回收Objective-C++对象时,待回收的对象会调用所有C++对象的析构函数(destructor)。编译器如果发现某个对象里含有C++对象,就会生成名为.cxx_destruct的方法。而ARC则借助此特性,在该方法中生成清理内存所需的代码。

不过,如果有非Objective-C的对象,比如CoreFoundation中的对象或是由malloc()分配在堆中的内存,那么仍然需要清理。然而不需要像原来那样调用超类的dealloc方法。ARC下不能直接调用dealloc方法。ARC会自动在.cxx_destruct方法中生成代码并运行此方法。而在生成的代码中会自动调用超类的dealloc方法。ARC环境下,dealloc方法可以像这样写:

-(void)dealloc{
    CFRelease(_coreFoundationObject);
    free(_heapAllocatedMemoryBlob);
}

因为ARC会自动生成回收对象时所执行的代码,所以通常无须再编写dealloc方法。

4. 覆写内存管理方法

不使用ARC时,可以覆写内存管理方法。但是在ARC环境下不能这么做,因为会干扰到ARC分析对象生命期的工作。

要点:

  • 有ARC之后,程序员就无须担心内存管理问题了。使用ARC来编程,可省去类中的许多“样板代码”。
  • ARC管理对象声明周期的办法基本上就是:在合适的地方插入“保留”及“释放”操作。在ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行“保留”及“释放”操作。
  • 由方法所返回的对象,其内存管理语义总是通过方法名来提现。ARC将此确定为开发者必须遵守的规则。
  • ARC只负责管理Objective-C对象的内存。尤其要注意:CoreFoundation对象不归ARC管理,开发者必须实时调用CFRetain/CFRelease。
  • 不要在属性名前面加上alloc、new、copy或mutableCopy,否则编译器会报错(Property follows Cocoa naming for returning ‘owned’objects)可用此方法解决,但是强烈不建议这么用

31.在dealloc方法中只释放引用并解除监听

对象在经历其生命期后,最终会为系统所回收,这时就要执行dealloc方法了,然后具体何时执行,则无法保证。不应该自己调用dealloc方法,运行期会在适当的生活调用它。

在dealloc方法中主要是要释放对象所拥有的引用,也就是说把所有Objective-C对象都释放掉,ARC会通过自动生成的.cxx_destruct方法,在dealloc中为你自动添加这些释放代码。对象所拥有的其他非Objective-C对象也要释放。比如CoreFoundation的对象就必须手工释放,因为它们是由纯C的API所生成的。

在dealloc方法中,通常还要把原来配置过的观测行为都清理掉。如果用NSNotificationCenter给此对象注册过某种通知,那么一般应该在这里注销,这样的话,通知系统就不再把通知发给回收后的对象了。

dealloc方法可以这样来写:

-(void)dealloc
{
    CFRelease(coreFoundationObject);
    [[NSNotificationCenter defaultCenter]removeObserver:self];
}

如果是手动管理引用计数的话,最后还要调用”[super dealloc]”,ARC会自动执行此操作。手动管理还要将当前对象所拥有的全部Objective-C对象逐个释放。

虽说应该于dealloc中释放引用,但是开销较大或系统内稀缺的资源则不在此列。像是文件描述符(file descriptor)、套接字(socket)、大块内存等,都属于这种资源。不能指望dealloc方法必定会在某个特定的时机调用,因为有一些无法预料的东西可能也持有此对象。通常的做法是,实现另外一个方法,当应用程序用完资源对象后,就调用此方法。

比方说,如果某个对象管理着连接服务器所用的套接字,那么也许就需要这种“清理方法”。此对象可能要通过套接字连接到数据库。

对于对象所属的类,其接口可以这样写:

#import <Foundation/Foundation.h>

@interface EOCServerConnection : NSObject
-(void)open:(NSString *)address;
-(void)close;
@end

该类与开发者之间的约定是:想打开连接,就调用“open:”方法;连接使用完毕,就调用close方法。“关闭”操作必须在系统把连接对象回收之前调用,否则就是编程错误,这与通过“保留”与“释放”操作来平衡引用计数是类似的。

在清理方法而非dealloc方法中清理资源还有个原因,就是系统并不保证每个创建出来的对象的dealloc都会执行。极个别情况下,当应用程序终止时,仍有对象处于存活状态,这些对象没有收到dealloc消息。由于应用程序终止之后,其占用资源也会返还给操作系统,所以实际上这些对象也就等于是消亡了。不调用dealloc方法是为了优化程序效率。在Mac OS X及iOS应用程序所对应的application delegate中,都含有一个会于程序终止时调用的方法。如果一定要清理某些对象,那么可在此方法中调用那些对象的“清理方法”。

在Mac OS X系统里,应用程序终止时会调用NSApplicationDelegate之中的下述方法:

-(void)applicationWillTerminate:(NSNotification *)notification

而在iOS系统里,应用程序终止时则会调用UIApplicationDelegate之中的下述方法:

-(void)applicationWillTerminate:(UIApplication *)application

如果对象管理着某些资源,那么在dealloc中也要调用“清理方法”,以防开发者忘了清理这些资源。下面举例说明close与dealloc方法应如何写:

-(void)close{
    //clean up resources
    _closed = YES;
}

-(void)dealloc{
    if(!_closed){
        NSLog(@"ERROR:close was not called before dealloc");
        [self close];
    }
}

编写dealloc方法时还需注意,不要在里面随便调用其他方法。无论在这里调用什么方法都不应该,因为对象此时“已近尾声”。如果在这里所调用的方法又要异步执行某些任务,或是又要继续调用它们自己的某些方法,那么等到那些任务执行完毕时,系统已经把当前这个待回收的对象彻底摧毁了。

调用dealloc方法的那个线程会执行“最终的释放操作”,令对象的保留计数降为0,而某些方法必须在特定的线程里(比方说主线程里)调用才行。若在dealloc里调用了那些方法,则无法保证当前这个线程就是那些方法所需的线程。通过编写常规代码的方式,无论如何都没办法保证其会安全运行在正确的线程上,因为对象处在“正在回收的状态”,为了指明此种情况,运行期系统已经改动了对象内部的数据结构。

在dealloc里也不要调用属性的存取方法,因为有人可能会覆写这些方法,并于其中做一些无法在回收阶段安全执行的操作。此外,属性可能正处于“键值观测”机制的监控之下,该属性的观察者可能会在属性改变时“保留”或使用这个即将回收的对象。这种做法会令运行期系统的状态完全失调,从而导致一些莫名其妙的错误。

要点:

  • 在dealloc方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观察(KVO)”或NSNotificationCenter等通知,不要做其他事情。
  • 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其他使用者约定:用完资源后必须调用close方法。
  • 执行异步任务的方法不应该在dealloc里调用;只能在正常状态下执行的那些方法也不应该在dealloc里调用,因为此时对象已处于正在回收的状态了。

32.编写“异常安全代码”时留意内存管理问题

Objective-C的错误模型表明,异常只应发生严重错误后抛出,虽说如此,不过有时仍然需要编写代码来捕获并处理异常。比如使用Objective-C++来编码时,或是编码中用到了第三方程序库而此程序库所判处的异常又不受你控制时,就需要捕获及处理异常了。
在try块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,那么,除非catch块能处理此问题,否则对象所占内存就将泄露。
异常处理例程将自动销毁对象,然而在手动管理引用计数时,销毁工作有些麻烦。以下面这段代码使用手工引用计数的Objective-C代码为例:

@try {
        EOCSomeClass *object = [[EOCSomeClass alloc]init];
        [object doSomethingThatMayThrow];
        [object release];
    } @catch (NSException *exception) {
        NSLog(@"Whoopse,there was an error.Oh well...");
    }

如果doSomethingThatMayThrow抛出异常,由于异常会令执行过程终止并跳转至catch块,因而其后的那行release代码不会运行。在这种情况下,如果代码抛出异常,那么对象就泄露了。解决办法是使用@finally块,无论是否抛出异常,其中的代码都保证会运行,且只运行一次。代码可改写如下:

EOCSomeClass *object;
    @try {
        object = [[EOCSomeClass alloc]init];
        [object doSomethingThatMayThrow];
    } @catch (NSException *exception) {
        NSLog(@"Whoopse,there was an error.Oh well...");
    } @finally {
        [object release];
    }

由于@finally块也要引用object对象,所以必须把它从@try块里移到外面去。要是所有对象都得如此释放,那这样做就会非常乏味。而且@try块中的逻辑更为复杂,含有多条语句,那么很容易就会因为忘记某个对象而导致泄露。若泄露的对象是文件描述符或数据库连接等稀缺资源(或是这些稀缺资源的管理者),则可能引发大问题,因为这样导致应用程序把所有系统资源都抓在自己手里而不及时释放。

在ARC环境下,问题会更严重。下面这段使用ARC的代码与修改前的那段代码等效:

@try {
        EOCSomeClass *object = [[EOCSomeClass alloc]init];
        [object doSomethingThatMayThrow];
    } @catch (NSException *exception) {
        NSLog(@"Whoopse,there was an error.Oh well...");
    }

现在问题更大了:由于不能调用release,所以无法像手动管理引用计数那样把释放操作移到@finally块中。你可能认为这种状况ARC自然会处理的。但实际上ARC不会自动处理,因为这样做需要加入大量样板代码,以便跟踪待清理的对象,从而在抛出异常时将其释放。可是,这段代码会严重影响运行期的性能,即便在不抛异常时也如此。而且,添加进来的额外代码还会明显增加应用程序的大小。这些副作用都不甚理想。

虽说默认状况下未开启,但ARC依然能生成这种安全处理异常所用的附加代码。-fobjc-arc-exception这个编译器标志用来开启此功能。其默认不开启的原因是:Objective-C代码中,只有当应用程序必须因异常状况而终止时才抛出异常。因此,如果应用程序即将终止,那么是否还会发生内存泄露就已经无关紧要了。在应用程序必须立即终止的情况下,还去添加安全处理异常所用的附加代码是没有意义的。

有种情况编译器会自动把-fobjc-arc-exception标志打开,就是出于Objective-C++模式时。因为C++处理异常所用的代码与ARC实现的附加代码类似,所以令ARC加入自己的代码以安全处理异常,其性能损失并不太大。此外,由于C++频繁使用异常,所以Objective-C++程序员很可能也会使用异常。

如果手工管理引用计数,而且必须捕获异常,那么要设法保证所编代码能把对象正确清理干净。若使用ARC且必须捕获异常,则需打开编译器的-fobjc-arc-exception标志。但最重要的是:在发现大量异常捕获操作时,应考虑重构代码,用第21条所讲的NSError式错误信息传递法来取代异常。

要点:

  • 捕获异常时,一定要注意将try块内所创立的对象清理干净。
  • 在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可以生成这种代码,不过会导致应用程序变大,而且会降低运行效率。

33.用弱引用避免循环引用

对象图里经常会出现一种情况,就是几个对象都以某种方式互相引用,从而形成”环“。由于Objective-C内存管理模型使用引用计数架构,所以这种情况通常会泄露内存,因为最后没有别的东西会引用环中的对象。这样的话,环里的对象就无法为外界所访问了,但对象之间尚有引用,这些引用使得他们都能继续存活下去,而不会为系统所回收。

最简单的循环引用由两个对象构成,他们互相引用对方。如图:

这种循环引用的产生原因不难理解,且很容易就能通过查看代码而侦测出来:

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject
@property(nonatomic,strong)EOCClassB *other;
@end

@interface EOCClassB : NSObject
@property(nonatomic,strong)EOCClassA *other;
@end

如果把EOCClassA实例的other属性设置成某个EOCClassB实例,而把那个EOCClassB实例的other属性又设置成这个EOCClassA实例,那么就会出现下图的循环引用:

循环引用会导致内存泄露。如果只剩下一个引用还指向循环引用中的实例,而现在又把这个引用移除,那么整个循环引用就泄露了。也就是说,没办法再访问其中的对象了。

避免循环引用的最佳方式就是弱引用。这种引用经常用来表示“非拥有关系”。将属性声明为unsafe_unretained即可。修改刚才那段代码,将其属性声明如下:

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject
@property(nonatomic,strong)EOCClassB *other;
@end

@interface EOCClassB : NSObject
@property(nonatomic,unsafe_unretained)EOCClassA *other;

修改之后,EOCClassB实例就不再通过other属性来拥有EOCClassA实例了。属性特质中的unsafe_unretained一词表明,属性值可能不安全,而且不归此实例所拥有。如果系统已经把属性所指的那个对象回收了,那么在其上调用方法可能会使应用程序崩溃。由于本对象并不保留属性对象,因此其有可能为系统所回收。

用unsafe_unretained修饰的属性特质,其语义同assign特质等价,然而assign通常只用于数值类型,unsafe_unretained则多用于对象类型。这个词本身就表明其所修饰的属性可能无法安全使用。

Objective-C中还有一项与ARC相伴的运行期特性,可能令开发者安全使用弱引用:这就是weak属性特质,它与unsafe_unretained的作用完全相同。然而,只要系统把属性回收,属性值就会自动设置为nil。在刚才那段代码中,EOCClassB的other属性可修改如下:

@property(nonatomic,weak)EOCClassA *other;

下图演示了unsafe_unretained与weak属性的区别:

当指向EOCClassA实例的引用移除后,unsafe_unretained属性仍然指向那个已经回收的实例,而weak属性则指向nil。

使用weak属性而非unsafe_unretained引用可以令代码更安全。应用程序也许会显示出错误的数据,但不直接崩溃。

一般来说,如果不拥有某对象,那就不要保留它。这条规则对collection例外。collection虽然并不直接拥有其内容,但是它要代表自己所属的那个对象来保留这些元素。有时,对象中的引用会指向另外一个并不归自己所拥有的对象,比如Delegate模式就是这样。

要点:

  • 将某些引用设为weak,可避免出现“循环引用”。
  • weak引用可以自动清空,也可以不自动清空。自动清空是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读写其数据,因为这种引用不会指向已经回收过的对象。

34.以“自动释放池块”降低内存峰值

Objective-C对象的生命期取决于其引用计数。在Objective-C的引用计数架构中,有一项特性叫做“自动释放池”(autorelease pool)。释放对象有两种方式:一种是调用release方法,使其保留计数立即递减;另一种是调用autorelease方法,将其加入“自动释放池”中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空自动释放池时,系统会向其中的对象发送release消息。

创建自动释放池所用语法如下:

@autoreleasepool {
        //...
    }

一般情况下无须担心自动释放池的创建问题。Mac OS X与iOS应用程序分别运行于Cocoa及Cocoa Touch环境中。系统会自动创建一些线程,比方说主线程或是GCD机制中的线程,这些线程默认都有自动释放池,每次执行“事件循环“(event loop)时,就会将其清空。因此,不需要自己来创建”自动释放池块“。通常只有一个地方需要创建自动释放池块,那就是在mian函数里,我们用自动释放池来包裹应用程序的主入口点。比方说,iOS程序的main函数经常这样写:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

从技术角度看,不是非得有个”自动释放池块”才行。因为块的末尾恰好就是应用程序的终止处,而此时操作系统会把程序所占的全部内存都释放掉。虽说如此,但是如果不写这个块的话,那么由UIApplicationMain函数所自动释放的那些对象,就没有自动释放池可以容纳了,于是系统会发出警告信息来表明这一情况。所以说,这个池可以理解成最外围捕捉全部自动释放对象所用的池。

位于自动释放池范围内的对象,将在此范围末尾处收到release消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的池里。比方说:

@autoreleasepool {
        NSString *string = [NSString stringWithFormat:@"1 = %i",1];
        @autoreleasepool {
            NSNumber *number = [NSNumber numberWithInt:1];
        }
    }

在本例中有两个对象,它们都由类的工厂方法所创建,这样创建出来的对象会自动释放。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];
        }
    }

加上这个自动释放池之后,应用程序在执行循环时的内存峰值就会降低,不再像原来那么高了。内存峰值是指应用程序在某个特定时段内的最大内存用量。新增的自动释放池块可以减少这个峰值,因为系统会在块的末尾把某些对象回收掉。而刚才提到的那种临时对象,就在回收之列。

自动释放池机制就像”栈“一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。

是否应该用池来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没完成这一步,那就别急着优化。尽管自动释放池块的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。

如果在ARC出现之前就写过Objective-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 circles
        if(++i == 10){
            [pool drain];
            i = 0;
        }
        //Also drain at the end in case the loop is not a multiple of 10
       [pool drain]; 
    }

现在不需要再这样写代码了。采用随着ARC所引入的新语法,可以创建出更为”轻量级“的自动释放池。原来缩写的代码可能会每执行n次循环清空一次自动释放池,现在可以改用自动释放池块把for循环中的语句包起来,这样的话,每次执行循环时都会简历并清空自动释放池。

@autoreleasepool语法还有个好处:每个自动释放池均有其范围,可以避免无意间误用了那些在清空池后已为系统所回收的对象。比方说,考虑下面这段采用旧式写法的代码:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
id object = [self createObject];
[pool drain];
[self useObject:object];

调用”userObject:“方法时所传入的那个对象,可能已经为系统所回收了。同样的代码改用信使写法就变成了:

@autoreleasepool {
        id object = [self createObject];
    }
[self useObject:object];

这次根本就无法编译,因为object变量出了自动释放池块的外围就不可用了,所以在调用”useObject:“方法时不能用它做参数。

要点:

  • 自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入到最顶端的池里。
  • 合理运用自动释放池,可降低应用程序的内存峰值。
  • @autoreleasepool这种新式写法能创建出更为轻便的自动释放池。

35.用“僵尸对象”调试内存管理问题

向业已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行与否,完全取决于对象所占内存有没有为其他内容所覆写。而这块内存有没有移作他用,又无法确定,因此,应用程序只是偶尔崩溃。在没有崩溃的情况下,那块内存可能只复用了其中一部分,所以对象中的某些二进制数据依然有效。还有一种可能,就是那块内存恰好为另外一个有效且存活的对象所占据。在这种情况下,运行期系统会把消息发到新对象那里,而此对象也许能应答,也许不能。

Cocoa提供了“僵尸对象”(Zombie Object)这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”,而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。

将NSZombieEnabled环境变量设为YES,即可开启此功能。在Mac OS X系统中用bash运行应用程序时,可以这么做:

export NSZombieEnabled=“YES”
./app

给僵尸对象发送消息后,控制台会打印消息,而应用程序则会终止。打印出来的消息就像这样:

*** -[CFString respondsToSelector:]:message sent to deallocated instance 0x7ff9e9c080e0

在Xcode中开启方法为:编辑应用程序的Scheme,在对话框左侧选择”Run“,然后切换至”Diagnostics“分页,最后勾选”Enable Zombie Objects“选项。

僵尸对象的工作原理是什么呢?它的实现代码深植与Objective-C的运行期程序库、Foundation框架及CoreFoundation框架中。系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤。这一步就是把对象转化为僵尸对象,而不彻底回收。

下面代码有助于理解这一步所执行的操作:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface EOCClass : NSObject

@end


#import "EOCClass.h"

@implementation EOCClass

@end

void PrintClassInfo(id obj){
    Class cls = object_getClass(obj);
    Class superCls = class_getSuperclass(cls);
    NSLog(@"=== %s : %s ===",class_getName(cls),class_getName(superCls));
}

int main(int argc, char *argv[]){
    EOCClass *obj = [[EOCClass alloc]init];
    NSLog(@"Before release:");
    PrintClassInfo(obj);
    
    [obj release];
    NSLog(@"After release:");
    PrintClassInfo(obj);
}

本例代码中有个函数,可以根据给定的对象打印出所属的类及其超类名称。此函数没有直接给对象发送Objective-C的class消息,而是调用了运行期库里的object_getClass()函数。因为如果参数已经是僵尸对象了,那么给其发送Objective-C消息后,控制台会打印错误消息,而且应用程序会崩溃。本例代码将输出下面这种消息:

Before release:
=== EOCClass : NSObject ===
After release:
=== _NSZombie_EOCClass : nil ====

对象所属的类已由EOCClass变成_NSZombie_EOCClass。_NSZombie_EOCClass实际上是在运行期生成的,当首次碰到EOCClass类的对象要变成僵尸对象时,就会创建这么一个类。创建过程中用到了运行期程序库里的函数,它们的功能很强大,可以操作类列表。

僵尸类是从名为NSZombie的模板类里复制出来的。这些僵尸类没有多少事情可做,只是充当一个标记。接下类介绍它们是怎样充当标记的。首先来看下嘛这段伪代码,其中演示了系统如何根据需要创建出僵尸类,而僵尸类又如何把待回收的对象转化成僵尸对象。

//Obtain the class of the object being deallocated
    Class cls = object_getClass(slef);
    
    //Get the class's name
    const char *clsName =  class_getName(cls);
    
    //Prepend _NSZombie_ to the class name
    const char *zombieClsName = "_NSZombie_" + clsName;
    
    //See if the specific zombie class exists
    Class zombieCls = objc_lookUpClass(zombieClsName);
    
    //If the specific zombie class doesn't exist
    //then it needs to be created
    if(!zombieCls){
        //Obtain the template zombie class called _NSZombie_
        Class baseZombieCls = objc_lookUpClass("_NSZombie_");
        
        //Duplicate the base zombie class,where the new class's
        //name is the prepended string from above
        zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
    }
    
    //Perform normal destruction of the object being deallocated
    objc_destructInstance(self);
    
    //Set the class of the object being deallocated
    //to the zombie class
    objc_setClass(self,zombieCls);
    
    //The class of 'self' is now _NSZombie_OrignalClass

这个过程其实就是NSObject的dealloc方法所做的事。运行期系统如果发现NSZombieEnabled环境变量已设置,那么就把dealloc方法”调配“(swizzle)成一个会执行上述代码的版本。执行到程序末尾时,对象所属的类已经变为_NSZombie_OriginalClass了,其中OriginalClass指的是原类名。

代码中的关键之处在于:对象所占内存没有(通过调用free()方法)释放,因此,这块内存不可复用。虽说内存泄露了,但这只是个调试手段,制作正式发行的应用程序时不会把这项功能打开,所以这种泄露问题无关紧要。

但是,系统为何要给每个变为僵尸的类都创建一个对应的新类呢?这是因为,给僵尸对象发消息后,系统可由此知道该对象原来所属的类。假如把所有僵尸对象都归到NSZombie类里,那原来的类名就丢了。创建新类的工作由运行期函数objc_duplicateClass()来完成,它会把整个NSZombie类结构拷贝一份,并赋予其新的名字。副本类的超类、实例变量及方法都和复制前相同。还有种做法也能保留旧类名,那就是不拷贝NSZombie,而是创建继承自NSZombie的新类,但是用相应的函数完成此功能,其效率不如直接拷贝高。

僵尸类的作用会在消息转发例程(第12条)中体现出来。NSZombie类(以及所有从该类拷贝出来的类)并未实现任何方法。此类没有超类,因此和NSObject一样,也是个”根类“,该类只有一个实例变量,叫做isa,所有Objective-C的根类都必须有此变量。由于这个轻量级的类没有实现任何方法,所以发给它的全部消息都要经过”完整的消息转发机制“。

在完整的消息转发机制中,forwarding是核心,调试程序时,大家可能在栈回溯消息里看见过这个函数。它首先要做的事情就包含检查接收消息的对象所属的类名。若名称前缀为NSZombie,则表明消息接收者是僵尸对象,需要特殊处理。此时会打印一条消息,其中指明了僵尸对象所收到的消息及原来所属的类,然后应用程序就终止了。在僵尸类名中嵌入原来类名的好处,这时就可以看出来了。只要把NSZombie从僵尸类名的开头拿掉,剩下的就是原始类名。下面伪代码演示了这一过程:

//Obtain the class of the object being deallocated
    Class cls = object_getClass(slef);
    
    //Get the class's name
    const char *clsName =  class_getName(cls);
    
    //Check if the class is prefixed with _NSZombie_
    if(string_has_prefix(clsName,"_NSZombie_"){
        //If so ,this object is a zombie
        
        //Get the original class name by skipping past the
        //_NSZombie_,i.e taking the substring from character 10
        const char *originalClsName = substring_from(clsName,10);
        
        //Get the selector name of the message
        const char *selectorName = sel_getName(_cmd);
        
        //Log a message to indicate wich selector is
        //being sent to which zombie
        Log("*** -[%s %s]:message sent to deallocated instance %p",originalClsName,selectorName,self);
        
        //Kill the application
        abort();
    }

要点:

  • 系统在回收对象时,可以不将其真的回收,而是把它转化成为僵尸对象。通过环境变量NSZombieEnabled可开启此功能。
  • 系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变成僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。

36.不要使用retainCount

NSObject协议中定义了下列方法,用于查询对象当前的保留计数:

- (NSUInteger)retainCount

这个方法看上去似乎挺合理、挺有用的。它毕竟返回了保留计数,而此值对每个对象来说显然都很重要。但问题在于,保留计数的绝对数值一般都与开发者所应留意的事情完全无关。即便只在调试时才调用此方法,通常也还是无所助益的。

此方法之所以无用,其首要原因在于:它所返回的保留计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空,因而不会将后续的释放操作从返回值里减去,这样的话,此值就未必能真实反映实际的保留计数了。因此,下面这种写法非常糟糕:

while([]){
[object release]
}

这种写法的第一个错误是:它没考虑到后续的自动释放操作,只是不停地通过释放操作来降低保留计数,直至对象为系统所回收。假如此对象也在自动释放池里,那么稍后系统清空池子时还要把它再释放一次,而这将导致程序崩溃。

第二个错误在于:retainCount可能永远不返回0,因为有时系统会优化对象的释放行为,在保留计数还是1的时候就把它回收了。只有在系统不打算这么优化时,计数值才会递减至0。因此,保留计数可能永远都不会完全归零。所以说,这段代码就算有时能正常运行,也多半是凭运气,而非理性判断。对象回收之后,如果while循环仍然在运行,那么目前的运行期系统一般会直接令应用程序崩溃。

从来都不需要编写这种代码。这段代码所要实现的操作,应该通过内存管理来解决。开发者在期望系统于某处回收对象时,应该确保没有尚未抵消的保留操作,也就是不要令保留计数大于期望值。在这种情况下,如果发现某对象的内存泄露了,那么应该检查还有谁仍然保留这个对象,并查明为何没有释放此对象。

下面这段代码:

NSString *string = @"Some string";
NSLog(@"string retainCount = %lu",[string retainCount]);
    
NSNumber *numberI = @1;
NSLog(@"numberI retainCount = %lu",[numberI retainCount]);
    
NSNumber *numberF = @3.141f;
NSLog(@"numberF retainCount = %lu",[numberF retainCount]);

在64位Mac OS X系统中,用Clang4.1编译后,这段代码输出的消息如下:

string retainCount = 18446744073709551615
numberI retainCount = 9223372036854775807
numberF retainCount = 1

第一个对象的保留计数是264-1,第二个对象的保留计数是263-1。由于二者皆为“单例对象”,所以其保留计数都很大。系统会尽可能把NSString实现成单例对象。如果字符串像本例所举的这样,是个编译器常量,那么久可以这样来实现了。在这种情况下,编译器会把NSString对象所表示的数据放到应用程序的二进制文件里,这样的话,运行程序时就可以直接用了,无须再创建NSString对象。NSNumber也类似,它使用了一种叫做“标签指针”(tagged pointer)的概念来标注特定类型的数值。这种做法不使用NSNumber对象,而是把与数值有关的全部消息都放在指针值里面。运行期系统会在消息派发期检测到这种标签指针,并对它执行相应操作,使其行为看上去和真正的NSNumber对象一样。这种优化只在某些场合使用,比如范例中的浮点数对象就没有优化,所以其保留计数就是1。

另外,像刚才所说的那种单例对象,其保留计数绝对不会变。这种对象的保留及释放操作都是“空操作”。可以看到,即便两个单例对象之间,其保留计数也各不相同,系统对其保留计数的这种处理方式再一次表明:我们不应该总是依赖保留计数的具体值来编码。

要点:

  • 对象的引用计数看似有用,实则不然,因为任何给定的时间点上的“绝对引用计数”都无法反映对象生命期的全貌。
  • 引入ARC之后,retainCount方法就正式废止了,在ARC下调用该方法会导致编译器报错。

转载请注明出处:第五章 内存管理

参考:《Effective Objective-C 2.0》

推荐阅读更多精彩内容