2.iOS内存管理

来源:互联网 发布:大数据建模工程师 编辑:程序博客网 时间:2024/05/01 06:24

iOS笔记:内存管理

Objective-C提供了三种内存管理方式:manual retain-release(MRR,手动管理),automatic reference counting(ARC,自动引用计数),garbage collection(垃圾回收GC)。iOS不支持垃圾回收;ARC作为苹果新提供的技术,苹果推荐开发者使用ARC技术来管理内存;这篇笔记主要讲的是手动管理。

内存管理的目的是:
1.不要释放或者覆盖还在使用的内存,这会引起程序崩溃;
2.释放不再使用的内存,防止内存泄露。iOS程序的内存资源是宝贵的。

MRC手动管理内存也是基于引用计数的,只是需要开发者发消息给某块内存(或者说是对象)来改变这块内存的引用计数以实现内存管理(ARC技术则是编译器代替开发者完成相应的工作)。一块内存如果计数是零,也就是没有使用者(owner),那么objective-C的运行环境会自动回收这块内存。

objective-C的内存管理遵守下面这个简单的策略:
注:文档中把引用计数加1的操作称为“拥有”(own,或者take ownership of)某块对象/内存;把引用计数减1的操作称为放弃(relinquish)这块对象/内存。拥有对象时,你可以放心地读写或者返回对象;当对象被所有人放弃时,objective-C的运行环境会回收这个对象。
1.你拥有你创建的对象
也就是说创建的对象(使用alloc,new,copy或者mutalbeCopy等方法)的初始引用计数是1。
2.给对象发送retain消息后,你拥有了这个对象
3.当你不需要使用该对象时,发送release或者autorelease消息放弃这个对象
4.不要对你不拥有的对象发送“放弃”的消息


注:简单的赋值不会拥有某个对象。比如:
...
NSString *name = person.fullName;
...
上面这个赋值操作不会拥有这个对象(这仅仅是个指针赋值操作);这和C++语言里的某些基于引用计数的类的行为是有区别的。想拥有一个objective-C对象,必须发送“创建”或者retain消息给该对象。


dealloc方法
dealloc方法用来释放这个对象所占的内存(包括成员变量)和其它资源。
不要使用dealloc方法来管理稀缺资源,比如文件,网络链接等。因为由于bug或者程序意外退出,dealloc方法不能保证一定会被调用。


Accessor Methods和内存管理
Accessor Methods,也就是对象的property(属性)的getter和setter方法。显然,如果getter返回的对象已经被运行环境回收了,那么这个getter的返回值是毫无意义的。这就需要在setter方法里“拥有”相应的property。
比如:
@interface Counter : NSObject
@property (nonatomic, retain) NSNumber *count;
@end
getter方法仅仅返回成员变量就可以:
-(NSNumber *)count {
    return _count;
}
setter方法需要保证对这个成员变量的“拥有”:
-(void)setCount:(NSNumber *)newCount {
    [newCount retain]; //拥有新值
    [_count release]; //放弃老值
    _count = newCount; //简单赋值
}

使用Accessor Methods
以下是一种使用方式:
...
NSNumber *zero = [NSNumber alloc] initWithInteger:0];
[self setCount:zero];
[zero release];
...
以下是一种可能引发错误的,偷懒的使用方式:
...
NSNumber *zero = [NSNumber alloc] initWithInteger:0];
[_count release];
_count = zero; //这个代码做了不合理的假设-_count对象“拥有”了某个内存。当修饰count属性的
                         //attribute发声变化时,这个假设就不一定正确了。
...

不要在初始化方法(Initializer)和dealloc方法里使用Accessor Methods
不要在初始化方法里使用accessor methods的原因可能是(原文档中没有说明):在初始化方法里,成员变量处于最初的状态,并没有任何值。考虑到一个成员变量的setter方法一般会对成员变量的旧值发送release消息。这种行为在初始化方法里没有意义。
如果需要在Initializer里给成员变量赋值,可参见一开始提到的原始文档里给出的示例代码。


使用weak reference(弱引用)来避免retain cycle
对一个对象发送retain消息会创建对这个对象的强引用(strong reference)。如果两个对象都有一个强引用指向对方,那么就形成了一个环(retain cycle)。这个环使得这两个对象都不可能被release。
弱引用(weak reference)指的是一种non-owning(非拥有)的关系,比如简单指针赋值关系。使用弱引用避免了retain cycle。但是需要注意的是,弱引用不能保证弱引用指向的对象是否存在,所以发消息给这个对象时一定要小心。如果弱引用指向的对象已经释放,那么发送消息给它会导致程序崩溃。所以,需要一点点额外的操作来使用弱引用所指的对象。比如,当向notification center注册一个对象时,notification center保存了一个指向这个对象的弱引用。当这个对象被回收时,需要通知下notification center。


当你使用对象时,要确保这个对象不会被回收。主要要注意以下两种情形:
1.当一个对象从collection对象(collection指的数组之类的集合)移除时,如果这个仅被collection对象拥有,那么移除操作了会被即可回收。所以如果要使用这个将要移除的对象,要先retain。
2.当“父”对象回收时。这和情形1类似。


Autorelease Pool
Autorelease Pool可以延后发送release消息给一个对象。发送一个autorelease消息给一个对象,相当于说这个对象在“一定时期”内都有效,“一定时期”后再release这个对象。
Autorelease Pool几个要点:
-autorelease pool是一个NSAutoreleasePool对象。
-程序里的所有autorelease pool是以桟(stack)的形式组织的。新创建的pool位于桟的最顶端。当发送autorelease消息给一个对象时,这个对象被加到栈顶的那个pool中。发送drain给一个pool时,这个pool里所有对象都会受到release消息,而且如果这个pool不是位于栈顶,那么位于这个pool“上端”的所有pool也会受到drain消息。
-一个对象被加到一个pool很多次,只要多次发送autorelease消息给这个对象就可以;同时,当这个pool被回收时,这个对象也会收到同样多次release消息。简单地可以认为接收autorelease消息等同于:接收一个retain消息,同时加入到一个pool里;这个pool用来存放这些暂缓回收的对象;一旦这个pool被回收(drain),那么pool里面的对象会收到同样次数的release消息。
-UIKit框架已经帮你自动创建一个autorelease pool。大部分时候,你可以直接使用这个pool,不必自己创建;所以你给一个对象发送autorelease消息,那么这个对象会加到这个UIKit自动创建的pool里。某些时候,可能需要创建一个pool:
1.没有使用UIKit框架或者其它内含autorelease pool的框架,那么要使用pool,就要自己创建。
2.如果一个循环体要创建大量的临时变量,那么创建自己的pool可以减少程序占用的内存峰值。(如果使用UIKit的pool,那么这些临时变量可能一直在这个pool里,只要这个pool受到drain消息;完全不使用autorelease pool应该也是可以的,可能只是要发一些release消息给这些临时变量,所以使用autorelease pool还是方便一些)
3.创建线程时必须创建这个线程自己的autorelease pool。
-使用alloc和init消息来创建pool,发送drain消息则表示这个pool不再使用。pool的创建和drain要在同一上下文中,比如循环体内。

内存管理策略(memory Management Policy)
NSObject protocol中定义的的方法和标准命名惯例一起提供了一个引用计数环境,内存管理的基本模式处于这个环境中。NSObject类定义了一个方法叫dealloc,当对象销毁的时候,dealloc会被自动调用。本文描述,在Cocoa中所有正确管理内存基本规则,并提供了一些使用正确的例子。

【基本的内存管理规则】
内存管理模式基于对象的“所有权”上。任何对象都会被有一个或多个使用者引用,只要对象还有一个使用者,该对象就应该继续存在。如果一个对象没有使用者了,系统将自动销毁它。为了让开发者清晰的了解:使用对象和不再使用对象的场景,Cocoa设置了以下策略:
1.管好自己创建的对象。开发者使用alloc、new、copy和mutableCopy来创建对象。
2.使用retain来获得对象的所有权。某个函数接受的对象,通常保证在该函数调用期间仍然可用,并可以安全返回对象给上层调用者。开发者在以下两种情况下使用retain
1)在“访问函数”(accessor)的实现中或者在init方法,为了将对象作为自己的属性。
2)防止对象被其他操作释放掉,从而变为无效的对象。

3.当你不在需要的时候,必须放弃对象所有权。

一个简单的例子
看看下面的代码段,可以证明刚刚的所说的策略
  1. {   
  2.     Person *aPerson = [[Person alloc] init];   
  3.     // ...   
  4.     NSString *name = aPerson.fullName;   
  5.     // ...   
  6.     [aPerson release];   
  7. }   
Person被通过alloc创建之后,当Person不在使用的时候,发送了一个release的消息。name这个变量没有使用,所以name不必发送release消息。

使用autorelease来发送一个延迟的release
典型的使用场景:函数返回一个对象的时候。例如,你可以像这样实现fullName的方法:
  1. - (NSString *)fullName {   
  2.     NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@",   
  3.                                           self.firstName, self.lastName] autorelease];   
  4.     return string;   
  5. }   
上面就是典型的场景:你想放弃对象的所有权,但是又想让调用者在string销毁前使用返回值。

还可以通过下面的实现达到上面的效果:
  1. - (NSString *)fullName {   
  2.     NSString *string = [NSString stringWithFormat:@"%@ %@",   
  3.                                  self.firstName, self.lastName];   
  4.     return string;   
  5. }   
根据命名惯例,full name方法不具备返回值的所有权。因此,调用者无需对返回值string进行release

开发者不应该获得“通过引用传递的对象”的所有权
Cocoa中的一些方法,指定是传递引用。例如NSError对象包涵错误的信息,比如:initWithContentsOfURL:options:error: (NSData) and initWithContentsOfFile:encoding:error: (NSString).这种情况,之前的规则中已经描述过了。你调用这些方法,但是没有创建NSError对象,所以,你没有它的所有权。因此不用release,比如:
  1. NSString *fileName = <#Get a file name#>;   
  2. NSError *error;   
  3. NSString *string = [[NSString alloc] initWithContentsOfFile:fileName   
  4.                         encoding:NSUTF8StringEncoding error:&error];   
  5. if (string == nil) {   
  6.     // Deal with error...   
  7. }   
  8. // ...   
  9. [string release];   

【实现dealloc放弃对象的所有权】
NSObject类定义了一个方法dealloc,当某个对象没有使用者,并它的内存是可再生的,delloc就自动被调用。delloc的角色就是释放对象占用的内存并且处理自己所拥有的资源,包括本身变量的释放。
下面的代码展示了,如何实现Person类的dealloc函数。
  1. @interface Person : NSObject   
  2. @property (retain) NSString *firstName;   
  3. @property (retain) NSString *lastName;   
  4. @property (assign, readonly) NSString *fullName;   
  5. @end   
  6.     
  7. @implementation Person   
  8. // ...   
  9. - (void)dealloc   
  10.     [_firstName release];   
  11.     [_lastName release];   
  12.     [super dealloc];   
  13. }   
  14. @end   

