raywenderlich写的关于内存管理,第一篇,再说一次基础知识点

原文连接地址:http://www.raywenderlich.com/2657/memory-management-in-objective-c-tutorialhtml

著做权声明:本文由http://www.cnblogs.com/andyque翻译,欢迎转载分享。请尊重做者劳动,转载时保留该声明和做者博客连接,谢谢!java

 

 

教程截图:node

  当我检查其余开发人员的代码时,彷佛最多见的错误老是围绕在以Object-C中的内存管理为中心。若是您使用的语言是java或C#,它们会自动为您处理内存管理,但这也会使你对于手工内存管理工做更加迷惑。所以,在本教程中,您将经过一些实践来学习Object-C中的内存管理是如何工做的。咱们将讨论引用计数如何工做,并经过学习内存管理的全部关键点来构建一个真实世界的例子——一个关于您喜好的寿司类型的应用程序。程序员

  本教程是针对初学者的iOS开发人员或者时关注这个主题的中级开发人员。废话就少啰嗦了,开始编码。objective-c

 

开始数组

  在xcode开发环境中,打开File\New Project,选择iOS\Application\Navigation-based Application,并将新项目命名为ProMemFun,执行Build\Build and Run, 在模拟器中你会看到一个以下空表视图:xcode

 

 

  比方说,咱们但愿在这个列表中填入咱们喜好的寿司类型。最简单的方法是建立一个数组来容下每一种寿司类型的字符串名称,而后每次咱们显示一行,从数组中放入合适的字符串到表格中。在rootViewController.h中为寿司类型声明一个实例变量,代码以下:安全

 

复制代码
#import <UIKit/UIKit.h>

@interface RootViewController : UITableViewController {
NSArray * _sushiTypes;
}

@end
复制代码

 

  经过这个声明,每一个RootViewController实例对象将有空间来存储一个指向NSArray数组的指针,这是一个Object-C类,使用这个数组初始化后就不能改变它。若是你须要更改一个初始化后的数组(例如,添加一项后),你应该使用NSMutableArray替代。app

 

  也许你会奇怪,为何咱们在命名的变量前面添加一个下划线?这刚好是我喜欢作的事情,这样作有些事情会变得更容易。在后续的关于Objec-C教程中我将讨论我为何喜欢这么作,可是如今请注意,到目前为止,咱们所做的是仅仅添加了一个实例变量,没有作与属性相关的东东,咱们把它命名为“如下划线开头”,这只是一个我的的喜爱问题,其实它没有作特别的东西。ide

  如今,打开RootViewController.m文件,注释viewDiaLoad,而后设置如下代码:

复制代码
- (void)viewDidLoad {
[super viewDidLoad];

_sushiTypes = [[NSArray alloc] initWithObjects:@"California Roll",
@"Tuna Roll", @"Salmon Roll", @"Unagi Roll",
@"Philadelphia Roll", @"Rainbow Roll",
@"Vegetable Roll", @"Spider Roll",
@"Shrimp Tempura Roll", @"Cucumber Roll",
@"Yellowtail Roll", @"Spicy Tuna Roll",
@"Avocado Roll", @"Scallop Roll",
nil];
}
复制代码

 

 

  如今咱们进入内存管理,Object-C中建立的对象使用的是引用计数。这就意味着每个对象都跟踪有多少其余的对象引用它。一旦引用计数变为0,这个对象的内存就会安全的释放掉。

  做为一个程序员,你要确保对象的引用计数老是准确的。当你在某个地方存储了一个对象的指针(好比是实例变量),你须要增长引用计数,有时候须要递减引用计数。

“个人天啊”,你可能会思考,“这听起来太复杂和混乱了”,不要担忧,作起来要比听起来简单些。

 

初始化对象和释放对象的内存

  无论何时你在Object-c中建立一个对象,首先你要调用alloc为这个对象去分配内存空间,而后调用init方法去初始化这个对象,当init方法不带任何参数时,有时候你会看到程序员用new方法替代(这相似于先调用alloc,而后调用init)。

  最重要的是一旦你这么作了,你会获得一个新的对象,而且它的引用计数置为1。所以,当完成全部的工做后,你须要递减引用计数。

