Effective Objective-C(第29-36条)内存管理篇,ARC、循环引用、引用计数

    在OC这种面向对象语言里,内存管理是个重要概念。要像用一门语言写出内存使用效率较高且又没有bug的代码,就得掌握内存管理模型的种种细节。一旦理解这些规则,你就会发现,其实OC的内存管理没有那么复杂,再进入ARC之后就更为简单了。

第29条:理解引用计数

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

引用计数工作原理


上图演示了对象自创造出来之后经历一次“保留”以及两次“释放”操作的过程。
下图演示了ObjectB与ObjectC都引用了ObjectA。若ObjectB与ObjectC都不再使用ObjectA,则其保留计数降为0,于是便可以摧毁了。

        在OC中,调用了alloc方法所返回的对象由调用者所拥有。也就是说,调用者已通过alloc方法表达了想令该对象继续存活下去的意愿。不过请注意,这并不是说对象此时的引用计数必定是1.在alloc或者initWith方法的实现代码中,也许还有其他对象也保留了此对象,所以,其保引用计数可能大于1。引用计数这个感念就应该这样理解才对。觉不应该说引用计数一定是某个值,只能说你所执行的操作是递增了还是递减。

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

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

顺序很重要,retain需在release之前。

自动释放池

-(NSSring*)stringValue{
NSString *str = [[NSString alloc]initWithFormat:@"I am this:%@",self];
return [string autorelease];
}
NSString* str = [self stringValue];
NSLog(@"The string is:%@",str);
autorelease能延长对象生命周期,使其在跨越方法调用边界后依然可以存活一段时间。

循环引用

使用引用计数机制时,经常要注意的一个问题就是循环引用,就是程环形相互引用的多个对象。这将导致内存泄露,因为循环中的对象其引用计数不会将为0.

在垃圾收集环境中,通常将这种情况认定为“孤岛”(island of isolation)。此时,垃圾收集器会把三个对象全部回收走。而在OC的引用计数框架中,则享受不到这一个便利。通常采用“弱引用”(weak reference)来解决此问题。
【本节要点】
● 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其引用计数至少为1.若引用计数为正,则对象继续存活。当引用计数降为0时,对象就被销毁了。
● 在对象生命周期中,其余对象通过引用来保留或释放对象。保留和释放分别会递增和递减引用计数。

● 以下场景容易引起循环引用block、nstimer、parent-child模式、代理模式、自己引用自己的时候。都可能造成循环引用。

第30条:以ARC简化引用计数

引用计数这个概念相当容易理解。需要执行保留与释放操作的地方很容易就能看出来。所以Clang编译器项目带有一个“静态分析器”(static analyzer)用于指名程序里引用计数出问题的地方,举例:

if([self shouldLogMessage]){
NSString *message = [[NSString alloc]initWithFormat:@"I am object%p",self];
NSLog(@"message=%@",message);
}

上面的代码有内存泄露,由于message在大括号之内没有释放,造成内存泄露,编译器很容易看到这一点,也可以帮助程序员把对象释放的代码添加上去。这正是“静态分析器”要做的事。

于是上面的问题代码,经过ARC之后自动改写如下:

if([self shouldLogMessage]){
NSString *message = [[NSString alloc]initWithFormat:@"I am object%p",self];
NSLog(@"message=%@",message);
[message release];//Add by ARC
}

由于ARC会自动执行retain、release、autorelease等操作,所以直接在ARC环境下调用下面的方法是错误的,编译不通过

1. retain

2. release

3. autorelease

4. dealloc

实际上,ARC在调用这些方法时,并不通过普通的Objective消息派发机制,而是直接调用底层函数节省很多CPU周期。

使用ARC时必须遵循的方法命名规则

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

 alloc、new、copy、mutableCopy。举例:

