随着iPhone SDK的发布,iPhone/iPod Touch被售出数以百万计(作者注:这是一篇以前的文章,现在iPhone/iPod Touch的销售数量远大于此数)而且App Store广受欢迎,越来越多的开发者开始进行iPhone及Cocoa Touch的开发。SDK设计精良而且很容易入门,最大的障碍来自于Apple选择作为OSX和iPhone开发的独特语言(其前身为NextStep):Object C。初看时,Object C非常丑陋而且与人们常用的基于C的语法(C, C++, ActionScript, JavaScript, Java, C#, GLSL等)并不相似。但一旦对其有所研究后,你会喜欢上它并明白其语法为何如此。

 

大部分人初次接触Objective C/Cocoa开发时的最大困难是内存管理 – 当你最终理解了它是多么简单(尽管这可能需要通过几个项目)时,你就会明白这一切实际上是多么漂亮。实际上,我发现其非常实用,所以编写了一个C++类来实现类似功能(http://code.google.com/p/ofxmsaof/wiki/ofxObjCPointer).

 

C/C++的方式

 

对于传统C/C++开发,确定使用过后的对象清理的职责多多少少有那么些噩梦的意味。C/C++语言本身并未提供怎样进行此项工作方法,一切都取决于程序员本身个人的习惯和约定。

 

想象一下程序员A编写了一段代码(类A),此段代码创建叫做Data的对象并为其分配内存然后将此对象传递给程序员B,B将在他的代码(类B)中使用此对象。两个程序员使用同一个对象Data。到底是谁应该在不再使用对象Data后对其进行删除?

 

如果类A只是创建和初始化Data并不再使用它,那么类B可以在使用完Data后安全地删除它。但是,怎样才能保证类A不再使用Data?如果在类B使用Data时类A仍然需要使用它,我们怎样才能知道何时删除它?如果类A在使用完Data后进行删除,类B有可能还需要使用,这会导致程序崩溃。如果类B在使用完后进行删除,同样如果类A还在使用将会引起崩溃。当然,如果类C也需要更新同一Data,那么情况会更复杂。

 

有一些约定和模式可以处理这些情况,比如有下列形式的通讯:”等下,不要删除对象,我正在使用” 或者 “好,我使用完毕,你可以任意处置“。这实际上正好是开发引用计数的原因,这种技术是Objective C使用的主要内存管理技术。

 

Objective C的方式

 

Objective C 使用’引用计数’作为其主要内存管理技术(wikipedia.org/wiki/Reference_counting)。 每个对象都保存一份记录了它被“需要”的次数的内部计数器。系统保证当对象被需要时不会被删除,当不被需要时才被删除。这听上去有点象自动垃圾收集(即Java, AS3 wikipedia.org/wiki/Garbage_collection_(computer_science)的工作方式),但实际上并不是如此。它与自动GC (Java, AS3 等)的主要区别在于,GC是一段单独的代码在后台定期运行检查是否对象被引用以及哪个对象不再被需要。然后所有未被使用的对象将被自动删除,程序员不需做任何特殊处理。而引用计数方法,程序员有责任通知他需要使用或完成使用某个对象,当对象不再被使用时,即引用计数为0时,它将立即被删除。(有关更多详情请参考以上wikipedia链接)。

 

注意: Objective C 2.0 同样具有一个选项允许自动垃圾收集。但是,开发iPhone程序不能使用此选项,所以了解引用计数,对象所有权等仍然十分重要。

 

对象拥有者

 

理解对象拥有者的概念十分重要。在Objective C中, 一个对象的所有者是宣称“我需要此对象,不要删除它”的某人(或某段代码)。它可能是创建此对象的人(或代码)(比如上例中的类A)。或者它可能是接收并需要此对象的另一个人(或代码)(即上例中的类B)。一个对象可以具有不只一个所有者。 一个对象的所有者的数量就是引用计数。

 

对象所有者有责任在使用完毕后通知对象。这样做后,他们将不再是对象的所有者,或者说对象的引用计数减一。当一个对象没有所有者时(即没人再需要时或引用计数为0),它将被删除。

 

不作为对象的拥有者(临时使用)你仍然可以使用对象,但是要记住此对象可能已经(或者将)被删除。所以如果你希望长期使用一个对象,你应该成为对象的拥有者。如果你只打算临时使用对象(例如,在你的函数中,或者事件循环中 – 后面会介绍autorelease pool),那么你不需要成为拥有者。

 

消息

 

你可以传递给一个对象的主要消息(有关内存管理)是:

 

alloc (即 [NSString alloc]): 此消息为对象分配了一个例程(本例中为NSString)。它同时将引用计数设为1。你是此对象的唯一拥有者。当使用完毕时,你必须释放此对象。(题外话:记住对新分配的对象调用init函数: [[NSString alloc] init] 或 [[NSString alloc] initWithFormat:] 。)

 

new (例如: [NSString new]): 这是[[NSString alloc] init]的简写。

 

retain (例如:[aString retain]): 当你传递一个存在的对象(内存已经分配)而且你需要保持它时你需要调用此函数。这就好像在说:“我需要此对象,不要在我完成前删除它”。引用计数将加1,你成为了此对象的拥有者(与以前拥有它的拥有者一起)。完成时,你必须释放它。

 

release (例如 [aString release]): 在你完成对象的使用后调用。你将不再拥有此对象,因此引用计数将减1。如果此时引用计数为0(即没有拥有者),那么对象将被自动删除,内存被释放,否则因为其他拥有者还在使用,对象将继续保存在内存中。这就好像在说“好了,我已经使用完此对象,如果没有人使用它,你可以删除它了”。如果你不是此对象的拥有者,你不应该调用此函数。如果你 对象的拥有者,在你完成后,你必须调用它。

 

autorelease (例如: [aString autorelease])。它表示你只是暂时需要对象,并不会使你成为对象的拥有者。这好像在说“Ok,我现在需要对象,在我完成几件事前保留在内存中,然后你可以删除它。”。更多介绍请参见稍后的’autorelease pools’部分。

 

所以当你在与Objective C指针/对象打交道时,重要的是要记住发送正确的消息。基本规则是:如果你拥有一个对象(分配(alloc)或保持 (retain)它),那么你必须释放(release)它。如果你不拥有它 (通过便捷方式产生或别人分配),那么你就不要释放它。

 

便捷方法

 

Cocoa下的许多类都具有所谓便捷方法。它们是用于直接分配和初始化对象的静态方法。你并不会成为返回对象的拥有者,它们将在autorelease pool (自动释放池)弹出时(通常在事件循环的结束处,但与你的程序相关)自动被删除。

 

例如,显性地分配和初始化一个NSNumber的方法是:

1
NSNumber *aNumber = [[NSNumber alloc] initWithFloat:5;

 

此方法创建一个NSNumber的新例程并通过’initWithFloat’方法使用参数5.0f进行初始化。

 

aNumber的引用计数为1。你是aNumber的拥有者,所以你必须在使用完毕后释放它。

 

使用便捷方法:

1
NSNumber *aNumber = [NSNumber numberWithFloat:5.0f];

 

此方法同样创建一个NSNumber的新例程,通过’numberWithFloat’方法使用参数5.0f进行初始化。

 

它同样具有引用计数1。但是你不是aNumber的拥有者所以不应该释放它。拥有者是NSNumber类,对象在当前作用域结束时被删除 – 由autorelease pool定义,详情稍后介绍 – 你不应该release(释放) 对象,但要记住对象将存在一段较长的时间。

 

便捷方法通常具有与相应init函数一样的名字,但将init 替换为对象类型的名字了,比如对于NSNumber: initWithFloat -> numberWithFloat, initWithInt -> numberWithInt。

 

一个NSString的例子:

1
2
3
NSString *aString1 = [[NSString alloc] initWithFormat:@"Results are %i and %i", int1, int2];  // explicit allocation, you are the owner, you must release when you are done

NSString *aString2 = [NSString stringWithFormat:@"Results are %i and %i", int1, int2];  // convenience method, you are not the owner, the object will be deleted when the autorelease pool is popped

 

自动释放池

 

自动释放池是NSAutoreleasePool的一个例程,它定义了临时对象(将被自动释放的对象)的作用域。任何将被自动释放的对象(例如:你对其发送了autorelease消息的对象或者使用便捷方法创建的对象)都会被加入当前autorelease pool(自动释放池)中。当autorelease pool被弹出(释放)时,所有加入其中的对象也将被自动释放。这是管理临时对象自动释放的一种简单方法。

 

例如,你希望创建几个对象用于临时计算,所以并不需要保持所有定义的局部变量以致在函数结束时需要为它们调用release, 你可以在创建时使用autorelease (或便捷方法),它们将在下一次自动释放池弹出时自动被释放。注意:这样的做法有一个缺点,我将在便捷和显性方式的比较部分中讨论。

 

Autorelease pool可以嵌套,这种情况下自动释放对象将被加入最近创建的autorelease pool中(释放池以后进先出堆栈的方式堆放)。

 

示例在自动释放,便捷与显性方式比较一节中。

 

数组,字典等

 

数组,字典等通常保存加入其中的任何对象。(当使用第三方集合型对象时,请检查其文档看是否对象被保存)。这意味着这些集合是对象的拥有者,你在添加对象前不需要保持。

 

示例

 

下列代码将产生内存泄漏:

1
2
3
4
5
-(void)addNumberToArray:(float)aFloat
{
    NSNumber *aNumber = [[NSNumber alloc] initWithFloat:aFloat]; // reference count is now 1, you are the owner
    [anArray addObject: aNumber];  // reference count is now 2, the array is also an owner as well as you.
}

 

如果你在数组之外的其他地方不需要用到对象,那么你必须在添加对象后进行释放。下面的代码是正确的:

1
2
3
4
5
-(void) addNumberToArray:(float)aFloat {
    NSNumber *aNumber = [[NSNumber alloc] initWithFloat:aFloat]; // reference count is now 1, you are the owner
    [anArray addObject: aNumber]; // reference count is now 2, the array is also an owner as well as you.
    [aNumber release]; // reference count is now 1, you are not the owner anymore
}

 

现在,当数组被释放时,或者从数组中移除对象时,引用计数将减一,因为数组不再是对象的拥有者,所以对象将被删除。

 

当然,另一种方法也是安全的:

1
2
3
4
-(void) addNumberToArray:(float)aFloat {
    NSNumber *aNumber = [NSNumber numberWithFloat:aFloat]; // reference count is now 1, NSNumber is the owner, you are not
    [anArray addObject: aNumber]; // reference count is now 2, the array is also an owner as well as NSNumber.
}

 

现在当autorelease pool弹出时,NSNumber失去其所有权,引用计数降为一(只有数组是对象的拥有者了)。当数组被释放时,或者当对象被从数组中移除时,引用计数降为0,对象将被删除。

 

你可能会想哪个才是最好的方法? 方法一(显性地使用alloc和release),或者方法二?(便捷方法)。在OSX中,我通常喜欢方法二,因为它看上去更简单而且需要更少的代码。功能上看上去是一样的,但实际上是不同的。实际上使用方法一是更好的方法(特别是为iPhone开发时),或者当你拥有自己的autorelease pool时,方法二更好。下面有详细说明。

 

自动释放,便捷方式与显性方式比较

 

你可能想知道到底下面两种方法有什么不同,或者说各有什么好处:

 

显性方式:

1
2
3
4
5
6
-(void) doStuff:(float)aFloat {
    NSNumber *aNumber = [[NSNumber alloc] initWithFloat:aFloat]; // refcount is 1, you are owner
/// ... do a bunch of stuff with aNumber...
...
    [aNumber release]; // release aNumber
}

 

自动释放方式:

1
2
3
4
5
-(void) doStuff:(float)aFloat {
    NSNumber *aNumber = [NSNumber numberWithFloat:aFloat];// refcount is 1, you are not ownder, will be automatically release
/// ... do a bunch of stuff with aNumber...
...
}

 

使用显性方式,aNumber在doStuf结束处立即被清除,内存被释放。

 

而自动释放方式时,aNumber将在autorelease pool弹出时被释放,通常发生在事件循环的结束处。所以如果你在事件循环中创建了许多自动释放对象,它们都将被加入池中,可能会用完内存。在上例中可能还不是很清楚,我再给出一个例子:

 

显性方式:

1
2
3
4
5
6
7
8
-(void) doStuff:(float)aFloat {
    for(int i=0; i<100; i++) {
        NSNumber *aNumber = [[NSNumber alloc] initWithFloat:aFloat]; // refcount is 1, you are owner
        /// ... do a bunch of stuff with aNumber...
        ...
        [aNumber release]; // release aNumber
    }
}

 

自动释放方式:

1
2
3
4
5
6
7
-(void) doStuff:(float)aFloat {
    for(int i=0; i<100; i++) {
        NSNumber *aNumber = [NSNumber numberWithFloat:aFloat];// refcount is 1, you are not owner, will be automatically released
        /// ... do a bunch of stuff with aNumber...
        ...
    }
}

 

现在你可以看到,在第一个例子中,内存中不会超过一个NSNumber(NSNumber在开始处被分配,并在循环结束时被清除)。而在第二个例子中,每个循环都会创建一个新的NSNumber,而旧NSNumber仍处于内存中等待autorelease pool被释放。在桌面系统中,由于拥有内存大,你可以有更奢侈的选择,但在一个像iPhone这类内存有限的平台,更为重要的是保证对象在不在被需要时尽快删除。

 

当然,另一个选择是创建你自己的autorelease pool,这在你使用了大量临时对象而你又不想麻烦分别释放它们时特别有用。看下下面代码:

 

显性方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-(void) doStuff {
    for(int i=0; i<100; i++) {
        NSNumber *aNumber1 = [[NSNumber alloc] initWithFloat:1]; // refcount is 1, you are owner
        NSNumber *aNumber2 = [[NSNumber alloc] initWithFloat:2]; // refcount is 1, you are owner
        NSNumber *aNumber3 = [[NSNumber alloc] initWithFloat:3]; // refcount is 1, you are owner
        NSNumber *aNumber4 = [[NSNumber alloc] initWithFloat:4]; // refcount is 1, you are owner
        NSNumber *aNumber5 = [[NSNumber alloc] initWithFloat:5]; // refcount is 1, you are owner
        NSNumber *aNumber6 = [[NSNumber alloc] initWithFloat:6]; // refcount is 1, you are owner

        // ... do a bunch of stuff with all objects above.
       ...

       // release all objects
       [aNumber1 release];
       [aNumber2 release];
       [aNumber3 release];
       [aNumber4 release];
       [aNumber5 release];
       [aNumber6 release];
    }
}

 

自动释放方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-(void) doStuff {
    for(int i=0; i<100; i++) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // create your own little autorelease pool

       // these objects get added to the autorelease pool you created above
       NSNumber *aNumber1 = [NSNumber numberWithFloat:1]; // refcount is 1, you are not owner, will be automatically released
       NSNumber *aNumber2 = [NSNumber numberWithFloat:2]; // refcount is 1, you are not owner, will be automatically released
       NSNumber *aNumber3 = [NSNumber numberWithFloat:3]; // refcount is 1, you are not owner, will be automatically released
       NSNumber *aNumber4 = [NSNumber numberWithFloat:4]; // refcount is 1, you are not owner, will be automatically released
       NSNumber *aNumber5 = [NSNumber numberWithFloat:5]; // refcount is 1, you are not owner, will be automatically released
       NSNumber *aNumber6 = [NSNumber numberWithFloat:6]; // refcount is 1, you are not owner, will be automatically released

       // ... do a bunch of stuff with all objects above.
       ...

       [pool release]; // all objects added to this pool (the ones above) are released
    }
}

 

在这种情况下,两段代码实际上是一样的。在第一个例子中,6个NSNumber在循环开始处被创建,在循环结束时被显性释放(你拥有这些对象)。内存中不会超过6个NSNumber.

 

在第二个例子中,你不会拥有任何NSNumber,但通过创建你自己的autorelease pool,你可以控制它们的生存期。因为你在循环中创建及释放autorelease pool,NSNumber仅存在于循环内,所以内存中也不会超过6个NSNumber。如果你不创建autorelease pool,那么在循环结束时,你会有6个NSNumber等待删除,在函数结束时将会有6×100=600个NSNumber存在于内存中。与在其他函数中创建的自动释放对象一起,你可能会拥有大量未用的对象等待释放(所以不会引起内存泄漏),但是如果你没有及时释放,可能会到达内存上限。

 

总结

 

拥有一个对象表示你显性地宣称你需要它(通过使用allocnew或者 retain)。

 

如果你拥有一个对象,你必须显性地宣称你已经使用完毕(通过使用release)。

 

如果你不是对象的拥有者(即你使用autorelease,或通过便捷方法,或它通过函数参数或返回值传递),那么不要释放对象。

 

只有在你长期需要一个对象时,才保存(retain)它。如果你只在一个函数中需要它,你可以安全地不用拥有它。

 

数组,字典等拥有加入其中对象的拥有者(调用retain),所以你在添加时不要显性保存。如果你在添加对象到集合前就拥有对象并且在集合之外不需要它,那么你必须在添加它到集合之后释放它。

 

如果你需要使用大量临时对象(自动释放/使用便捷方法),为避免临时内存过大,应考虑使用临时autorelease pool。在iPhone中如果到达内存上限,程序将退出 – 尽管占用的内存不再需要时将会在autorelease pool中等待释放,但你仍然要有效地定期分配和释放内存。

 

更详细的信息,请参考官方内存管理文档。
developer.apple.com/documentation/Cocoa/Conceptual/MemoryMgmt/MemoryMgmt.html

 

原文见:Memory Management with Objective C / Cocoa / iPhone