免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!
注:本教程由北方和我本人合作翻译。
教程截图:
当我检查其他开发人员的代码时,似乎最常见的错误总是围绕在以Object-C中的内存管理为中心。如果您使用的语言是java或C#,它们会自动为您处理内存管理,但这也会使你对于手工内存管理工作更加迷惑。因此,在本教程中,您将通过一些实践来学习Object-C中的内存管理是如何工作的。我们将讨论引用计数如何工作,并通过学习内存管理的所有关键点来构建一个真实世界的例子——一个关于您喜爱的寿司类型的应用程序。
本教程是针对初学者的iOS开发人员或者时关注这个主题的中级开发人员。废话就少啰嗦了,开始编码。
开始
在xcode开发环境中,打开File\New Project,选择iOS\Application\Navigation-based Application,并将新项目命名为ProMemFun,执行Build\Build and Run, 在模拟器中你会看到一个如下空表视图:
比方说,我们希望在这个列表中填入我们喜爱的寿司类型。最简单的方法是创建一个数组来容下每一种寿司类型的字符串名称,然后每次我们显示一行,从数组中放入合适的字符串到表格中。在rootViewController.h中为寿司类型声明一个实例变量,代码如下:
[代码]c#/cpp/oc代码:
1
#import <UIKit/UIKit.h>
2
3
@
interface
RootViewController : UITableViewController {
4
NSArray * _sushiTypes;
5
}
6
7
@end
通过这个声明,每个RootViewController实例对象将有空间来存储一个指向NSArray数组的指针,这是一个Object-C类,使用这个数组初始化后就不能改变它。如果你需要更改一个初始化后的数组(例如,添加一项后),你应该使用NSMutableArray替代。
也许你会奇怪,为什么我们在命名的变量前面添加一个下划线?这恰好是我喜欢做的事情,这样做有些事情会变得更容易。在后续的关于Objec-C教程中我将讨论我为什么喜欢这么做,但是现在请注意,到目前为止,我们所作的是仅仅添加了一个实例变量,没有做与属性相关的东东,我们把它命名为“以下划线开头”,这只是一个个人的喜好问题,其实它没有做特别的东西。
现在,打开RootViewController.m文件,注释viewDiaLoad,然后设置以下代码:
[代码]c#/cpp/oc代码:
01
- (
void
)viewDidLoad {
02
[super viewDidLoad];
03
04
_sushiTypes = [[NSArray alloc] initWithObjects:
@"California Roll"
,
05
@"Tuna Roll"
,
@"Salmon Roll"
,
@"Unagi Roll"
,
06
@"Philadelphia Roll"
,
@"Rainbow Roll"
,
07
@"Vegetable Roll"
,
@"Spider Roll"
,
08
@"Shrimp Tempura Roll"
,
@"Cucumber Roll"
,
09
@"Yellowtail Roll"
,
@"Spicy Tuna Roll"
,
10
@"Avocado Roll"
,
@"Scallop Roll"
,
11
nil];
12
}
现在我们进入内存管理,Object-C中创建的对象使用的是引用计数。这就意味着每一个对象都跟踪有多少其他的对象引用它。一旦引用计数变为0,这个对象的内存就会安全的释放掉。
作为一个程序员,你要确保对象的引用计数总是准确的。当你在某个地方存储了一个对象的指针(比如是实例变量),你需要增加引用计数,有时候需要递减引用计数。
“我的天啊”,你可能会思考,“这听起来太复杂和混乱了”,不要担心,做起来要比听起来简单些。
初始化对象和释放对象的内存
不管什么时候你在Object-c中创建一个对象,首先你要调用alloc为这个对象去分配内存空间,然后调用init方法去初始化这个对象,当init方法不带任何参数时,有时候你会看到程序员用new方法替代(这类似于先调用alloc,然后调用init)。
最重要的是一旦你这么做了,你会得到一个新的对象,并且它的引用计数置为1。因此,当完成所有的工作后,你需要递减引用计数。
好了,我们给出一个开头。仍然是在RootViewController.m中,去文件末尾,像下面一样设置viewDidUnload和dealloc方法:
[代码]c#/cpp/oc代码:
01
- (
void
)viewDidUnload {
02
[_sushiTypes release];
03
_sushiTypes = nil;
04
}
05
06
- (
void
)dealloc {
07
[_sushiTypes release];
08
_sushiTypes = nil;
09
[super dealloc];
10
}
记住当你用alloc/init创建一个array时,它的引用计数已经为1了。因此当你完成与array相关的工作时,需要递减它的引用计数。在Object-C中,你可以通过对这个对象调用release方法。
但是你应该在什么地方release呢?哦,你一定要在dealloc方法中release这个array,显然易见,当你释放这个array后,你不会再需要这个array了。无论何时你在viewDidUnload中创建一个对象(这个对象的引用对象计数设置为1),你应该在viewDidUnload中释放这个对象。不要太担心,关于这儿主题我会专门写一篇教程。
注意,释放对象后,请将其设置为nil,如果你试图调用一个指向nil的指针,你的程序会崩溃。
好了,现在让我们使用新的array。首先,替换掉tableView:numberOfRowsInSection 里面的”return 0″,替换成下面的语句:
[代码]c#/cpp/oc代码:
1
// Replace "return 0;" in tableView:numberOfRowsInSection with this
2
return
_sushiTypes.count;
这里意思是说,tableView里面的数据行数等于sushiTypes数组里面的记录个数。
现在,我们需要告诉table view,每一行具体显示什么内容。找到tableView:cellForRowAtIndexPath函数,然后找到注释 “Configure the cell”,在后面添加下列代码:
[代码]c#/cpp/oc代码:
1
NSString * sushiName = [_sushiTypes objectAtIndex:indexPath.row];
// 1
2
NSString * sushiString =
3
[[NSString alloc] initWithFormat:
@"%d: %@"
,
4
indexPath.row, sushiName];
// 2
5
cell.textLabel.text = sushiString;
// 3
6
[sushiString release];
// 4
让我们一行一行代码解释一下上面的程序:
编译并运行,如果一切OK的话,你将会看到sushi的列表。
目前为止,你知道了,当你调用alloc/init的时候,引用计数是1,当你用完这个对象的时候,你需要调用release把引用计数变为0.
接下来,让我们讨论一下另外一种方法—-autorelease。
当你给一个对象发送autorelease消息后,它的意思是说“嘿!我想让你在将来某个时刻被释放掉,比如当前run loop结束的时候。但是,现在我能够使用你”。
最容易理解的方式就是看代码。修改 tableView:cellForRowAtIndexPath 方法,找到 “Configure the cell”注释,在后面添加下列代码:
[代码]c#/cpp/oc代码:
1
NSString * sushiName = [_sushiTypes objectAtIndex:indexPath.row];
// 1
2
NSString * sushiString =
3
[[[NSString alloc] initWithFormat:
@"%d: %@"
,
4
indexPath.row, sushiName] autorelease];
// 2
5
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方法,修改成下面的样子,然后你就知道我刚刚讲的是什么意思了:
[代码]c#/cpp/oc代码:
1
NSString * sushiName = [_sushiTypes objectAtIndex:indexPath.row];
// 1
2
NSString * sushiString =
3
[NSString stringWithFormat:
@"%d: %@"
,
4
indexPath.row, sushiName];
// 2
5
cell.textLabel.text = sushiString;
// 3
这里代码改变之处是第2行。你不是自己调用 alloc/init/autorelease,而是使用NSString的一个类方法stringWithFormat。这个方法会返回一个引用计数为1的字符串,并且它是一个autorelease的对象。因此,和上面的写法一样,你可以放心的使用这个字符串,但是,如果你不retain它,然后又在后面某个地方使用它的话,那么程序就会崩溃。
你可能会奇怪,你怎么知道哪些对象返回给你的时候是autorelease的?好吧,让我教你一个简单的惯用法,具体如下:
Retain Your Wits
如果你现在有一个autorelease对象,并且像在后面继续使用它,那么该怎么办呢?其实很简单,你只需要对它发送retain消息就OK了。这样会把引用计数变为2,但是,只要出了当前runLoop,那么引用计数又会变为1,那么对象还是不会销毁(因为只有引用计数为0才能销毁)。
让我们来看看具体怎么做。打开RootViewController.h ,然后在@interface里面添加一个实例变量:
[代码]c#/cpp/oc代码:
1
NSString * _lastSushiSelected;
这里只是定义了一个新的实例变量,它将用来追踪选中的最后那一行的字符串。
接下来,修改 tableView:didSelectRowAtIndexPath ,修改如下:
[代码]c#/cpp/oc代码:
01
- (
void
)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
02
03
NSString * sushiName = [_sushiTypes objectAtIndex:indexPath.row];
// 1
04
NSString * sushiString = [NSString stringWithFormat:
@"%d: %@"
,
05
indexPath.row, sushiName];
// 2
06
07
NSString * message = [NSString stringWithFormat:
@"Last sushi: %@. Cur sushi: %@"
, _lastSushiSelected, sushiString];
// 3
08
UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:
@"Sushi Power!"
09
message:message
10
delegate
:nil
11
cancelButtonTitle:nil
12
otherButtonTitles:
@"OK"
, nil] autorelease];
// 4
13
[alertView show];
// 5
14
15
[_lastSushiSelected release];
// 6
16
_lastSushiSelected = [sushiString retain];
// 7
17
18
}
这里的代码比较多,让我们一行一行来看:
还有一件事你不能忘记。为了保存不会有任何内存泄漏,你需要在RootViewController的dealloc方法里面调用下面方法来释放内存:
[代码]c#/cpp/oc代码:
1
[_lastSushiSelected release];
2
_lastSushiSelected = nil;
基本上,在dealloc方法被里面,你需要对“你负责的对象”发送release消息,并且要把它赋值为nil。
编译并运行,现在,当你选中一行,你就可以看到下面的屏幕输出了。
让我们回顾一下所学的知识:
本教程只讲述了objc内存管理的很基本的部分,如果想获得更多的信息,请参考苹果的文档: Memory Management Programming Guide.
这里有本教程的完整源代码。
不管你是一个多么优秀的开发者,或者你对内存管理的理解有多么的深入,你还是不可避免地要犯一些内存相关的错误。因此,在我的下一篇教程中,我将教大家如果使用XCode, Instruments, 和 Zombies来检测内存泄漏。因此,提前准备好跟我来吧!