【重要】
任何时候,不要直接调用某一对象的dealloc。
不许在dealloc的最后一行调用父类的dealloc
不要尝试管理系统资源。(参考内存管理实践)
应用程序终止的时候,对象的dealloc可能不会被调用。因为进程的内存是自动清除退出,让操作系统清理资源比调用所有的内存管理方法更有效地。

Core Foundation使用相似的却又不同的规则
Core Foundation对象使用类似的内存管理规则(查看Memory Management Programming Guide for Core Foundation),但是Cocoa和Core Foundation的命名管理并不相同。尤其是,Core Foundation的Create Rule(在Memory Management Programming Guide for Core Foundation中查看“The Create Rule” )并不适用于返回Objective-C对象的方法。比如以下代码片段:
  1. MyClass *myInstance = [MyClass createInstance]; 



 内存管理实践

尽管基本的概念在“内存管理策略”文章中简单得阐述了,但是还有一些实用的步骤让你更容易管理内存;有助于确保你的程序最大限度地减少资源需求的同时,保持可靠和强大。

 

使用“访问器方法”让内存管理更简单

假如,你的程序有一个对象类型的属性,你必须保证:当你使用的时候,任何的已经赋值了的对象不会被销毁。被赋新值的时候,开发者必须获得对象的所有权,并放弃正在使用对象的所有权。

 

有时候,这些听起来很老套和繁琐,如果开发者统一使用访问器方法,内存管理有问题的机会大大减少。如果开发者在代码中总是使用retain和release管理实例变量,几乎肯定会做错事,换句话说:访问器是必须的。

 

来看一个Counter对象

  1. @interface Counter : NSObject   
  2. @property (nonatomic, retain) NSNumber *count;   
  3. @end;   

该属性声明了两个访问器。典型的做法,开发者告诉编译器合成(synthesize)访问器方法。了解访问器是如何实现的对开发者是有好处的。

 

在get访问器中,开发者只需要返回变量即可,不需要retain和release

  1. - (NSNumber *)count {   
  2.     return _count;   
  3. }   

 

在set访问器方法里,如果每一位开发者都能按照相同的规则,对新的count进行负责,开发者必须通过retain来获得对象的所有权。开发者也需要通过release来放弃旧对象的所有权。(在Objective-c中给对象发送nil消息是允许的,即是_count没有被设置,仍然是安全的。)为了防止两个对象是相同的,开发者需要先调用[newCount retain](开发者可不希望意外的把对象给销毁)

  1. - (void)setCount:(NSNumber *)newCount {   
  2.     [newCount retain];// retain new object first;   
  3.     [_count release];   
  4.     // Make the new assignment.   
  5.     _count = newCount;   
  6. }   

 

【使用访问器方法设置属性值】

假设你想实现一个重置counter的方法。你有多个选择。

 

第一种方式:用alloc创建NSNumber实例,然后可以用release释放。

  1. - (void)reset {   
  2.     NSNumber *zero = [[NSNumber alloc] initWithInteger:0];   
  3.     [self setCount:zero];   
  4.     [zero release];   
  5. }   

第二种方法:用便捷构造方法创建NSNumber对象。因此,不用调用retain和release等。

  1. - (void)reset {   
  2.     NSNumber *zero = [NSNumber numberWithInteger:0];   
  3.     [self setCount:zero];   
  4. }   

注意:以上两种方法都使用访问器方法。

 

下面将几乎可以肯定,正常的情况下,因为它可能避开访问器方法,这样很诱人。这样做几乎肯定会导致一个错误在某些时候。(比如,当开发者忘记retain或者release,或者将来内存管理机制有变化)

  1. - (void)reset {   
  2.     NSNumber *zero = [[NSNumber alloc] initWithInteger:0];   
  3.     [_count release];   
  4.     _count = zero;   
  5. }   

 注意:如果开发者使用key-value observing,上面这种方法也不属于KVO范畴

 

不要在初始化和dealloc函数中使用访问器方法

唯一不让使用访问器的地方就是initializer和dealloc。为了将counter初始化为零,开发者可以这样实现:

  1. - init {   
  2.     self = [super init];   
  3.     if (self) {   
  4.         _count = [[NSNumber alloc] initWithInteger:0];   
  5.     }   
  6.     return self;   
  7. }   

 

为了初始化为非零数据,可以这么实现initWithCount方法:

  1. - initWithCount:(NSNumber *)startingCount {   
  2.     self = [super init];   
  3.     if (self) {   
  4.         _count = [startingCount copy];   
  5.     }   
  6.     return self;   
  7. }   

 

既然Counter类有一个类属性,开发者还需要实现dealloc,dealloc通过发送release,将放弃左右对象的所有权,最终dealloc还会调用父类的dealloc函数。

  1. - (void)dealloc {   
  2.     [_count release];   
  3.     [super dealloc];   
  4. }   

 

使用弱引用(Weak References)来避免循环retain

对一个对象retain操作是强引用(strong reference)。所有强引用都被release之后对象才被销毁。如果,两个对象有彼此的强引用,就出现众所周知的问题——循环retain。(包括:直接,或通过其他对象强引用链的情况)

 

对象的关系如图所示,有一个潜在的循环retain。Document对象每一页都有一个Page对象。每个Page对象有一个paragraphs属性,表明该Page在那一Document中。如果Document中的Page是强引用,Page类中的paragraphs属性也是强引用,那个对象都不会被销毁。Document的引用值直到Page对象被释放才变为0,而Page对象直到Document释放才被释放。

解决循环引用的的方法是使用弱引用。弱引用是一种非占有所有权的关系,不对源对象retain,只是引用(reference)。然后,为了保持对象图的完整性,强引用还是必要的(如果只有弱引用,pages和paragraphs将没有任何的所有者,也就不能被释放)Cocoa形成了一个惯例:父对象应该强引用子对象,子变量应该弱引用父对象。所以,在图1中,Document对象强引用page对象,page对象弱引用Document对象。

 

Cocoa中的弱引用例子包含了(但不限于)table data sources, outline view items, notification observers, and miscellaneous targets and delegates.

 

开发者给弱引用对象发送消息应小心一些。如果给一个已经销毁的对象发消息,程序将crash。当对象可用的时候,开发者需具备良好的定义条件(You must have well-defined conditions for when the object is valid.)。

 

大多数情况:弱引用对象知道其他对象弱引用了自己,当自己被销毁的时候,有责任通知其他对象。比如,当开发者用notification center注册一个对象,notification center存储一个弱引用对象,并发送post消息给对象。当对象已经被销毁了。

 

开发者需从notification center中注销对象,防止notification center发送消息给不存在的对象。同样,当一个delegate对象被销毁后,开发者嘘移除delegate通过发送一个参数为nil的setDelegate消息而这些消息通常从对象的dealloc中发送。

 

避免销毁正在使用的对象

Cocoa's所有权策略指定接受的对象应该,保证在函数调用范围可用;还可以返回接受的对象,而不用担心被release掉。保证从程序中的gerrer方法中返回实例变量或者计算后的值是没问题的。

 

问题是,当需要的时候对象仍然有效。偶尔有些例外,主要是下面两种情况:

 

1.从基础集合类中移除对象

  1. heisenObject = [array objectAtIndex:n];   
  2. [array removeObjectAtIndex:n];   
  3. // heisenObject could now be invalid.   

当对象从基本集合类移除,集合类发送一个release(而不是autorelease)消息。如果集合类是对象的唯一拥有者,被移除的对象(例子中heisenObject)就被立即销毁。

 

2.父对象被销毁

  1. id parent = <#create a parent object#>;   
  2. // ...   
  3. heisenObject = [parent child] ;   
  4. [parent release]; // Or, for example: self.parent = nil;   
  5. // heisenObject could now be invalid.   

 某些情况,开发者从其他对象获得一个对象,直接或见解释放父对象。release父对象导致被销毁,父对象又是子对象的唯一拥有者,子对象将同时被销毁。

 

防止这些情况,当开发者收到heisenObject时retain,使用完release掉,比如:

  1. heisenObject = [[array objectAtIndex:n] retain];   
  2. [array removeObjectAtIndex:n];   
  3. // Use heisenObject...   
  4. [heisenObject release];   

 

不要对稀缺资源进行dealloc

不要在dealloc函数中管理file descriptor、network connections、buffer和caches这些资源。通常,开发者不应设计带有dealloc这样的类。dealloc可能延迟调用,要么就成为程序的一个bug或者造成程序崩溃。

 

相反,如果你有一个稀缺资源的类,你应该这样设计应用程序,例如,你知道当你不再需要的资源的时候,然后可以告诉实例clean up。通常,你会再释放该实例,紧接着调用dealloc,你不会受到额外的问题。

 

如果您尝试在dealloc中背驮式得资源管理,可能会出现问题。

1.顺序依赖被拆散。虽然开发者可能希望得到一个特定顺序,被拆散的对象图本质上是无序的。如果对象是被autorelease的,被拆散的顺序可能还有变化,也可能导致意想不到的结果。

2.非回收式的稀缺资源。内存泄露是可以修复bug,内存泄露的伤害不是立即致命的。如果稀缺资源在不能释放的时候,被你释放了,你可能会碰到更严重的问题。如果你的应用程序使用文件描述符(file descriptor),可能导致不能写数据。

3.在错误的线程中执行cleanup逻辑。如果一个对象被开发者设置为是autorelease的,它会被任意一个“它正好存在于的”线程的自动释放池给释放掉。这是很容易的致命错误:该资源应该在一个线程中使用和释放。

(If an object is autoreleased at an unexpected time, it will be deallocated on whatever thread’s autorelease pool block it happens to be in. This can easily be fatal for resources that should only be touched from one thread)

 

集合拥有它所包含的对象

当你添加一个对象到集合(如,array,dictionary和set),集合获得对象的所有权。对象被移除的时候或者集合本身release的时候,放弃对象的所有权。如果开发者想创建一个带有粒度的array,可以这么搞:

  1. NSMutableArray *array = <#Get a mutable array#>;   
  2. NSUInteger i;   
  3. // ...   
  4. for (i = 0; i < 10; i++) {   
  5.     NSNumber *convenienceNumber = [NSNumber numberWithInteger:i];   
  6.     [array addObject:convenienceNumber];   
  7. }   

 

这种情况,开发者没有调用alloc,所以无需掉用release。也没有必要retain新的convenienceNumber。

  1. NSMutableArray *array = <#Get a mutable array#>;   
  2. NSUInteger i;   
  3. // ...   
  4. for (i = 0; i < 10; i++) {   
  5.     NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];   
  6.     [array addObject:allocedNumber];   
  7.     [allocedNumber release];   
  8. }   