好了,咱们给出一个开头。仍然是在RootViewController.m中,去文件末尾,像下面同样设置viewDidUnload和dealloc方法:

 

复制代码
- (void)viewDidUnload {
[_sushiTypes release];
_sushiTypes = nil;
}

- (void)dealloc {
[_sushiTypes release];
_sushiTypes = nil;
[super dealloc];
}
复制代码

 

 

 

 

  记住当你用alloc/init建立一个array时,它的引用计数已经为1了。所以当你完成与array相关的工做时,须要递减它的引用计数。在Object-C中,你能够经过对这个对象调用release方法。

  可是你应该在什么地方release呢?哦,你必定要在dealloc方法中release这个array,显然易见,当这个viewController销毁后,你也不会再须要这个array了。因此,记住不管什么时候你在viewDidLoad中建立一个对象(这个对象的引用计数会初始化为1),你应该在viewDidUnload中释放这个对象。不要太担忧,关于这儿主题我会专门写一篇教程。

  注意,释放对象后,请将其设置为nil,若是你试图调用一个指向nil的指针,你的程序会崩溃。

  好了,如今让咱们使用新的array。首先,替换掉tableView:numberOfRowsInSection 里面的"return 0",替换成下面的语句:

// Replace "return 0;" in tableView:numberOfRowsInSection with this
return _sushiTypes.count;

  这里意思是说,tableView里面的数据行数等于sushiTypes数组里面的记录个数。

 

 

 

 

  如今,咱们须要告诉table view,每一行具体显示什么内容。找到tableView:cellForRowAtIndexPath函数,而后找到注释 “Configure the cell”,在后面添加下列代码:

NSString * sushiName = [_sushiTypes objectAtIndex:indexPath.row]; // 1
NSString * sushiString =
[[NSString alloc] initWithFormat:@"%d: %@",
indexPath.row, sushiName]; // 2
cell.textLabel.text = sushiString; // 3
[sushiString release]; // 4

 

  让咱们一行一行代码解释一下上面的程序:

  1. 根据当前行号查找sushiTypes数组里面对应的字符串
  2. 咱们想这样显示字符串:“3: Unagi Roll“,3表明行号,而“Unagi Roll” 是那一行的sushi的名字。要构建一个具备这种格式的字符串的话,你能够用NSString的initWithFormat来轻松构建。记住,当你这样作完以后,返回的字符串的引用计数是1.
  3. 设置当前行的文本为刚刚获得的格式化字符串。当你这样设置以后,text label会把sushiString copy一下。(相应的,其引用计数会加1)
  4. 咱们用完sushiString了,所以,调用release把它释放掉。若是你忘了这样作的话,那么这里就会致使一个内存泄漏。由于字符串的引用计数是1,永远也不会获得释放。(即便text label把sushiString释放了一次,也没用。由于刚开始建立的时候是1,赋值的时候为2,而后再label再释放一次,为1。而若是你不调用[sushiString release]的话,那么就会内存泄漏)

  编译并运行,若是一切OK的话,你将会看到sushi的列表。

Autorelease Your Potential

  目前为止,你知道了,当你调用alloc/init的时候,引用计数是1,当你用完这个对象的时候,你须要调用release把引用计数变为0.

  接下来,让咱们讨论一下另一种方法----autorelease。

  当你给一个对象发送autorelease消息后,它的意思是说“嘿!我想让你在未来某个时刻被释放掉,好比当前run loop结束的时候。可是,如今我可以使用你”。

  最容易理解的方式就是看代码。修改 tableView:cellForRowAtIndexPath 方法,找到 “Configure the cell”注释,在后面添加下列代码:

NSString * sushiName = [_sushiTypes objectAtIndex:indexPath.row]; // 1
NSString * sushiString =
[[[NSString alloc] initWithFormat:@"%d: %@",
indexPath.row, sushiName] autorelease]; // 2
cell.textLabel.text = sushiString; // 3

  所以,和上一次相比,这里只改了两个地方。首先,你在第二行结尾的时候调用了autorelease。其次,你把最后一行release的调用代码移除掉了。

  接上来,我解释一下。在第2行代码结束的时候,sushiString的引用计数是1,可是,咱们给它发送了一个autorelease消息。这意味着,你能够在这个函数里面使用sushiString,可是,一旦下一次run loop被调用的时候,它就会被发送release对象。而后引用计数改成0,那么内存也就被释放掉了。(关于autorelease究竟是怎么工做的,个人理解是:每个线程都有一个autoreleasePool的栈,里面放了不少autoreleasePool对象。当你向一个对象发送autorelease消息以后,就会把该对象加到当前栈顶的autoreleasePool中去。当当前runLoop结束的时候,就会把这个pool销毁,同时对它里面的全部的autorelease对象发送release消息。而autoreleasePool是在当前runLoop开始的时候建立的,并压入栈顶。那么什么是一个runLoop呢?一个UI事件,Timer call, delegate call, 都会是一个新的Runloop。)

  在这个例子中,上面的解决办法很是好,可是,后面咱们不会使用它。然而,若是咱们想要存储一个变量(可是不retain它),而后在某个地方使用这个变量(好比用户点击某一行的时候,选中那一行),那么咱们就有大麻烦了。由于那样咱们是在尝试访问一个已经销毁的对象,可想而知,程序确定是crash拉!

  有时候,当你调用一些方法的时候,你获得的返回给你的对象的引用计数是1,可是,它是一个autorelease的对象。你修改一下tableView:cellForRowAtIndexPath方法,修改为下面的样子,而后你就知道我刚刚讲的是什么意思了:

NSString * sushiName = [_sushiTypes objectAtIndex:indexPath.row]; // 1
NSString * sushiString =
[NSString stringWithFormat:@"%d: %@",
indexPath.row, sushiName]; // 2
cell.textLabel.text = sushiString; // 3

  这里代码改变之处是第2行。你不是本身调用 alloc/init/autorelease,而是使用NSString的一个类方法stringWithFormat。这个方法会返回一个引用计数为1的字符串,而且它是一个autorelease的对象。所以,和上面的写法同样,你能够放心的使用这个字符串,可是,若是你不retain它,而后又在后面某个地方使用它的话,那么程序就会崩溃。

  你可能会奇怪,你怎么知道哪些对象返回给你的时候是autorelease的?好吧,让我教你一个简单的惯用法,具体以下:

  • 若是一个方法以init或者copy开头,那么返回给你的对象的引用计数是1,而且这不是一个autorelease的对象。换句话说,你调用这些方法的话,你就对返回的对象负责,你再用完以后必须手动调用release来释放内存。
  • 若是一个方法不是以init或者copy开头的话,那么返回的对象引用计数为1,可是,这是一个autorelease对象。换句话说,你如今能够放心使用此对象,用完以后它会自动释放内存。可是,若是你想在其它地方使用它(好比换个函数),那么,这时,你就须要手动retain它了。

Retain Your Wits

  若是你如今有一个autorelease对象,而且像在后面继续使用它,那么该怎么办呢?其实很简单,你只须要对它发送retain消息就OK了。这样会把引用计数变为2,可是,只要出了当前runLoop,那么引用计数又会变为1,那么对象仍是不会销毁(由于只有引用计数为0才能销毁)。

  让咱们来看看具体怎么作。打开RootViewController.h ,而后在@interface里面添加一个实例变量:

NSString * _lastSushiSelected; 

  这里只是定义了一个新的实例变量,它将用来追踪选中的最后那一行的字符串。

  接下来,修改 tableView:didSelectRowAtIndexPath ,修改以下:

复制代码
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