+(EOCPerson)newPerson{
EOCPerson *person = [[EOCPerson alloc]init];
return person;
/*
这个方法以new开头,那么不需要retain、release和autorelease了
*/
}
+(EOCPerson)somePerson{
EOCPerson *person = [[EOCPerson alloc]init];
return person;
/*
这个方法以new、alloc等这些拥有“对象”的词语开头,所以就需要插入 [person autorelease]了
*/
}
-(void)doSomething{
EOCPerson *personOne = [EOCPerson newPerson];
EOCPerson *personTwo = [EOCPerson somePerson];
/*
personOne和personTwo已经到了作用范围,因此ARC需要清理他们
-personOne 拥有对象,所以需要release
-personTwo不拥有对象,所以不需要release
*/
除了会自动调用retain和release方法之外,使用ARC还有其他好处,它可以执行一些手工操作很难甚至无法完成的优化。例如,在编译期,ARC会把能够相互抵消的retain、release、autorelease操作简约。

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

__strong:默认语义保留此值

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

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

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

我们经常会给变量加上修饰符,用以打破由“块”(block,参见第40条)所引入的循环引用。block会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,就导

致循环引用了。可以使用__weak局部变量来打破这个循环引用:

NSURL *url = [NSURL URLWithString:@"http://www.example.com/")];
ECONetwrokFetcher *fetcher = [[EOCNetworkFetcher alloc]initWithURL:url];
ECONetworkFetch* __weak weakFetcher = fetcher;
[fetcher startWithCompletion:^(BOOL sucess){NSLog(@"Finished fetching from%@",weakFetcher.url);
}];

ARC如何清理实例变量

     ARC会在dealloc方法中插入这些代码。用了ARC之后,就不需要再编写这种dealloc方法了,因为ARC会借用OC的一项特性来生成清理例程。回收OC++对象时,待回收的对象会调用所有C++对象的析构函数。
不过,如果有非OC的对象,比如CoreFoundation中的对象或是由malloc()分派在堆上的内存,那么仍然需要清理。然而不需要
像原来那样调用父类的dealloc方法。ARC环境下,dealloc方法可以像这样写:
-(void) dealloc{
CFRelease(_coreFoundationObject);
free(_heapAllocateMemoryBlob);
}
因为ARC会自动生成回收对象时所执行的代码,所以通常无需再编写dealloc方法。

要点:

● 有了ARC后,程序员无需担心内存问题了。少写了很多样板代码

● ARC管理对象生命周期的办法基本上是:在合适的地方插入“retain”和“release”操作。在ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手动执行retain和release

● 由方法返回对象,其内存管理语义通过方法名来体现。ARC将此确定为开发者必须遵守的规则。

● ARC只负责管理OC的对象内存。尤其要注意:CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain和CFRelease

● ARC的好处还有在CF对象跟OC对象转换的时候经过__bridge_transfer,CF对象转化成OC,ARC负责销毁OC对象。还有block可以任意copy,ARC总是会自动释放block的!

第31条:在dealloc方法中使用引用并解除监听

对象经历其生命周期后,最终会为系统所收回,这时就要执行dealloc方法了。在每个对象的生命期内,次方法仅执行一次。那么dealloc方法中做些什么呢?把所有的OC对象都释放。ARC会通过自动生成的.cxx_destruct方法在dealloc中为你自动添加这些释放代码。在dealloc方法中,通常还需要做一件事,那就是把原来配置过的观测行为(observation behavior)都清理掉。

比如一些连接服务器的socket,这些资源应该使用自己的清理方法。比如open对应者close。

无论在dealloc里调用什么方法都不太应该,因为对象此时“已近尾声”,此时,盗用方法或者异步执行某些任务,显然是非常危险的。

● 在dealloc里,应该做的事情就是释放指向其他对象的引用,并取消KVO和NSNotifationCenter等

● 如果对象持有文件描述符等资源,那么应该专门编写一个方法来释放此种资源。

● 异步执行的方法不应在dealloc中调用。

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

许多时下流行的语言都提供了“异常”(exception)这一特性。纯C中没有异常,而C++与OC都支持异常。实际上,C++和OC的异常相互兼容,也就是说,从其中一门语言里抛出的异常能用另外一门语言来捕获。

发生异常时应该如何管理内存是个值得研究的问题。看下面的代码:

@try{
EOCSomeClass *object = [[EOCSomeClass alloc]init];
[object doSomethingThatMayThrow];
}
@catch(...)
{
    NSLog(@"Whoops there was an error.oh well");
}

如果doSomethingThatMayThrow抛出异常,由于异常会跳至catch块,因为那行release代码不会运行。在这种情况下,如果代码抛出异常,那么对象就泄露了。解决办法是使用@finally块,无论是否抛出异常,其中的代码都保证会运行,且只运行一次,如下所示:

@try{
EOCSomeClass *object = [[EOCSomeClass alloc]init];
[object doSomethingThatMayThrow];
[object release];
}
@catch(...)
{
    NSLog(@"Whoops there was an error.oh well");
}
@finally{
    [object release];
}

很遗憾,在ARC下,由于不是手动release,所以上面的情况ARC不会自动处理。因为这样做要插入大量样板代码。

● 捕获异常时,一定要注意将try块内所创立的对象清理干净

●默认情况下ARC不会生成安全处理异常所需要的清理代码。开启编译选项后,可以插入代码,不过会导致应用程序过大,而且会降低效率。

第33条:以弱引用避免循环引用

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


这样的引用,导致谁也不会被释放掉。还有多个对象的循环应用,如图:


避免循环引用的最佳方式就是弱引用。这种引用经常用来表示“非拥有关系”在MRC下使用unsafe_unratain和ARC下的weak。


上图中的虚线就是weak,

● 将某些引用设为weak,可避免出现“循环引用”。

● weak引用可以自动清空,也可以不清空。自动情况(autonilling)是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象

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

考虑下面代码:

for(int i=0;i<100000;i++){
    [self doSomethingWithInt:i];
}

如果“doSomethingWithInt”方法要创建临时对象,那么这些对象很可能会被放到自动释放池里。这样一来在执行for循环时,应用程序所占用内存就会持续上涨,而等到临界值对象都释放后,内存用量又会突然下降。
for(int i=0;i<100000;i++){
@autoreleasepool{
    [self doSomethingWithInt:i];
}
}
加上这个自动释放池之后,应用程序循环时的内存峰值就会降低。自动释放池就像“栈”(stack)系统创建好自动释放池后,将其推入栈中,而清空自动释放池,则相当于从栈中弹出。
“自动释放池”本身又有开销,所以是否使用“自动释放池”取决于应用程序。
● 自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池子里
● 合理运用自动释放池,可以降低应用程序的内存峰值
● @autoreleasepool这种新式写法能创建出更为轻便的自动释放池

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

Cocoa提供了“僵尸对象”(Zombie Object)这个非常方便的功能。它的实现代码深植于OC的运行期程序库、Foundation框架以及CoreFoundation框架中。他的原理是这样的:系统在即将回收对象时,如果发现xcode启用了对僵尸象功能,那么还将执行一个附加步骤:把对象转化为僵尸对象,而不彻底回收。

即便是使用了ARC,也依然会出现这种内存bug
● 系统在回收对象时,可以不将其真的回收,而是把它转化位僵尸对象。通过环境变量NSZombieEnable可以开启此功能。

● 系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序。

第36条:不要使用retainCount

这里我们禁止使用retainCount,无论是ARC环境还是MRC环境。

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

-(NSUInterger)retainCount;

然而ARC已经将此方法废弃了。实际上,如果在ARC中调用,编译器就会报错,跟ARC中调用retain、release、autorelease的情况是一样的。但是在非ARC下还是可以调用retainCount接口。为啥不要使用retainCount呢?

此方法之所以无用,首要原因在于:它返回的保留计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空,所以未必真实反应实际的保留计数了。

下面的写法是非常糟糕的

while([object retainCount]){
    [object release];
}

第二个错误在于:retainCount可能永远不返回0,因为有时候系统会优化对象的释放行为。

第三种情况:看下面的代码

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",[number retainCount]);

在64位MAC OSX 10.8.2系统中,用Clang 4.1编译后,这段代码输出的消息如下:

string retainCount = 18446744073709551615 //2^64-1
numberI retainCount = 923372036854775807 //2^63-1
number retainCount = 1

    string是个常量,编译器把NSString对象所表示的数据放到应用程序的二进制文件里,这样运行程序时就可以直接用了,无须再创建NSString对象。NSNumber也类似,它使用了一种叫做“标签指针”(tagged pointer)的概念来标注特定类型的数值。这总做法不使用NSNumber对象,而是把数值有关的全部消息放到指针值里面。运行期系统会在消息派发期间检测到这种标签指针,并对它志向相应操作,使其行为看上去和真正的NSNumber一样。这种优化在某些场合使用,但是浮点数就没有这个优化,保留计数还是1

官方“转向ARC”的翻译文档点击打开链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值