这个情况,开发者需要向allocedNumber发送一个release消息在for的作用域之内,来匹配alloc。既然array的addObject方法 retain了allocedNumber,allocedNumber就会被array管理删除。

 

要像理解这些,把自己当做设计集合类的开发者们。你想保证:即使不在你照看下,集合中的变量能仍然好用,所以,当对象传入的时候,你retain了一次。当对象移除集合类,你需要发送release消息。

 

对象所有权策略是基于引用计数实现的

对象所有权的策略是通过引用计数——通常叫做retain count实现的。每一个对象有一个retaincount变量。

1.创建对象后,它的retaincount是1

2. retain之后,retain count +1

3.release之后 retain count -1 

4.autorelease之后,在自动释放池最后-1

5.对象的retain count减少到0的时候,对象被销毁。

 

【重要】

不要显式调用对象的retainCount,结果往往具有误导性,作为开发者可能不了解框架式如何对对象retain的。在调试内存管理中,你应该只关注确保你的代码遵循所有权规则。

 


关于iOS内存管理

应用程序内存管理是:“程序运行时,开辟的内存空间。使用它,释放它”的过程,写的好的程序尽可能少使用内存。在Objective-C中,内存管理被看做是:“在很多数据、代码下,分配受限内存资源所有权方法”。当你依据这个指南完成你的程序时,你将获得“通过显式管理对象的命周期,不使用的时候释放他们,来管理程序内存”的知识。

 

尽管,典型的内存管理是作用于单个对象,你的目标是通过管理对象图。你想确保:在内存中没有比实际需要的还多的对象。

概述

Objective-C提供两种内存管理的方法:

1.“manual-release”(MRR),需要显式管理内存通过跟踪对象的所有权。MRR基于NSObject类在运行时提供的引用计数实现的。

2.“Automatic Reference Counting”ARC,系统使用相同的引用计数基于MRR,但是在编译时,为开发者适当插入一些内存管理方法。强烈建议开发者在新项目中使用ARC。使用ARC就无需理解本文所描述的内容了。

 

防止内存相关问题的好的做法

两个主要的内存管理误用问题

1.释放或覆盖正在使用的数据。这将造成内存损坏,造成应用程序崩溃,或者更坏的情况是损坏用户数据。

2.没有释放数据,导致内存泄露。泄漏导致应用程序的内存使用量逐渐增加,这反过来又可能会导致系统性能较差或者应用程序被终止(crash)

 

引用计数内存管理的角度思考,但是,往往是适得其反,因为你往往会考虑内存管理方面的实现细则,而不是在你的实际目标。相反,你应该想到的内存管理对象所有权和对象图的角度。

1.Cocoa使用简单的命名惯例来指示,是否拥有函数返回的对象。(点击查看内存管理策略)

2.尽管内存管理基本策略很简单,有一些实际的步骤,你可以使内存管理更轻松,有助于确保你的程序仍然可靠和稳定的,而在同一时间最大限度地减少资源需求。(点击查看内存管理实践)

3.Autorelease pool 提供一种机制:让对象延迟release。这个对你想放弃所有权,但又想避免立即释放(比如函数的返回值)。有些时候,你可能会使用自己的autorelease池块。(点击查看自动释放池)。

 

使用分析工具来调试内存问题

在编译时候找出代码的问题。使用Xcode内嵌的Clang Static Analyzer 。

如果内存管理的问题仍然发生,还有其他的工具和技术,你可以用它来识别和诊断问题。

1.多数工具和技术都在TN2239中有描述,iOS Debugging Magic 特别是NSZombie来帮助找到过度释放对象。

2.使用Instruments来追踪引用计数事件并找到内存泄露。


iOS内存管理

分类: iOS2012-03-21 00:29 19899人阅读 评论(20) 收藏 举报
iosxcodetimerinterfaceleakclass

1.  内总管理原则(引用计数)
    IOS的对象都继承于NSObject,   该对象有一个方法:retainCount ,内存引用计数。 引用计数在很多技术都用到: window下的COM组件,多线程的信号量,读写锁,思想都一样。
    
   (一般情况下: 后面会讨论例外情况)
    alloc      对象分配后引用计数为1
    retain    对象的引用计数+1
    copy      copy 一个对象变成新的对象(新内存地址) 引用计数为1 原来对象计数不变
   
    release            对象引用计数-1 如果为0释放内存
    autorelease      对象引用计数-1 如果为0不马上释放,最近一个个pool时释放    
NSLog(@"sMessage retainCount:%u",[sMessage retainCount]);
[sMessage retain]; //2
NSLog(@"sMessage retainCount:%u",[sMessage retainCount]);
NSLog(@"sMessage retainCount:%u",[sMessage retainCount]);
NSLog(@"sMessage retainCount:%u",[sMessage retainCount]);
    内存管理的原则就是最终的引用计数要平衡,
   如果最后引用计数大于0  则会内存泄露
   如果引用 计数等于0还对该对象进行操作,则会出现内存访问失败,crash    所以尽量设置为nil
   
   这两个问题都很严重,所以请一定注意内存释放和不用过后设置为nil
2. autoReleasePool
   每个工程都有一个 main.m 文件: 内容如下:
   int main(int argc, char *argv[]) {
   
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    int retVal = UIApplicationMain(argc, argv, nil, nil);
    [pool release];
    return retVal;
}
   很明显C语言的main 函数: 
  NSAutoreleasePool 是用来做autorelease 变量释放的,前面说了, autorelease不会马上释放,当他到了最近的pool release 时会检查reatin count 是不是为0, 为0就释放。 
当我们在一段代码时加入了大量的autorelease变量时,我们应该为这段代码加上
Autoreleasepool,其它时候不用
在返回一个对象时常常要返回一个autorelease 对象,因为objectC 对象都是动态内存,没有
栈的概念,所以不能像C/C++一样返回一个对象到栈,只能用autorelease对象。
3. 成员变量与属性
    实际情况并非上面那么简单,你可能需要在一个函数里调用另一个函数分配的变量这时候
有两个选择:  类成员变量和使用属性
   @interface TestMem: NSObject {
            TestObject   *m_testObject ;            // 成员变量
            TestObject   *testObject;                 //成员变量
    }
 成员变量与上面的内存管理是一致的,只是在不同的函数里要保持引用计数加减的平衡
所以要你要每次分配的时候检查是否上次已经分配了。是否还能调用 
什么时候用属性?
  1. 把成员做为public.
  2. outlet 一般声明为属性( 这个内存于系统控制,但我们还是应该做一样操作,后面会讲)
  3. 如果很多函数都需要改变这个对象 ,或这个函数会触发很多次,建议使用属性。我们看看属性函数展开后是什么样子:

// assign
-(void)setTestObject :(id)newValue{
    testObject= newValue;
}
// retain
-(void)setTestObject  :(id)newValue{
    if (testObject!= newValue) {
        [testObject release];
        testObject= [newValue retain];
    } 
}
// copy
-(void)setTestObject :(id)newValue{
    if (testObject != newValue) {
        [testObject release];
        testObject = [newValue copy];
    }
}
asssign 相于于指针赋值,不对引用计数进行操作,注意原对象不用了,一定要把这个设置为nil
retain 相当于对原对象的引用计数加1
copy 不对原对象的引用计数改变,生成一个新对象引用计数为1
注意:
  self.testObject 左值调用的是setTestObject 方法. 右值为get方法,get 方法比较简单不用说了
  而 真接testObject 使用的是成员变量
  self.testObject = [[testObject alloc] init];   // 错  reatin 两次
  testObject =   [NSArray objectbyindex:0];  //错 不安全,没有retain 后面release会出错
                                                                  如果testObject已有值也会mem leak       
4. 自动管理对象
   IOS 提供了很多static(+) 创建对象的类方法,这些方面是静态的,可以直接用类名
调用如:
   NSString *testString = [NSString stringWithFormat:@"test" ];
   testString 是自动管理的对象,你不用relese 他,他有一个很大的retain count, release后数字不变。
  
5. 例外
   有一些通过alloc 生成的对象相同是自动管理的如:
   NSString  *testString  = [[NSString alloc] initWithString:@"test1"];
   retain count 同样是很大的数,没办法release
   但为了代码对应,还是应该加上[ testString release];
   不然xcode的Analyze 会认识内存leak, 但Instruments leak 工具检测是没有的
6.view  内存管理
   通常我们编程会用到view,   在view 中的方法:
   
  viewDidload 
  didReceiveMemoryWarning 
 viewDidUnload 
@property (retain) NSArray *iarrTestMem
        
        viewDidLoad
            init retain is         0
            (alloc)               +1 =1
                                    
        if(memwarning)
               didrecivememwarning  -0 =1
                                               
               didviewunload        -1
                                           =0  
                                  will load  viewDidLoad when next load view
        esle
               dealloc:             -1
                                      =0
    我们来看一个变量的生命周期 
 
   当view被alloc
     A. 框架会自动调用viewDidLoad
         一般来说在viewDidLoad 中分配变量: 
         假设为alloc 现在变量的reatin 为 0 +1 = 1;
     
     第一种情况:
      B. 如果这时候view被释放,会调用 dealloc, 
        这时候我们应该在dealloc里release 这个变量现在为0
     
     第二种情况:
       B. 这时候view被换到下层,如navigation的上级,不可显示状态:如果
          系统内存吃紧: 系统会发 消息,我们的didrecivememwarning 函数
          被调用 ,该函数是提醒我们应该释放一些现在用不上的东西,特别是一些较大的
          如图片,cache 数据等, 注意如果在这里释放了,代码在用的地方要进行
          判断,是否需要重新加载
       C. 他会调用didviewunload
          这时候我们要注意了,因为他调用了didviewunload,在下次这个view被显示的时候
         会再次调用didviewload, 我们在didviewload里分配的娈童就会被分配两次,所以我
         们在这里一定要释放didview里分配的变量,不管直接分配的还是间接分配的,
         如发消息给其它函数分配的。 那我们在下次分配之前释放一次行不行, 不行,因为这
         时候内存吃紧了,你应该多释放内存,不然你的程序可能会被系统kill. 还有虽然对
         [xxx release]  对nil 发 release 是没问题的,但逻辑上让人觉得很奇怪,以为在别的
         地方分配过。 所以这里应该释放内存,
            如果你是一个属性,用:
                  self.xxx = nil 比较好,他相当于帮你释放了原来的,还把xxx设置为了nil.
           如果不是一个属性 :也最好把xxx= nil.  这样比较安全。如果这个view不再被换入
          下一步直接调用 dealloc, 这时候你的dealloc里的[xxx release], 如果这里的xxx不为nil你在
          didviewunload里release 了,就非常危险
        D. 如果这时候view被释放,会调用 dealloc, 
           这时候我们应该在dealloc里release 
    