NSString * sushiName = [_sushiTypes objectAtIndex:indexPath.row]; // 1
NSString * sushiString = [NSString stringWithFormat:@"%d: %@",
indexPath.row, sushiName]; // 2

NSString * message = [NSString stringWithFormat:@"Last sushi: %@. Cur sushi: %@", _lastSushiSelected, sushiString]; // 3
UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:@"Sushi Power!"
message:message
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:@"OK", nil] autorelease]; // 4
[alertView show]; // 5

[_lastSushiSelected release]; // 6
_lastSushiSelected = [sushiString retain]; // 7

}
复制代码

  这里的代码比较多,让咱们一行一行来看:

  1. 查找当前行对应的shshiTypes数组里面的字符串。
  2. 根据当前行号构建一个新的字符串。注意,这里使用的是stringWithFormat方法,它返回的是一个autorelease的字符串。由于这个方法并非以init或者copy开头,因此你就知道。记住,这意味着,你能够在这个函数里面使用此字符串,可是出了这个函数的话,若是你还想继续使用之,那必需要对它发送一个retain消息。
  3. 构建一个消息,用来显示当前选中的sushi和最后选中的sushi。和上面同样,这里也是使用的stringWithFormat方法,它返回的是一个autorelease对象。由于咱们只想在这个函数里面使用,因此没有retain。
  4. 建立一个alertView来显示刚刚构建的那个消息。这里是经过alloc/init方式建立的,因此咱们须要在以后再发送一个autorelease消息,这样在出了这个函数之后,这个对象就会被释放掉了。
  5. 显示这个alert view。
  6. 再你设置lastSushiSelected实例变量以前,你须要先释放当前的lastSushiSelected实例变量,若是当前实例变是已是nil的话,也没有关系,因上nil对象能够接收任何消息。
  7. 由于你想在这个函数以外再使用lastSushiSelected这个字符串,因此你须要retain它。

  还有一件事你不能忘记。为了保存不会有任何内存泄漏,你须要在RootViewController的dealloc方法里面调用下面方法来释放内存:

[_lastSushiSelected release];
_lastSushiSelected = nil;

  基本上,在dealloc方法被里面,你须要对“你负责的对象”发送release消息,而且要把它赋值为nil。

  编译并运行,如今,当你选中一行,你就能够看到下面的屏幕输出了。

引用计数相关参考资料

  让咱们回顾一下所学的知识:

  • 当你调用alloc/init的时候,你获得一个引用计数是1的对象。
  • 当你用完这个对象以后,你要对它调用release消息,使其引用计数为0,这样它的内存才会被释放掉。
  • 当你调用一个方法,它不是以init或者copy开头的,这时,返回给你的对象是autorelease的,它是一种在未来某个时刻会自动被释放的对象。(这里我也要提醒你们一句,好比你在写一个函数,它的名字是xxx,没有以init或者copy开头,那么记得你返回的对象必定要是autorelease的,不然,别人在使用你这个函数的时候就会把它当前是autorelease的,那么他就不会release它,这样就会形成内存泄漏,千万要切记!!!)
  • 若是你想继续使用autorelease对象,那么你就要给它放送一个retain消息。
  • 若是你使用alloc/init方法建立了一个对象,可是你想让它本身在出了runLoop以后被自动释放的话,那么你能够在alloc/init以后再调用autorelease。这也是一种见得比较多的写法了。好比,cocos2d里面调用[xxx node]的时候,就等于[[[xxx alloc] init]autorelease].

  本教程只讲述了objc内存管理的很基本的部分,若是想得到更多的信息,请参考苹果的文档: Memory Management Programming Guide.

何去何从?

  这里有本教程的完整源代码

  无论你是一个多么优秀的开发者,或者你对内存管理的理解有多么的深刻,你仍是不可避免地要犯一些内存相关的错误。所以,在个人下一篇教程中,我将教你们若是使用XCode, Instruments, 和 Zombies来检测内存泄漏。所以,提早准备好跟我来吧!  

相关文章
相关标签/搜索