Effective Objective-C(第29-36条)内存管理篇,ARC、循环引用、引用计数
2014-08-10 15:08
141 查看
在OC这种面向对象语言里,内存管理是个重要概念。要像用一门语言写出内存使用效率较高且又没有bug的代码,就得掌握内存管理模型的种种细节。一旦理解这些规则,你就会发现,其实OC的内存管理没有那么复杂,再进入ARC之后就更为简单了。
上图演示了对象自创造出来之后经历一次“保留”以及两次“释放”操作的过程。
下图演示了ObjectB与ObjectC都引用了ObjectA。若ObjectB与ObjectC都不再使用ObjectA,则其保留计数降为0,于是便可以摧毁了。
在OC中,调用了alloc方法所返回的对象由调用者所拥有。也就是说,调用者已通过alloc方法表达了想令该对象继续存活下去的意愿。不过请注意,这并不是说对象此时的引用计数必定是1.在alloc或者initWith方法的实现代码中,也许还有其他对象也保留了此对象,所以,其保引用计数可能大于1。引用计数这个感念就应该这样理解才对。觉不应该说引用计数一定是某个值,只能说你所执行的操作是递增了还是递减。
顺序很重要,retain需在release之前。
autorelease能延长对象生命周期,使其在跨越方法调用边界后依然可以存活一段时间。
在垃圾收集环境中,通常将这种情况认定为“孤岛”(island of isolation)。此时,垃圾收集器会把三个对象全部回收走。而在OC的引用计数框架中,则享受不到这一个便利。通常采用“弱引用”(weak reference)来解决此问题。
【本节要点】
● 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其引用计数至少为1.若引用计数为正,则对象继续存活。当引用计数降为0时,对象就被销毁了。
● 在对象生命周期中,其余对象通过引用来保留或释放对象。保留和释放分别会递增和递减引用计数。
● 以下场景容易引起循环引用block、nstimer、parent-child模式、代理模式、自己引用自己的时候。都可能造成循环引用。
上面的代码有内存泄露,由于message在大括号之内没有释放,造成内存泄露,编译器很容易看到这一点,也可以帮助程序员把对象释放的代码添加上去。这正是“静态分析器”要做的事。
于是上面的问题代码,经过ARC之后自动改写如下:
由于ARC会自动执行retain、release、autorelease等操作,所以直接在ARC环境下调用下面的方法是错误的,编译不通过
1. retain
2. release
3. autorelease
4. dealloc
实际上,ARC在调用这些方法时,并不通过普通的Objective消息派发机制,而是直接调用底层函数节省很多CPU周期。
alloc、new、copy、mutableCopy。举例:
在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:
__strong:默认语义保留此值
__unsafe_unretained:不保留此值,这么做可能不安全,因为等到再次使用变量时,其对象可能已经回收了。
__weak:不保留此值,但是变量还可以安全使用,因为如果系统把这个对象回收了,会自动清空它
__autorelease:把对象按照引用传递给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。
我们经常会给变量加上修饰符,用以打破由“块”(block,参见第40条)所引入的循环引用。block会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,就导
致循环引用了。可以使用__weak局部变量来打破这个循环引用:
不过,如果有非OC的对象,比如CoreFoundation中的对象或是由malloc()分派在堆上的内存,那么仍然需要清理。然而不需要
像原来那样调用父类的dealloc方法。ARC环境下,dealloc方法可以像这样写:
因为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的!
比如一些连接服务器的socket,这些资源应该使用自己的清理方法。比如open对应者close。
无论在dealloc里调用什么方法都不太应该,因为对象此时“已近尾声”,此时,盗用方法或者异步执行某些任务,显然是非常危险的。
● 在dealloc里,应该做的事情就是释放指向其他对象的引用,并取消KVO和NSNotifationCenter等
● 如果对象持有文件描述符等资源,那么应该专门编写一个方法来释放此种资源。
● 异步执行的方法不应在dealloc中调用。
发生异常时应该如何管理内存是个值得研究的问题。看下面的代码:
如果doSomethingThatMayThrow抛出异常,由于异常会跳至catch块,因为那行release代码不会运行。在这种情况下,如果代码抛出异常,那么对象就泄露了。解决办法是使用@finally块,无论是否抛出异常,其中的代码都保证会运行,且只运行一次,如下所示:
很遗憾,在ARC下,由于不是手动release,所以上面的情况ARC不会自动处理。因为这样做要插入大量样板代码。
● 捕获异常时,一定要注意将try块内所创立的对象清理干净
●默认情况下ARC不会生成安全处理异常所需要的清理代码。开启编译选项后,可以插入代码,不过会导致应用程序过大,而且会降低效率。
这样的引用,导致谁也不会被释放掉。还有多个对象的循环应用,如图:
避免循环引用的最佳方式就是弱引用。这种引用经常用来表示“非拥有关系”在MRC下使用unsafe_unratain和ARC下的weak。
上图中的虚线就是weak,
● 将某些引用设为weak,可避免出现“循环引用”。
● weak引用可以自动清空,也可以不清空。自动情况(autonilling)是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象
如果“doSomethingWithInt”方法要创建临时对象,那么这些对象很可能会被放到自动释放池里。这样一来在执行for循环时,应用程序所占用内存就会持续上涨,而等到临界值对象都释放后,内存用量又会突然下降。
加上这个自动释放池之后,应用程序循环时的内存峰值就会降低。自动释放池就像“栈”(stack)系统创建好自动释放池后,将其推入栈中,而清空自动释放池,则相当于从栈中弹出。
“自动释放池”本身又有开销,所以是否使用“自动释放池”取决于应用程序。
● 自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池子里
● 合理运用自动释放池,可以降低应用程序的内存峰值
● @autoreleasepool这种新式写法能创建出更为轻便的自动释放池
即便是使用了ARC,也依然会出现这种内存bug
● 系统在回收对象时,可以不将其真的回收,而是把它转化位僵尸对象。通过环境变量NSZombieEnable可以开启此功能。
● 系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序。
NSObject协议中定义了下列方法,用于查询对象的保留计数:
然而ARC已经将此方法废弃了。实际上,如果在ARC中调用,编译器就会报错,跟ARC中调用retain、release、autorelease的情况是一样的。但是在非ARC下还是可以调用retainCount接口。为啥不要使用retainCount呢?
此方法之所以无用,首要原因在于:它返回的保留计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空,所以未必真实反应实际的保留计数了。
下面的写法是非常糟糕的
第二个错误在于:retainCount可能永远不返回0,因为有时候系统会优化对象的释放行为。
第三种情况:看下面的代码
在64位MAC OSX 10.8.2系统中,用Clang 4.1编译后,这段代码输出的消息如下:
string是个常量,编译器把NSString对象所表示的数据放到应用程序的二进制文件里,这样运行程序时就可以直接用了,无须再创建NSString对象。NSNumber也类似,它使用了一种叫做“标签指针”(tagged pointer)的概念来标注特定类型的数值。这总做法不使用NSNumber对象,而是把数值有关的全部消息放到指针值里面。运行期系统会在消息派发期间检测到这种标签指针,并对它志向相应操作,使其行为看上去和真正的NSNumber一样。这种优化在某些场合使用,但是浮点数就没有这个优化,保留计数还是1
官方“转向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”的翻译文档点击打开链接
相关文章推荐
- 自动引用计数(ARC)必须遵守一些规则
- Block的引用循环问题 (ARC & non-ARC)
- swift详解之九---------------自动引用计数、循环引用
- 初探swift语言的学习笔记六(ARC-自动引用计数,内存管理)
- [Swift]可选链和ARC引用计数
- __strong 修饰符内存分析及循环引用带来的内存泄露
- 【iOS】自动引用计数 (循环引用)
- 内存泄漏,循环引用
- block使用小结、在arc中使用block、如何防止循环引用
- C++智能指针类似OC 内存引用计数实现
- ARC机制下的循环引用【结合非ARC引用】
- iOS开发之ARC(自动引用计数)
- [iOS]ARC下循环引用的问题
- cocos2dx CCHttpRequest里面的内存引用计数的故事
- std::shared_ptr 和 std::weak_ptr的用法以及引用计数的循环引用问题
- Swift 自动引用计数(ARC)
- 【2015-10-19】内存管理---深浅拷贝,autorelease,ARC(自动引用计数)
- ARC环境下循环引用案例
- std::shared_ptr 和 std::weak_ptr的用法以及引用计数的循环引用问题
- 解决fastjson内存对象重复/循环引用json错误