前面我们说了outlet的内存没办法管理(看下生命周期)
   
   
  @property (nonatomic,retain) IBOutlet UILabel *ilblTestMem;
         init retain is 2
         
         didviewLoad:     
                              = 2
         if(memwarning)
                 memwaring       - retain count -1
                                         = 1
                viewdidunload:   -1  
                                         =0
                              will load  viewDidLoad when next load view
        else
             dealloc:         -1
                                =1
      第一种情况
       A. didviewLoad:
                retain count 为2 
       B.  dealloc:
                 -1   retain count 为1
      第二种情况:
       B.   memwarning 
               系统会把 retain count 减到1
       C.   viewdidunload
               我们应该release 一次 这样 retain count 到0,变量会马上dealloc,更快的释放内存
               注意用 self.ilblTestMem = nil  设置为nil,  为 dealloc 做准备
       D.  dealloc:
              -1 

NSObject *sMessage = [[NSObjectalloc]init]; //1

     

[sMessagerelease]; //1

NSLog(@"sMessage retainCount:%u",[sMessage retainCount]); 


[sMessagerelease]; //0

//not crash  (retainCount also can call)

//crash  (can not call)

[sMessagerelease];

@property (nonatomic, retain) TestObject  testObject*; //为testObject成员变量生成属性方法

@end



iPhone/Mac Objective-C内存管理教程和原理剖析

 

前言

初学objectice-C的朋友都有一个困惑,总觉得对objective-C的内存管理机制琢磨不透,程序经常内存泄漏或莫名其妙的崩溃。我在这里总结了自己对objective-C内存管理机制的研究成果和经验,写了这么一个由浅入深的教程。希望对大家有所帮助,也欢迎大家一起探讨。

 

此文涉及的内存管理是针对于继承于NSObject的Class。

一 基本原理

Objective-C的内存管理机制与.Net/Java那种全自动的垃圾回收机制是不同的,它本质上还是C语言中的手动管理方式,只不过稍微加了一些自动方法。

1           Objective-C的对象生成于堆之上,生成之后,需要一个指针来指向它。

ClassA *obj1 = [[ClassA alloc] init];

 

2           Objective-C的对象在使用完成之后不会自动销毁,需要执行dealloc来释放空间(销毁),否则内存泄露。

[obj1 dealloc];

         这带来了一个问题。下面代码中obj2是否需要调用dealloc?

ClassA *obj1 = [[ClassA alloc] init];

ClassA *obj2 = obj1;

[obj1 hello]; //输出hello

[obj1 dealloc];

[obj2 hello]; //能够执行这一行和下一行吗?

[obj2 dealloc];

         不能,因为obj1和obj2只是指针,它们指向同一个对象,[obj1 dealloc]已经销毁这个对象了,不能再调用[obj2 hello]和[obj2 dealloc]。obj2实际上是个无效指针。

         如何避免无效指针?请看下一条。

 

3           Objective-C采用了引用计数(ref count或者retain count)。对象的内部保存一个数字,表示被引用的次数。例如,某个对象被两个指针所指向(引用)那么它的retain count为2。需要销毁对象的时候,不直接调用dealloc,而是调用release。release会让retain count减1,只有retain count等于0,系统才会调用dealloc真正销毁这个对象。

ClassA *obj1 = [[ClassA alloc] init]; //对象生成时,retain count = 1

[obj1 release]; //release使retain count减1,retain count = 0,dealloc自动被调用,对象被销毁

我们回头看看刚刚那个无效指针的问题,把dealloc改成release解决了吗?

ClassA *obj1 = [[ClassA alloc] init]; //retain count = 1

ClassA *obj2 = obj1; //retain count = 1

[obj1 hello]; //输出hello

[obj1 release]; //retain count = 0,对象被销毁

[obj2 hello];

[obj2 release];

         [obj1 release]之后,obj2依然是个无效指针。问题依然没有解决。解决方法见下一条。

 

4           Objective-C指针赋值时,retain count不会自动增加,需要手动retain。

ClassA *obj1 = [[ClassA alloc] init]; //retain count = 1

ClassA *obj2 = obj1; //retain count = 1

[obj2 retain]; //retain count = 2

[obj1 hello]; //输出hello

[obj1 release]; //retain count = 2 – 1 = 1

[obj2 hello]; //输出hello

[obj2 release]; //retain count = 0,对象被销毁

问题解决!注意,如果没有调用[obj2 release],这个对象的retain count始终为1,不会被销毁,内存泄露。(1-4可以参考附件中的示例程序memman-no-pool.m)

这样的确不会内存泄露,但似乎有点麻烦,有没有简单点的方法?见下一条。

 

5           Objective-C中引入了autorelease pool(自动释放对象池),在遵守一些规则的情况下,可以自动释放对象。(autorelease pool依然不是.Net/Java那种全自动的垃圾回收机制)

5.1          新生成的对象,只要调用autorelease就行了,无需再调用release!

ClassA *obj1 = [[[ClassA alloc] init] autorelease]; //retain count = 1 但无需调用release

 

5.2          对于存在指针赋值的情况,代码与前面类似。

ClassA *obj1 = [[[ClassA alloc] init] autorelease]; //retain count = 1

ClassA *obj2 = obj1; //retain count = 1

[obj2 retain]; //retain count = 2

[obj1 hello]; //输出hello

//对于obj1,无需调用(实际上不能调用)release

[obj2 hello]; //输出hello

[obj2 release]; //retain count = 2-1 = 1

 

细心的读者肯定能发现这个对象没有被销毁,何时销毁呢?谁去销毁它?(可以参考附件中的示例程序memman-with-pool.m)请看下一条。

 

6           autorelease pool原理剖析。(其实很简单的,一定要坚持看下去,否则还是不能理解Objective-C的内存管理机制。)

6.1          autorelease pool不是天生的,需要手动创立。只不过在新建一个iphone项目时,xcode会自动帮你写好。autorelease pool的真名是NSAutoreleasePool。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

6.2          NSAutoreleasePool内部包含一个数组(NSMutableArray),用来保存声明为autorelease的所有对象。如果一个对象声明为autorelease,系统所做的工作就是把这个对象加入到这个数组中去。

ClassA *obj1 = [[[ClassA alloc] init] autorelease]; //retain count = 1,把此对象加入autorelease pool中

6.3          NSAutoreleasePool自身在销毁的时候,会遍历一遍这个数组,release数组中的每个成员。如果此时数组中成员的retain count为1,那么release之后,retain count为0,对象正式被销毁。如果此时数组中成员的retain count大于1,那么release之后,retain count大于0,此对象依然没有被销毁,内存泄露。

6.4          默认只有一个autorelease pool,通常类似于下面这个例子。

int main (int argc, const char *argv[])

{

NSAutoreleasePool *pool;

pool = [[NSAutoreleasePool alloc] init];

 

// do something

 

[pool release];

return (0);

} // main

所有标记为autorelease的对象都只有在这个pool销毁时才被销毁。如果你有大量的对象标记为autorelease,这显然不能很好的利用内存,在iphone这种内存受限的程序中是很容易造成内存不足的。例如:

int main (int argc, const char *argv[])

{

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

int i, j;

for (i = 0; i < 100; i++ )

{

 for (j = 0; j < 100000; j++ )

    [NSString stringWithFormat:@"1234567890"];//产生的对象是autorelease的。

}

[pool release];

return (0);

} // main

(可以参考附件中的示例程序memman-many-objs-one-pool.m,运行时通过监控工具可以发现使用的内存在急剧增加,直到pool销毁时才被释放)你需要考虑下一条。

 

7           Objective-C程序中可以嵌套创建多个autorelease pool。在需要大量创建局部变量的时候,可以创建内嵌的autorelease pool来及时释放内存。(感谢网友hhyyttneogui的提醒,某些情况下,系统会自动创建autorelease pool, 请参见第四章)

int main (int argc, const char *argv[])

{

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

int i, j;

for (i = 0; i < 100; i++ )

{

 NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init];

 for (j = 0; j < 100000; j++ )

    [NSString stringWithFormat:@"1234567890"];//产生的对象是autorelease的。

 [loopPool release];

}

[pool release];

return (0);

} // main

 

二 口诀与范式

1           口诀。

1.1          谁创建,谁释放(类似于“谁污染,谁治理”)。如果你通过allocnewcopy来创建一个对象,那么你必须调用releaseautorelease。换句话说,不是你创建的,就不用你去释放。
例如,你在一个函数中alloc生成了一个对象,且这个对象只在这个函数中被使用,那么你必须在这个函数中调用releaseautorelease。如果你在一个class的某个方法中alloc一个成员对象,且没有调用autorelease,那么你需要在这个类的dealloc方法中调用release;如果调用了autorelease,那么在dealloc方法中什么都不需要做。

1.2          除了allocnewcopy之外的方法创建的对象都被声明了autorelease

1.3          retain,谁release。只要你调用了retain,无论这个对象是如何生成的,你都要调用release。有时候你的代码中明明没有retain,可是系统会在默认实现中加入retain。不知道为什么苹果公司的文档没有强调这个非常重要的一点,请参考范式2.7和第三章。

2           范式。
范式就是模板,就是依葫芦画瓢。由于不同人有不同的理解和习惯,我总结的范式不一定适合所有人,但我能保证照着这样做不会出问题。

2.1          创建一个对象。

ClassA *obj1 = [[ClassA alloc] init];

2.2          创建一个autorelease的对象。

ClassA *obj1 = [[[ClassA alloc] init] autorelease];

2.3          Release一个对象后,立即把指针清空。(顺便说一句,release一个空指针是合法的,但不会发生任何事情)

[obj1 release];

obj1 = nil;

2.4          指针赋值给另一个指针。

ClassA *obj2 = obj1;

[obj2 retain];

//do something

[obj2 release];

obj2 = nil;

2.5          在一个函数中创建并返回对象,需要把这个对象设置为autorelease

ClassA *Func1()

{

  ClassA *obj = [[[ClassA alloc]init]autorelease];

  return obj;

}

2.6          在子类的dealloc方法中调用基类的dealloc方法

-(void) dealloc

{

         …

         [super dealloc];

}

2.7          在一个class中创建和使用property

2.7.1     声明一个成员变量。

ClassB *objB;

2.7.2     声明property,加上retain参数。

@property (retain) ClassB* objB;

2.7.3     定义property。(property的默认实现请看第三章)

@synthesize objB;

2.7.4     除了dealloc方法以外,始终用.操作符的方式来调用property

self.objB 或者objA.objB

2.7.5     dealloc方法中release这个成员变量。

[objB release];

示例代码如下(详细代码请参考附件中的memman-property.m,你需要特别留意对象是在何时被销毁的。):

@interface ClassA : NSObject

{

         ClassB* objB;

}

 

@property (retain) ClassB* objB;

@end

 

@implementation ClassA

@synthesize objB;

-(void) dealloc

{

         [objB release];

         [super dealloc];

}

@end

2.7.6     给这个property赋值时,有手动releaseautorelease两种方式。

void funcNoAutorelease()

{

         ClassB *objB1 = [[ClassB alloc]init];

         ClassA *objA = [[ClassA alloc]init];

         objA.objB = objB1;

         [objB1 release];

         [objA release];

}

 

void funcAutorelease()

{

         ClassB *objB1 = [[[ClassB alloc]init] autorelease];

         ClassA *objA = [[[ClassA alloc]init] autorelease];

         objA.objB = objB1;

}

 

三 @property (retain)@synthesize的默认实现

在这里解释一下@property (retain) ClassB* objB;@synthesize objB;背后到底发生了什么(retain property的默认实现)property实际上是gettersetter,针对有retain参数的property,背后的实现如下(请参考附件中的memman-getter-setter.m,你会发现,结果和memman-property.m一样):

@interface ClassA : NSObject

{

         ClassB *objB;

}

-(ClassB *) getObjB;

-(void) setObjB:(ClassB *) value;

@end

 

@implementation ClassA

-(ClassB*) getObjB

{

         return objB;

}

 

-(void) setObjB:(ClassB*) value

{

         if (objB != value)

         {

                   [objB release];

                   objB = [value retain];

         }

}

 

setObjB中,如果新设定的值和原值不同的话,必须要把原值对象release一次,这样才能保证retain count是正确的。

由于我们在class内部retain了一次(虽然是默认实现的),所以我们要在dealloc方法中release这个成员变量。

-(void) dealloc

{

         [objB release];

         [super dealloc];

}

 

四 系统自动创建新的autorelease pool

在生成新的Run Loop的时候,系统会自动创建新的autorelease pool(非常感谢网友hhyyttneogui的提醒)。注意,此处不同于xcode在新建项目时自动生成的代码中加入的autorelease poolxcode生成的代码可以被删除,但系统自动创建的新的autorelease pool是无法删除的(对于无Garbage Collection的环境来说)。Objective-C没有给出实现代码,官方文档也没有说明,但我们可以通过小程序来证明。

在这个小程序中,我们先生成了一个autorelease pool,然后生成一个autoreleaseClassA的实例,再在一个新的run loop中生成一个autoreleaseClassB的对象(注意,我们并没有手动在新run loop中生成autorelease pool)。精简的示例代码如下,详细代码请见附件中的memman-run-loop-with-pool.m

int main(int argc, char**argv) 

{

         NSLog(@"create an autorelasePool\n");

         NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];   

 

         NSLog(@"create an instance of ClassA and autorelease\n");

         ClassA *obj1 = [[[ClassA alloc] init] autorelease];

         NSDate *now = [[NSDate alloc] init];

         NSTimer *timer = [[NSTimer alloc] initWithFireDate:now

                   interval:0.0

                   target:obj1

                   selector:@selector(createClassB)

                   userInfo:nil

                   repeats:NO];

         NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

         [runLoop addTimer:timer forMode:NSDefaultRunLoopMode];

         [timer release];

         [now release];

         [runLoop run]; //在新loop中调用一函数,生成ClassBautorelease实例

 

         NSLog(@"releasing autorelasePool\n");

         [pool release];

         NSLog(@"autorelasePool is released\n");

         return 0;

输出如下:

create an autorelasePool

create an instance of ClassA and autorelease

create an instance of ClassB and autorelease

ClassB destroyed

releasing autorelasePool

ClassA destroyed

autorelasePool is released

注意在我们销毁autorelease pool之前,ClassBautorelease实例就已经被销毁了。

有人可能会说,这并不能说明新的run loop自动生成了一个新的autorelease pool,说不定还只是用了老的autorelease pool,只不过后来drain了一次而已。我们可以在main函数中不生成autorelease pool。精简的示例代码如下,详细代码请见附件中的memman-run-loop-without-pool.m

int main(int argc, char**argv) 

{

         NSLog(@"No autorelasePool created\n");

 

         NSLog(@"create an instance of ClassA\n");

         ClassA *obj1 = [[ClassA alloc] init];

         NSDate *now = [[NSDate alloc] init];

         NSTimer *timer = [[NSTimer alloc] initWithFireDate:now

                   interval:0.0

                   target:obj1

                   selector:@selector(createClassB)

                   userInfo:nil

                   repeats:NO];

         NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

         [runLoop addTimer:timer forMode:NSDefaultRunLoopMode];

         [timer release];

         [now release];

         [runLoop run]; //在新loop中调用一函数,生成ClassBautorelease实例

         NSLog(@"Manually release the instance of ClassA\n");

         [obj1 release];

 

         return 0;

输出如下:

No autorelasePool created

create an instance of ClassA

create an instance of ClassB and autorelease

ClassB destroyed

Manually release the instance of ClassA

ClassA destroyed

我们可以看出来,我们并没有创建任何autorelease pool,可是ClassB的实例依然被自动销毁了,这说明新的run loop自动创建了一个autorelease pool,这个pool在新的run loop结束的时候会销毁自己(并自动release所包含的对象)。

 

 

补充说明

在研究retain count的时候,我不建议用NSString。因为在下面的语句中,

NSString *str1 = @”constant string”;

str1retain count是个很大的数字。Objective-C对常量字符串做了特殊处理。

当然,如果你这样创建NSString,得到的retain count依然为1

NSString *str2 = [NSString stringWithFormat:@”123”];

涉及的示例程序代码(已去除隐藏,觉得有用的话请顶一下此文):http://files.cnblogs.com/VinceYuan/objective-c-memman.zip

什么是ARC

Automatic Reference Counting,自动引用计数,即ARC,可以说是WWDC2011和iOS5所引入的最大的变革和最激动人心的变化。ARC是新的LLVM 3.0编译器的一项特性,使用ARC,可以说一举解决了广大iOS开发者所憎恨的手动内存管理的麻烦。

在工程中使用ARC非常简单:只需要像往常那样编写代码,只不过永远不写retain,release和autorelease三个关键字就好~这是ARC的基本原则。当ARC开启时,编译器将自动在代码合适的地方插入retain, release和autorelease,而作为开发者,完全不需要担心编译器会做错(除非开发者自己错用ARC了)。好了,ARC相当简单吧~到此为止,本教程结束。

等等…也许还有其他问题,最严重的问题是“我怎么确定让ARC来管理不会出问题?”或者“用ARC会让程序性能下降吧”。对于ARC不能正处理内存管理的质疑自从ARC出生以来就一直存在,而现在越来越多的代码转向ARC并取得了很好的效果,这证明了ARC是一套有效的简化开发复杂程度的机制,另外通过研究ARC的原理,可以知道使用ARC甚至能提高程序的效率。在接下来将详细解释ARC的运行机理并且提供了一个step-by-step的教程,将非ARC的程序转换为ARC。



ARC工作原理

手动内存管理的机理大家应该已经非常清楚了,简单来说,只要遵循以下三点就可以在手动内存管理中避免绝大部分的麻烦:

如果需要持有一个对象,那么对其发送retain 如果之后不再使用该对象,那么需要对其发送release(或者autorealse) 每一次对retain,alloc或者new的调用,需要对应一次release或autorealse调用

初学者可能仅仅只是知道这些规则,但是在实际使用时难免犯错。但是当开发者经常使用手动引用计数 Manual Referecen Counting(MRC)的话,这些规则将逐渐变为本能。你会发现少一个release的代码怎么看怎么别扭,从而减少或者杜绝内存管理的错误。可以说MRC的规则非常简单,但是同时也非常容易出错。往往很小的错误就将引起crash或者OOM之类的严重问题。

在MRC的年代里,为了避免不小心忘写release,Xcode提供了一个很实用的小工具来帮助可能存在的代码问题(Xcode3里默认快捷键Shift+A?不记得了),可以指出潜在的内存泄露或者过多释放。而ARC在此基础上更进一步:ARC是Objective-C编译器的特性,而不是运行时特性或者垃圾回收机制,ARC所做的只不过是在代码编译时为你自动在合适的位置插入release或autorelease,就如同之前MRC时你所做的那样。因此,至少在效率上ARC机制是不会比MRC弱的,而因为可以在最合适的地方完成引用计数的维护,以及部分优化,使用ARC甚至能比MRC取得更高的运行效率。

ARC机制

学习ARC很简单,在MRC时代你需要自己retain一个想要保持的对象,而现在不需要了。现在唯一要做的是用一个指针指向这个对象,只要指针没有被置空,对象就会一直保持在堆上。当将指针指向新值时,原来的对象会被release一次。这对实例变量,sunthesize的变量或者局部变量都是适用的。比如


1
NSString*firstName=self.textField.text;

firstName现在指向NSString对象,这时这个对象(textField的内容字符串)将被hold住。比如用字符串@“OneV”作为例子,这个时候firstName持有了@”OneV”。

当然,一个对象可以拥有不止一个的持有者(这个类似MRC中的retainCount>1的情况)。在这个例子中显然self.textField.text也是@“OneV”,那么现在有两个指针指向对象@”OneV”(被持有两次,retainCount=2,其实对NSString对象说retainCount是有问题的,不过anyway~就这个意思而已.)。

过了一会儿,也许用户在textField里输入了其他的东西,那么self.textField.text指针显然现在指向了别的字符串,比如@“onevcat”,但是这时候原来的对象已然是存在的,因为还有一个指针firstName持有它。现在指针的指向关系是这样的:

只有当firstName也被设定了新的值,或者是超出了作用范围的空间(比如它是局部变量但是这个方法执行完了或者它是实例变量但是这个实例被销毁了),那么此时firstName也不再持有@“OneV”,此时不再有指针指向@”OneV”,在ARC下这种状况发生后对象@”OneV”即被销毁,内存释放。

类似于firstName和self.textField.text这样的指针使用关键字”strong”进行标志,它意味着只要该指针指向某个对象,那么这个对象就不会被销毁。反过来说,ARC的一个基本规则即使,只要某个对象被任一strong指针指向,那么它将不会被销毁。如果对象没有被任何strong指针指向,那么就将被销毁。在默认情况下,所有的实例变量和局部变量都是strong类型的。可以说strong类型的指针在行为上和MRC时代retain的property是比较相似的。

既然有”strong”,那肯定有”weak”咯~weak类型的指针也可以指向对象,但是并不会持有该对象。比如:


1
__weakNSString*weakName=self.textField.text

得到的指向关系是:

这里声明了一个weak的指针weakName,它并不持有@“onevcat”。如果self.textField.text的内容发生改变的话,根据之前提到的“只要某个对象被任一strong指针指向,那么它将不会被销毁。如果对象没有被任何strong指针指向,那么就将被销毁”原则,此时指向@“onevcat”的指针中没有strong类型的指针,@”onevcat”将被销毁。同时,在ARC机制作用下,所有指向这个对象的weak指针将被置为nil。这个特性相当有用,相信无数的开发者都曾经被指针指向已释放对象所造成的EXC_BAD_ACCESS困扰过,使用ARC以后,不论是strong还是weak类型的指针,都不再会指向一个dealloced的对象,从根源上解决了意外释放导致的crash。

不过在大部分情况下,weak类型的指针可能并不会很常用。比较常见的用法是在两个对象间存在包含关系时:对象1有一个strong指针指向对象2,并持有它,而对象2中只有一个weak指针指回对象1,从而避免了循环持有。一个常见的例子就是oc中常见的delegate设计模式,viewController中有一个strong指针指向它所负责管理的UITableView,而UITableView中的dataSource和delegate指针都是指向viewController的weak指针。可以说,weak指针的行为和MRC时代的assign有一些相似点,但是考虑到weak指针更聪明些(会自动指向nil),因此还是有所不同的。细节的东西我们稍后再说。

注意类似下面的代码似乎是没有什么意义的:


1
2
__weakNSString*str=[[NSStringalloc]initWithFormat:…];
NSLog(@"%@",str);//输出是"(null)"

由于str是weak,它不会持有alloc出来的NSString对象,因此这个对象由于没有有效的strong指针指向,所以在生成的同时就被销毁了。如果我们在Xcode中写了上面的代码,我们应该会得到一个警告,因为无论何时这种情况似乎都是不太可能出现的。你可以把weak换成strong来消除警告,或者直接前面什么都不写,因为ARC中默认的指针类型就是strong。

property也可以用strong或weak来标记,简单地把原来写retain和assign的地方替换成strong或者weak就可以了。


1
2
@property(nonatomic,strong)NSString *firstName;
@property (nonatomic,weak)id  delegate;

ARC可以为开发者节省很多代码,使用ARC以后再也不需要关心什么时候retain,什么时候release,但是这并不意味你可以不思考内存管理,你可能需要经常性地问自己这个问题:谁持有这个对象?

比如下面的代码,假设array是一个NSMutableArray并且里面至少有一个对象:


1
2
3
idobj=[arrayobjectAtIndex:0]; 
[array removeObjectAtIndex:0]; 
NSLog(@"%@",obj);

在MRC时代这几行代码应该就挂掉了,因为array中0号对象被remove以后就被立即销毁了,因此obj指向了一个dealloced的对象,因此在NSLog的时候将出现EXC_BAD_ACCESS。而在ARC中由于obj是strong的,因此它持有了array中的首个对象,array不再是该对象的唯一持有者。即使我们从array中将obj移除了,它也依然被别的指针持有,因此不会被销毁。

一点提醒

ARC也有一些缺点,对于初学者来说,可能仅只能将ARC用在objective-c对象上(也即继承自NSObject的对象),但是如果涉及到较为底层的东西,比如Core Foundation中的malloc()或者free()等,ARC就鞭长莫及了,这时候还是需要自己手动进行内存管理。在之后我们会看到一些这方面的例子。另外为了确保ARC能正确的工作,有些语法规则也会因为ARC而变得稍微严格一些。

ARC确实可以在适当的地方为代码添加retain或者release,但是这并不意味着你可以完全忘记内存管理,因为你必须在合适的地方把strong指针手动设置到nil,否则app很可能会oom。简单说还是那句话,你必须时刻清醒谁持有了哪些对象,而这些持有者在什么时候应该变为指向nil。

ARC必然是Objective-C以及Apple开发的趋势,今后也会有越来越多的项目采用ARC(甚至不排除MRC在未来某个版本被弃用的可能),Apple也一直鼓励开发者开始使用ARC,因为它确实可以简化代码并增强其稳定性。可以这么说,使用ARC之后,由于内存问题造成的crash基本就是过去式了(OOM除外 )

我们正处于由MRC向ARC转变的节点上,因此可能有时候我们需要在ARC和MRC的代码间来回切换和适配。Apple也想到了这一点,因此为开发这提供了一些ARC和非ARC代码混编的机制,这些也将在之后的例子中列出。另外ARC甚至可以用在C++的代码中,而通过遵守一些代码规则,iOS 4里也可以使用ARC(虽然我个人认为在现在iOS 6都呼之欲出的年代已经基本没有需要为iOS 4做适配的必要了)、

总之,聪明的开发者总会尝试尽可能的自动化流程,已减轻自己的工作负担,而ARC恰恰就为我们提供了这样的好处:自动帮我们完成了很多以前需要手动完成的工作,因此对我来说,转向ARC是一件不需要考虑的事情。


具体操作

说了这么多,终于可以实践一下了。在决定使用ARC后,很多开发者面临的首要问题是不知如何下手。因为可能手上的项目已经用MRC写了一部分,不想麻烦做转变;或者因为新项目里用ARC时遇到了奇怪的问题,从而放弃ARC退回MRC。这都是常见的问题,而在下面,将通过一个demo引导大家彻底转向ARC的世界。

Demo

例子很简单,这是一个查找歌手的应用,包含一个简单的UITableView和一个搜索框,当用户在搜索框搜索时,调用MusicBrainz的API完成名字搜索和匹配。MusicBrainz是一个开放的音乐信息平台,它提供了一个免费的XML网页服务,如果对MusicBrainz比较有兴趣的话,可以到它的官网逛一逛。

Demo的起始例子可以从这里下载,为了照顾新人,在这边进行简单说明。在Xcode中打开下载的例子,应该可以看到如下内容(Xcode和iOS开发熟练者请跳过此段)

AppDelegate.h/m 这是整个app的delegate,没什么特殊的,每个iOS/Mac程序在main函数以后的入口,由此进入app的生命周期。在这里加载了最初的viewController并将其放到Window中展示出来。另外appDelegate还负责处理程序开始退出等系统委托的事件

MainViewController.h/m/xib 这个demo最主要的ViewController,含有一个TableView和一个搜索条。 SoundEffect.h/m 简单的播放声音的类,在MusicBrainz搜索完毕时播放一个音效。 main.m 程序入口,所有c程序都从main函数开始执行

AFHTTPRequestOperation.h/m 这是有名的网络框架AFNetworking的一部分,用来帮助等简单地处理web服务请求。这里只包含了这一个类而没有将全部的AFNetworking包括进来,因为我们只用了这一个类。完整的框架代码可以在github的相关页面上找到https://github.com/gowalla/AFNetworking

SVProgresHUD.h/m/bundle 是一个常用的进度条指示,当搜索的时候出现以提示用户正在搜索请稍后。bundle是资源包,里面包含了几张该类用到的图片,打进bundle包的目的一方面是为了资源容易管理,另一方面也是主要方面时为了不和其他资源发生冲突(Xcode中资源名字是资源的唯一标识,同名字的资源只能出现一次,而放到bundle包里可以避免这个潜在的问题)。SVProgresHUD可以在这里找到https://github.com/samvermette/SVProgressHUD

快速过一遍这个应用吧:MainViewController是UIViewController的子类,对应的xib文件定义了对应的UITableView和UISearchBar。TableView中显示searchResult数组中的内容。当用户搜索时,用AFHTTPRequestOperation发一个HTTP请求,当从MusicBrainz得到回应后将结果放入searchResult数组中并用tableView显示,当返回结果是空时在tableView中显示没找到。主要的逻辑都在MainViewController.m中的-searchBarSearchButtonClicked:方法中,生成了用于查询的URL,根据MusicBrainz的需求替换了请求的header,并且完成了返回逻辑,然后在主线程中刷新UI。整个程序还是比较简单的~

MRC到ARC的自动转换

回到正题,我们讨论的是ARC,关于REST API和XML解析的技术细节就暂时先忽略吧..整个程序都是用MRC来进行内存管理的,首先来让我们把这个demo转成ARC吧。基本上转换为ARC意味着把所有的retain,release和autorelease关键字去掉,在之前我们明确几件事情:
* Xcode提供了一个ARC自动转换工具,可以帮助你将源码转为ARC
* 当然你也可以自己动手完成ARC转换
* 同时你也可以指定对于某些你不想转换的代码禁用ARC,这对于很多庞大复杂的还没有转至ARC的第三方库帮助很大,因为不是你写的代码你想动手修改的话代码超级容易mess…

对于我们的demo,为了说明问题,这三种策略我们都将采用,注意这仅仅只是为了展示如何转换。实际操作中不需要这么麻烦,而且今后的绝大部分情况应该是从工程建立开始就是ARC的。

首先,ARC是LLVM3.0编译器的特性,而老的工程特别是Xcode3时代的工程的默认编译器很可能是GCC或者LLVM-GCC,因此第一步就是确认编译器是否正确。在Project设置面板,选择target,在Build Settings中将Compiler for C/C++/Objective-C选为Apple LLVM compiler 3.0或以上。为了确保之后转换的顺利,在这里我个人建议最好把Treat Warnings as Errors和 Run Static Analyzer都打开,确保在改变编译器后代码依旧没有警告或者内存问题(虽然静态分析可能不太能保证这一点,但是聊胜于无)。好了~clean(Shift+Cmd+K)以后Bulid一下试试看,经过修改后的demo工程没有任何警告和错误,这是很好的开始。(对于存在警告的代码,这里是很好的修复的时机..请在转换前确保原来的代码没有内存问题)。

接下来就是完成从MRC到ARC的伟大转换了。还是在Build Settings页面,把Objective-C Automatic Reference Counting改成YES(如果找不到的话请看一看搜索栏前面的小标签是不是调成All了..这个选项在Basic里是不出现的),这样我们的工程就将在所有源代码中启用ARC了。然后…试着编译一下看看,嗯..无数的错误。

这是很正常的,因为ARC里不允许出现retain,release之类的,而MRC的代码这些是肯定会有的东西。我们可以手动一个一个对应地去修复这些错误,但是这很麻烦。Xcode为我们提供了一个自动转换工具,可以帮助重写源代码,简单来说就是去掉多余的语句并且重写一些property关键字。

这个小工具是Edit->Refactor下的Convert to Objective-C ARC,点击后会让我们选择要转换哪几个文件,在这里为了说明除了自动转换外的方法,我们不全部转换,而只是选取其中几个转换(MainViewController.m和AFHTTPRequestOperation.m不做转换)。注意到这个对话框上有个警告标志告诉我们target已经是ARC了,这是由于之前我们在Build Settings里已经设置了启用ARC,其实直接在这里做转换后Xcode会自动帮我们开启ARC。点击检查后,Xcode告诉我们一个不幸的消息,不能转换,需要修复ARC readiness issues..后面还告诉我们要看到所有的所谓的ARC readiness issues,可以到设置的General里把Continue building after errors勾上…What the f**k…好吧~先乖乖听从Xcode的建议”Cmd+,“然后Continue building after errors打勾然后再build。

问题依旧,不过在issue面板里应该可以看到所有出问题的代码了。在我们的例子里,问题出在SoundEffect.m里:


1
2
3
4
5
6
7
8
NSURL*fileURL=[[NSBundlemainBundle]URLForResource:filenamewithExtension:nil];
if (fileURL!=nil)
{
    SystemSoundID theSoundID;
    OSStatuserror=AudioServicesCreateSystemSoundID((CFURLRef)fileURL,&theSoundID);
    if (error==kAudioServicesNoError)
        soundID=theSoundID;
}

这里代码尝试把一个NSURL指针强制转换为一个CFURLRef指针。这里涉及到一些Core Services特别是Core Foundation(CF)的东西,AudioServicesCreateSystemSoundID()函数接受CFURLRef为参数,这是一个CF的概念,但是我们在较高的抽象层级上所建立的是NSURL对象。在Cocoa框架中,有很多顶层对象对底层的抽象,而在使用中我们往往可以不加区别地对这两种对象进行同样的对待,这类对象即为可以”自由桥接”的对象(toll-free bridged)。NSURL和CFURLRef就是一对好基友好例子,在这里其实CFURLRef和NSURL是可以进行替换的。

通常来说为了代码在底层级上的正确,在iOS开发中对基于C的API的调用所传入的参数一般都是CF对象,而Objective-C的API调用都是传入NSObject对象。因此在采用自由桥接来调用C API的时候就需要进行转换。但是在使用ARC编译的时候,因为内存管理的原因,编译器需要知道对这些桥接对象要实行什么样的操作。如果一个NSURL对象替代了CFURLRef,那么在作用区域外,应该由谁来决定内存释放和对象销毁呢?为了解决这个问题,引入了bridge,bridge_transfer和__bridge_retained三个关键字。关于选取哪个关键字做转换,需要由实际的代码行为来决定。如果对于自由桥接机制感兴趣,大家可以自己找找的相关内容,比如适用类型、内部机制和一个简介~之后我也会对这个问题做进一步说明

回到demo,我们现在在上面的代码中加上__bridge进行转换。然后再运行ARC转换工具,这时候检查应该没有其他问题了,那么让我们进行转换吧~当然在真正转换之前会有一个预览界面,在这里我们最好检查一下转换是不是都按照预想进行了..要是出现大面积错误又没有备份或者出现各种意外的话就可以哭了…

前后变化的话比较简单,基本就是去掉不需要的代码和改变property的类型而已,其实有信心的话不太需要每次都看,但是如果是第一次执行ARC转换的操作的话,我还是建议稍微看一下变化,这样能对ARC有个直观上的了解。检查一遍,应该没什么问题了..需要注意的是main.m里关于autoreleasepool的变化以及所有dealloc调用里的[super dealloc]的删除,它们同样是MRC到ARC的主要变化..

好了~转换完成以后我们再build看看..应该会有一些警告。对于原来retain的property,比较保险的做法是转为strong,在LLVM3.0中自动转换是这样做的,但是在3.1中property默认并不是strong,这样在使用property赋值时存在警告,我们在property声明里加上strong就好了~然后就是SVProgressHUD.m里可能存在问题,这是由于原作者把release的代码和其他代码写在一行了.导致自动转换时只删掉了部分,而留下了部分不应该存在的代码,删掉对变量的空调用就好了..

自动转换之后的故事

然后再编译,没有任何错误和警告了,好棒~等等…我们刚才没有对MainViewController和AFHTTPRequestOperation进行处理吧,那么这两个文件里应该还存在release之类的东西吧..?看一看这两个文件,果然有各种release,但是为什么能编译通过呢?!明明刚才在自动转换前他们还有N多错的嘛…答案很简单,在自动转换的时候因为我们没有勾选这两个文件,因此编译器在自动转换过后为这两个文件标记了”不使用ARC编译”。可以看到在target的Building Phases下,MainViewController.m和AFHTTPRequestOperation.m两个文件后面被加上了-fno-objc-arc的编译标记,被加上该标记的文件将不使用ARC规则进行编译。

提供这样的编译标记的原因是显而易见的,因为总是有一部分的第三方代码并没有转换为ARC(可能是由于维护者犯懒或者已经终止维护),所以对于这部分代码,为了迅速完成转换,最好是使用-fno-objc-arc标记来禁止在这些源码上使用ARC。

为了方便查找,再此列出一些在转换时可能出现的问题,当然在我们使用ARC时也需要注意避免代码中出现这些问题:

  • “Cast … requires a bridged cast”

这是我们在demo中遇到的问题,不再赘述

  • Receiver type ‘X’ for instance message is a forward declaration

这往往是引用的问题。ARC要求完整的前向引用,也就是说在MRC时代可能只需要在.h中申明@class就可以,但是在ARC中如果调用某个子类中未覆盖的父类中的方法的话,必须对父类.h引用,否则无法编译。

  • Switch case is in protected scope

现在switch语句必须加上{}了,ARC需要知道局部变量的作用域,加上{}后switch语法更加严格,否则遇到没有break的分支的话内存管理会出现问题。

  • A name is referenced outside the NSAutoreleasePool scope that it was declared in

这是由于写了自己的autoreleasepool,而在转换时在原来的pool中申明的变量在新的@autoreleasepool中作用域将被局限。解决方法是把变量申明拿到pool的申请之前。

  • ARC forbids Objective-C objects in structs or unions

可以说ARC所引入的最严格的限制是不能在C结构体中放OC对象了..因此类似下面这样的代码是不可用的


1
2
3
4
typedefstruct{
    UIImage *selectedImage;
    UIImage*disabledImage;
} ButtonImages;

这个问题只有乖乖想办法了..改变原来的结构什么的..

手动转换

刚才做了对demo的大部分转换,还剩下了MainViewController和AFHTTPRequestOperation是MRC。但是由于使用了-fno-objc-arc,因此现在编译和运行都没有问题了。下面我们看看如何手动把MainViewController转为ARC,这也有助于进一步理解ARC的规则。

首先,我们需要转变一下观念…对于MainViewController.h,在.h中申明了两个实例变量:


1
2
3
4
5
@interfaceMainViewController:UIViewController  
{
    NSOperationQueue*queue;
    NSMutableString*currentStringValue;
}

我们不妨仔细考虑一下,为什么在interface里出现了实例变量的申明?通常来说,实例变量只是在类的实例中被使用,而你所写的类的使用者并没有太多必要了解你的类中有哪些实例变量。而对于绝大部分的实例变量,应该都是protected或者private的,对它们的操作只应该用setter和getter,而这正是property所要做的工作。可以说,将实例变量写在头文件中是一种遗留的陋习。更好的写实例变量名字的地方应当与类实现关系更为密切,为了隐藏细节,我们应该考虑将它们写在@implementation里。好消息是,在LLVM3.0中,不论是否开启ARC,编译器是支持将实例变量写到实现文件中的。甚至如果没有特殊需要又用了property,我们都不应该写无意义的实例变量申明,因为在@synthesize中进行绑定时,我们就可以设置变量名字了,这样写的话可以让代码更加简洁。

在这里我们对着两个实例变量不需要property(外部成员不应当能访问到它们),因此我们把申明移到.m里中。修改后的.h是这样的,十分简洁一看就懂~


1
2
3
4
5
#import
@interface MainViewController:UIViewController
@property(nonatomic,retain)IBOutlet UITableView *tableView;  
@property (nonatomic,retain)IBOutlet UISearchBar *searchBar; 
@end

然后.m的开头变成这样:


1
2
3
4
5
@implementationMainViewController
{
    NSOperationQueue*queue; 
    NSMutableString*currentStringValue; 
}

这样的写法让代码相当灵活,而且不得不承认.m确实是这些实例变量的应该在的地方…build一下,没问题..当然对于SoundEffect类也可以做相似的操作,这会让使用你的类的人很开心,因为.h越简单越好..P.S.另外一个好处可以减少.h里的引用,减少编译时间(虽然不明显=。=)

然后就可以在MainViewController里启用ARC了,方法很简单,删掉Build Phases里相关文件的-fno-objc-arc标记就可以了~然后..然后当然是一大堆错误啦。我们来手动一个个改吧,虽然谈不上乐趣,但是成功以后也会很有成就~(如果你不幸在启用ARC后build还是成功了,恭喜你遇到了Xcode的bug,请Cmd+Q然后重新打开Xcode把=_=)

dealloc

红色最密集的地方是dealloc,因为每一行都是release。由于在这里dealloc并没有做除了release和super dealloc之外的任何事情,因此简单地把整个方法删掉就好了。当然,在对象被销毁时,dealloc还是会被调用的,因此我们在需要对非ARC管理的内存进行管理和必要的逻辑操作的时候,还是应该保留dealloc的,当然这涉及到CF以及以下层的东西:比如对于retain的CF对象要CFRelease(),对于malloc()到堆上的东西要free()掉,对于添加的observer可以在这里remove,schedule的timer在这里invalidate等等~[super dealloc]这个消息也不再需要发了,ARC会自动帮你搞定。

另外,在MRC时代一个常做的事情是在dealloc里把指向自己的delegate设成nil(否则就等着EXC_BAD_ACCESS吧 ),而现在一般delegate都是weak的,因此在self被销毁后这个指针自动被置成nil了,你不用再为之担心,好棒啊..

去掉各种release和autorelease

这个很直接,没有任何问题。去掉就行了~不再多说

讨论一下Property

在MainViewController.m里的类扩展中定义了两个property:


1
2
3
4
@interfaceMainViewController()
@property (nonatomic,retain)NSMutableArray *searchResults;
@property(nonatomic,retain)SoundEffect *soundEffect; 
@end

申明的类型是retain,关于retain,assign和copy的讨论已经烂大街了,在此不再讨论。在MRC的年代使用property可以帮助我们使用dot notation的时候简化对象的retain和copy,而在ARC时代,这就显得比较多余了。在我看来,使用property和点方法来调用setter和getter是不必要的。property只在将需要的数据在.h中暴露给其他类时才需要,而在本类中,只需要用实例变量就可以。因此我们可以移去searchResults和soundEffect的@property和@synthesize,并将起移到实例变量申明中:


1
2
3
4
5
6
7
@implementationMainViewController
    NSOperationQueue*queue; 
    NSMutableString*currentStringValue;
    NSMutableArray*searchResults;
    SoundEffect *soundEffect; 
}

相应地,我们需要将对应的self.searchResult和self.soundEffect的self.都去去掉。在这里需要注意的是,虽然我们去掉了soundEffect的property和synthesize,但是我们依然有一个lazy loading的方法- (SoundEffect *)soundEffect,神奇之处在于(可能你以前也不知道),点方法并不需要@property关键字的支持,虽然大部分时间是这么用的..(property只是对setter或者getter的申明,而点方法是对其的调用,在这个例子的实现中我们事实上实现了-soundEffect这个getter方法,所以点方法在等号右边的getter调用是没有问题的)。为了避免误解,建议把self.soundEffect的getter调用改写成[self soundEffect]。

然后我们看看.h里的property~里面有两个retain的IBOutlet。retain关键字在ARC中是依旧可用的,它在ARC中所扮演的角色和strong完全一样。为了避免迷惑,最好在需要的时候将其写为strong,那样更符合ARC的规则。对于这两个property,我们将其申明为weak(事实上,如果没有特别意外,除了最顶层的IBOutlet意外,自己写的outlet都应该是weak)。通过加载xib得到的用户界面,在其从xib文件加载时,就已经是view hierarchy的一部分了,而view hierarchy中的指向都是strong的。因此outlet所指向的UI对象不应当再被hold一次了。将这些outlet写为weak的最显而易见的好处是你就不用再viewDidUnload方法中再将这些outlet设为nil了(否则就算view被摧毁了,但是由于这些UI对象还在被outlet指针指向而无法释放,代码简洁了很多啊..)。

在我们的demo中将IBOutlet的property改为weak并且删掉viewDidUnload中关于这两个IBOutlet的内容~

总结一下新加入的property的关键字类型:

  • strong 和原来的retain比较相似,strong的property将对应__strong的指针,它将持有所指向的对象
  • weak 不持有所指向的对象,而且当所指对象销毁时能将自己置为nil,基本所有的outlet都应该用weak
  • unsafe_unretained 这就是原来的assign。当需要支持iOS4时需要用到这个关键字
  • copy 和原来基本一样..copy一个对象并且为其创建一个strong指针
  • assign 对于对象来说应该永远不用assign了,实在需要的话应该用unsafe_unretained代替(基本找不到这种时候,大部分assign应该都被weak替代)。但是对于基本类型比如int,float,BOOL这样的东西,还是要用assign。

特别地,对于NSString对象,在MRC时代很多人喜欢用copy,而ARC时代一般喜欢用strong…(我也不懂为什么..求指教)

自由桥接的细节

MainViewController现在剩下的问题都是桥接转换问题了~有关桥接的部分有三处:

  • (NSString *)CFURLCreateStringByAddingPercentEscapes(…):CFStringRef至NSString *
  • (CFStringRef)text:NSString *至CFStringRef
  • (CFStringRef)@“!‘();:@&=+$,/?%#[]“:NSString 至CFStringRef

编译器对前两个进行了报错,最后一个是常量转换不涉及内存管理。

关于toll-free bridged,如果不进行细究,NSString和CFStringRef是一样的东西,新建一个CFStringRef可以这么做:


1
CFStringRefs1=[[NSStringalloc]initWithFormat:@"Hello, %@!",name];

然后,这里alloc了而s1是一个CF指针,要释放的话,需要这样:


1
CFRelease(s1);

相似地可以用CFStringRef来转成一个NSString对象(MRC):


1
2
3
4
5
CFStringRefs2=CFStringCreateWithCString(kCFAllocatorDefault,bytes,kCFStringEncodingMacRoman); 
NSString *s3=(NSString*)s2;

// release the object when you're done
[s3release];

在ARC中,编译器需要知道这些指针应该由谁来负责释放,如果把一个NSObject看做是CF对象的话,那么ARC就不再负责它的释放工作(记住ARC是only for NSObject的)。对于不需要改变持有者的对象,直接用简单的bridge就可以了,比如之前在SoundEffect.m做的转换。在这里对于(CFStringRef)text这个转换,ARC已经负责了text这个NSObject的内存管理,因此这里我们需要一个简单的bridge。而对于CFURLCreateStringByAddingPercentEscapes方法,方法中的create暗示了这个方法将形成一个新的对象,如果我们不需要NSString转换,那么为了避免内存的问题,我们需要使用CFRelease来释放它。而这里我们需要一个NSString,因此我们需要告诉编译器接手它的内存管理工作。这里我们使用bridge_transfer关键字,将内存管理权由CF object移交给NSObject(或者说ARC)。如果这里我们只用bridge的话,内存管理的负责人没有改变,那么这里就会出现一个内存泄露。另外有时候会看到CFBridgingRelease(),这其实就是transfer cast的内联写法..是一样的东西。总之,需要记住的原则是,当在涉及CF层的东西时,如果函数名中有含有Create, Copy, 或者Retain之一,就表示返回的对象的retainCount+1了,对于这样的对象,最安全的做法是将其放在CFBridgingRelease()里,来平衡retain和release。

还有一种bridge方式,__bridge_retained。顾名思义,这种转换将在转换时将retainCount加1。和CFBridgingRelease()相似,也有一个内联方法CFBridgingRetain()来负责和CFRelease()进行平衡。

需要注意的是,并非所有的CF对象都是自由桥接的,比如Core Graphics中的所有对象都不是自由桥接的(如CGImage和UIImage,CGColor和UIColor)。另外也不是只有自由桥接对象才能用bridge来桥接,一个很好的特例是void(指向任意对象的指针,类似id),对于void和任意对象的转换,一般使用_bridge。(这在将ARC运用在Cocos2D中很有用)

终于搞定了

至此整个工程都ARC了~对于AFHTTPRequestOperation这样的不支持ARC的第三方代码,我们的选择一般都是就不使用ARC了(或者等开源社区的大大们更新ARC适配版本)。可以预见,在近期会有越来越多的代码转向ARC,但是也一定会有大量的代码暂时或者永远保持MRC等个,所以对于这些代码就不用太纠结了~


写在最后

写了那么多,希望你现在能对ARC有个比较全面的了解和认识了。ARC肯定是以后的趋势,也确实能让代码量大大降低,减少了很多无意义的重复工作,还提高了app的稳定性。但是凡事还是纸上得来终觉浅,希望作为开发者的你,在下一个工程中去尝试用用ARC~相信你会和我一样,马上爱上这种make life easier的方式的~

参考文档:来自apple开发者官网的<Advanced Memory Management Programming Guide>


内存错误,没有报错

[p2 release];//释放

    [p1 setName:@"林志林"];//如果多个指针指向一个区域,某一个指针release,释放后再赋值仍能打出,但一旦该区域内存被占用,程序就会崩溃,避免这种情况发生;如果指针用了retain就没关系;

    NSLog(@"%@",p1.name);


@autoreleasepool {//写最上面

  Person *liuyan=[[Person alloc]initWithName:@"柳岩" age:19];

/*    执行顺序1.[person alloc]开辟的内存大小取决于类里所有变量的大小

 arcno    arc系统自动管理

 程序 crash原因

 1内存溢出,超了

 2.野指针  僵尸指针,活死人指针;

 面试题 什么时候会收到内存警告,遇到后如何处理;

 数组越界必崩声明没实现也会崩,内存问题崩;

 内存管理方式

 1垃圾回收

 2MRC(Manual Reference)人工引用技术

 3ARC(Auto Reference)自动引用技术

 方法+alloc开辟空间 0,0-1,

 方法-retain+1

 copy+1

 release-1

 autorelease-1

 */

    [liuyan setName:@"柳妍"];

    //NSLog(@"%ld",[liuyan retainCount]);

    Person *p1=[liuyan retain];//retain 只负责+1

    //NSLog(@"%ld",[p1 retainCount]);

    Person *p2=[liuyan retain];

//NSLog(@"%ld %ld",[liuyan retainCount],[p1 retainCount],[p2 retainCount]);

    [liuyan retain];

NSLog(@"%ld",[liuyan retainCount]);

    

//    Person *lixiaolu=[[Person alloc]initWithName:@"李小璐" age:19];

    //NSLog(@"%ld",[lixiaolu retainCount]);

    

    [p2 autorelease];//释放

    //[liuyan release];

    [p1 setName:@"林志林"];//如果多个指针指向一个区域,某一个指针release,释放后再赋值或者打印retainCount仍能打出,但一旦该区域内存被占用,程序就会崩溃,避免这种情况发生;如果指针用了retain就没关系;

//    NSLog(@"%@",p1.name);

//    [p1 dealloc];//强制删除

//    ----------------------------------------------//

/*    NSAutoreleasePool *pool=[[NSAutoreleasePool alloc]init];///////////////////释放池;

    [liuyan autorelease];

    NSLog(@"%ld",[liuyan retainCount]);

    [pool release];/////////////////////释放池;

 */

//    ----------------------------------------------//

    

    

    

//    释放池 新格式

   

        [liuyan copy];

        [liuyan autorelease];

        @autoreleasepool {

        [p2 autorelease];

        [p1 autorelease];

    }

    }

    

 /* //    -------------------内存管理原则--------------------//

  提前避免;

 1.出池尽量让内存为0;引用计数为0;

  2.在函数段里看到alloc retain才能release,相加和减少的次数必须相等+便利构造器在main函数里就不能对它release;便利构造器只能在函数体内进行释放;

  3.copy产生的内存在外部释放,实现函数里不用release释放!!

  //    -------------------内存管理原则--------------------//

  Copy

  深拷贝  两个对象,两块内容,互不相干;

  浅拷贝  两个对象,同一块内容.//相当于快捷方式

  */


iOS- 非ARC的项目内存管理细节详解(实战)

1.前言  

接上文:iOS- 如何将非ARC的项目转换成ARC项目(实战)

 

2.内存管理时相关的配置  

当我们把将非ARC的内存管理都管理好后,发现在做有些操作的时候内存还是在一直的缓慢增加 比如做一个最简单的随机数UITableView的显示与滑动,进行内存管理后,不应该出现内存增加的,但是一直滑动内存就一直缓慢的往上增加的情况。

这时候我们可以检查下看这里的属性是否打勾:

或者检测app一启动时控制台有没有立即输出下列这句话

 

如果勾上,上面三个选项,控制台就会出现下列几行输出

ARCTest(651,0x1f321a8) malloc: stack logs being written into /tmp/stack-logs.651.8af7000.ARCTest.jGqgoD.indexARCTest(651,0x1f321a8) malloc: recording malloc and VM allocation stacks to disk using standard recorder

 

如果发现有的话,把图1的三个属性的勾去掉。这三个选项只要用户在不断的操作,程序就会记录下一些东西,这个时候就会耗费一定的内存。

3.内存管理方法  

3.1.在非ARC环境下,所有的类方法

在非ARC环境下,所有的类方法初始化

系统不会帮你管理内存。

而且你直接 release 也没有效果

内存还是会增加直至崩溃/
 
这样释放是错误的,没有效果的,内存还是会继续增加:
 
1
2
3
4
5
6
7
while (true) {      
 
  NSMutableArray *arr = [NSMutableArray array];
 
      [arr release];
 
}

 正确的类方法内存管理应该是:

    

 

正确内存走势图:

 

3.2.而如果是init 初始化的方法
 
1
2
3
4
5
6
7
while (true) {
 
     NSString *re = [[NSString alloc] initWithFormat:@"ddd"];
 
     [re release];
 
 }
我们直接手动release一次,即可将内存完成释放正确。
这就是在非ARC里类初始化和init初始化的内存管理上区别。
因为之前在网上看到有帖子说类初始化不需要用户手动管理,所以自己做了个测试。在非ARC里是行不通的。

 

0 0