个性化阅读
专注于IT技术分析

Objective-C内存管理完全解读

关于内存管理

应用程序内存管理是在程序运行时分配内存、使用内存并在使用完内存后释放内存的过程。编写良好的程序使用尽可能少的内存。在Objective-C中,它也可以被看作是在许多数据和代码之间分配有限内存资源所有权的一种方式。完成本指南的学习后,你将掌握管理应用程序内存所需的知识,方法是显式地管理对象的生命周期,并在不再需要对象时释放它们。

虽然内存管理通常是在单个对象级别上考虑的,但是你的目标实际上是管理对象图。我们希望确保内存中没有超出实际需要的对象。

内存管理解释图解

内存管理主要内容

Objective-C提供了两种应用程序内存管理方法。

  • 在本指南中描述的方法(称为“手动保留-释放”或MRR)中,通过跟踪你拥有的对象来显式地管理内存。这是使用基础类NSObject与运行时环境一起提供的模型(称为引用计数)实现的。
  • 在自动引用计数(ARC)中,系统使用与MRR相同的引用计数系统,但它在编译时为你插入适当的内存管理方法调用。强烈建议你在新项目中使用ARC。如果你使用ARC,通常不需要理解本文档中描述的底层实现,尽管在某些情况下它可能是有帮助的。

良好的练习可以防止内存相关的问题

错误的内存管理会导致两种主要的问题:

  • 释放或覆盖仍在使用中的数据
    • 这将导致内存损坏,通常会导致应用程序崩溃,或者更糟,导致用户数据损坏。
  • 不释放不再使用的数据会导致内存泄漏
    • 内存泄漏是指分配的内存没有被释放,即使它再也不会被使用。泄漏会导致应用程序使用越来越多的内存,从而可能导致系统性能低下或应用程序被终止。

但是,从引用计数的角度考虑内存管理通常会适得其反,因为你倾向于从实现细节而不是实际目标的角度考虑内存管理。相反,你应该从对象所有权和对象图的角度考虑内存管理。

Cocoa使用一个简单的命名约定来指示何时拥有一个方法返回的对象。

参见下面的内存管理策略

虽然基本的策略很简单,但是你可以采取一些实际的步骤来简化内存管理,并帮助确保你的程序保持可靠和健壮,同时最小化它的资源需求。

参见下面的实用内存管理

自动释放池块提供了一种机制,你可以通过该机制向对象发送“延迟”释放消息。当你想要放弃一个对象的所有权,但又想要避免它被立即释放的情况下(例如当你从一个方法中返回一个对象时),这是非常有用的。在某些情况下,你可能会使用自己的自动释放池块。

参见使用自动释放池块

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

要在编译时识别代码中的问题,可以使用Xcode中内置的Clang静态分析器。

如果确实出现内存管理问题,你可以使用其他工具和技术来识别和诊断这些问题。

  • 技术说明TN2239、iOS调试魔法中描述了许多工具和技术,特别是使用NSZombie来帮助查找过度释放的对象。
  • 你可以使用工具来跟踪引用计数事件并查找内存泄漏。参见在应用程序上收集数据。

内存管理策略

在引用计数环境中用于内存管理的基本模型是由NSObject协议中定义的方法和标准方法命名约定的组合提供的。NSObject类还定义了一个方法dealloc,它在对象被解除分配时自动调用。本文描述了在Cocoa程序中正确管理内存所需了解的所有基本规则,并提供了一些正确使用的示例。

基本内存管理规则

内存管理模型基于对象所有权。任何对象可以有一个或多个所有者。只要对象至少有一个所有者,它就会继续存在。如果一个对象没有所有者,运行时系统将自动销毁它。为了确保你什么时候有对象,什么时候没有,Cocoa设置了以下策略:

你拥有创建的任何对象

使用名称以“alloc”、“new”、“copy”或“mutableCopy”(例如,alloc、newObject或mutableCopy)开头的方法创建对象。

可以使用retain获得对象的所有权

接收到的对象通常保证在接收到的方法中保持有效,并且该方法也可以安全地将对象返回给调用者。在两种情况下使用retain:(1)在读写方法或init方法的实现中,获取要存储为属性值的对象的所有权;(2)防止一个对象作为其他操作的副作用而失效(解释为避免导致正在使用的对象的回收)。

当你不再需要它时,你必须放弃你所拥有的对象的所有权

通过发送一个release消息或一个autorelease消息,即放弃了对象的所有权。在Cocoa术语中,放弃对象的所有权通常被称为“释放”对象。

你绝不能放弃你并不拥有的对象的所有权

这只是前面明确声明的策略规则的推论。

一个简单的例子

为了说明该策略,请考虑以下代码片段:

{
    Person *aPerson = [[Person alloc] init];
    // ...
    NSString *name = aPerson.fullName;
    // ...
    [aPerson release];
}

Person对象是使用alloc方法创建的,因此当不再需要它时,它随后会发送一个release消息。使用任何拥有的方法都不会检索persion的名称,因此不会发送release消息。但是请注意,这个示例使用的是release而不是autorelease。

使用autorelease发送延迟释放

当你需要发送release消息时(通常是在从方法返回对象时),可以使用autorelease。例如,你可以这样实现fullName方法:

- (NSString *)fullName {
    NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@",
                                          self.firstName, self.lastName] autorelease];
    return string;
}

你拥有由alloc返回的字符串。要遵守内存管理规则,必须在丢失对字符串的引用之前放弃对它的所有权。但是,如果使用release,字符串将在返回之前被释放(该方法将返回一个无效的对象)。使用autorelease,表示希望放弃所有权,但是允许方法的调用者在释放之前使用返回的字符串。

你也可以像这样实现fullName方法:

- (NSString *)fullName {
    NSString *string = [NSString stringWithFormat:@"%@ %@",
                                 self.firstName, self.lastName];
    return string;
}

根据基本规则,你不拥有stringWithFormat:返回的字符串,因此可以安全地从方法返回字符串。

相比之下,下面的实现是错误的:

- (NSString *)fullName {
    NSString *string = [[NSString alloc] initWithFormat:@"%@ %@",
                                         self.firstName, self.lastName];
    return string;
}

根据命名约定,不需要表示fullName方法的调用者拥有返回的字符串。因此,调用者没有理由释放返回的字符串,因此它将被泄漏。

不拥有通过引用返回的对象

Cocoa中的一些方法指定通过引用返回一个对象(也就是说,它们采用ClassName **或id *类型的参数)。一种常见的模式是使用一个NSError对象,该对象包含发生错误时有关错误的信息,如initWithContentsOfURL:options:error: (NSData)和initWithContentsOfFile:encoding:error: (NSString)所示。

在这些情况下,适用的规则与前面描述的相同。当你调用这些方法中的任何一个时,你不会创建NSError对象,因此你不拥有它。因此没有必要释放它,如下例所示:

NSString *fileName = <#Get a file name#>;
NSError *error;
NSString *string = [[NSString alloc] initWithContentsOfFile:fileName
                        encoding:NSUTF8StringEncoding error:&error];
if (string == nil) {
    // Deal with error...
}
// ...
[string release];

实现dealloc来放弃对象的所有权

NSObject类定义了一个方法dealloc,当一个对象没有所有者并且它的内存被回收时,这个方法会被自动调用——在Cocoa术语中,它是“释放的”或者“释放的”。dealloc方法的作用是释放对象自身的内存,并处理它持有的任何资源,包括对象实例变量的所有权。

下面的例子演示了如何为Person类实现dealloc方法:

@interface Person : NSObject
@property (retain) NSString *firstName;
@property (retain) NSString *lastName;
@property (assign, readonly) NSString *fullName;
@end
 
@implementation Person
// ...
- (void)dealloc
    [_firstName release];
    [_lastName release];
    [super dealloc];
}
@end

重要:永远不要直接调用另一个对象的dealloc方法。

你必须在实现结束时调用超类的实现。

你不应该将系统资源的管理与对象生存期联系起来;不要使用dealloc来管理稀缺资源。

当应用程序终止时,对象可能不会被发送dealloc消息。因为进程的内存在退出时被自动清除,所以允许操作系统清理资源比调用所有内存管理方法更有效。

Core Foundation使用类似但不同的规则

Core Foundation对象也有类似的内存管理规则(参见Core Foundation的内存管理编程指南)。然而,Cocoa和Core Foundation的命名约定是不同的。特别是Core Foundation的Create规则(参见Create规则)并不适用于返回Objective-C对象的方法。例如,在下面的代码片段中,你不需要负责放弃myInstance的所有权:

MyClass *myInstance = [MyClass createInstance];

实用内存管理

尽管内存管理策略中描述的基本概念很简单,但是你可以采取一些实际的步骤来简化内存管理,并帮助确保你的程序保持可靠和健壮,同时最小化它的资源需求。

使用访问器方法使内存管理更容易

如果你的类有一个属性是一个对象,你必须确保在你使用它的时候任何被设置为这个值的对象都不会被释放。因此,你必须在设置对象时声明对象的所有权。你还必须确保随后放弃当前持有的任何值的所有权。

有时候,它可能看起来很单调乏味,但如果始终如一地使用访问器方法,内存管理出现问题的几率就会大大降低。如果在整个代码中对实例变量使用retain和release,那么几乎可以肯定是在做错误的事情。

考虑一个你想要设置其计数的计数器对象。

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

属性声明了两个访问器方法。通常,你应该要求编译器来合成这些方法;但是,看看如何实现它们是有意义的。

在“get”访问器中,你只需返回合成的实例变量,因此不需要retain或release:

- (NSNumber *)count {
    return _count;
}

在“set”方法中,如果其他所有人都在遵循相同的规则,则必须假定可以随时处置新计数,因此必须通过发送retian消息来获取对象的所有权。 你还必须在此处通过发送release消息来放弃旧计数对象的所有权。 (在Objective-C中允许将消息发送到nil,因此,如果尚未设置_count,该实现仍将起作用。)如果这两个对象是同一对象,则必须在[newCount keep]之后发送此消息-如果你不这样做, 想要无意间使它被释放。

- (void)setCount:(NSNumber *)newCount {
    [newCount retain];
    [_count release];
    // Make the new assignment.
    _count = newCount;
}

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

假设你想实现一个重置计数器的方法。你有几个选择。第一个实现使用alloc创建NSNumber实例,因此你可以用一个release来平衡它。

- (void)reset {
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [self setCount:zero];
    [zero release];
}

第二个使用一个便利的构造函数来创建一个新的NSNumber对象。因此,不需要retain或release消息

- (void)reset {
    NSNumber *zero = [NSNumber numberWithInteger:0];
    [self setCount:zero];
}

注意,两者都使用set访问器方法。

以下几乎肯定会在简单的情况下正常工作,但随着诱人,因为它可能会避开访问器方法,这样做几乎肯定会导致一个错误在某个阶段(例如,当你忘记保留或释放,或者实例变量的内存管理语义变化)。

- (void)reset {
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [_count release];
    _count = zero;
}

还要注意,如果你使用KVO,那么以这种方式更改变量是不符合KVO的。

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

只有在初始化方法和dealloc中不应该使用访问器方法来设置实例变量。要用表示0的数字对象初始化计数器对象,可以实现以下init方法:

- init {
    self = [super init];
    if (self) {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}

为了允许使用非零的计数来初始化计数器,你可以实现一个initWithCount:方法,如下所示:

- initWithCount:(NSNumber *)startingCount {
    self = [super init];
    if (self) {
        _count = [startingCount copy];
    }
    return self;
}

由于Counter类有一个对象实例变量,你还必须实现dealloc方法。它应该放弃任何实例变量的所有权,通过发送一个release消息,并最终调用super的实现:

- (void)dealloc {
    [_count release];
    [super dealloc];
}

使用弱引用来避免循环强引用

保留对象将创建对该对象的强引用。在释放对象的所有强引用之前,无法释放该对象。因此,如果两个对象可能有循环引用(即它们彼此有一个强引用),那么就会出现一个称为retain cycle的问题。

图1中显示的对象关系演示了一个潜在的retain循环。文档对象对文档中的每个页面都有一个Page对象。每个Page对象都有一个跟踪其所在文档的属性。如果文档对象有一个指向页面对象的强引用,而页面对象有一个指向文档对象的强引用,那么两个对象都不能被释放。在释放页对象之前,文档的引用计数不能变为零,而在释放文档对象之前,页面对象不会被释放。

图1循环引用的说明

循环强引用问题的解决方案是使用弱引用。弱引用是一种非拥有关系,其中源对象不保留它有引用的对象。

然而,要保持对象图完整,必须在某个地方有强引用(如果只有弱引用,那么页面和段落可能没有任何所有者,因此需要释放)。因此,Cocoa建立了一个约定,即“父”对象应该保持对其“子”的强引用,而子对象应该对其父对象有弱引用

因此,在图1中,文档对象有一个对其页面对象的强引用(retain),而页面对象有一个对文档对象的弱引用(不retain)。

Cocoa中的弱引用示例包括(但不限于)表数据源datasource、大纲视图项、通知观察者以及其他目标和代理delegate。

你需要小心地将消息发送给仅持有弱引用的对象。如果在对象被释放后向其发送消息,则应用程序将崩溃。当对象有效时,必须有定义良好的条件。在大多数情况下,弱引用对象知道另一个对象对它的弱引用,就像循环引用一样,并且负责在释放另一个对象时通知它。例如,当你向通知中心注册一个对象时,通知中心将存储对该对象的弱引用,并在发布适当的通知时向其发送消息。当对象被解除分配时,你需要将其从通知中心注销,以防止通知中心向对象发送任何其他消息,因为对象已不再存在。同样,当委托对象被解除分配时,你需要通过向另一个对象发送带有nil参数的setDelegate: message来删除委托链接。这些消息通常来自对象的dealloc方法。

避免造成正在使用的对象的重新分配

Cocoa的所有权策略指定接收到的对象通常在调用方法的范围内保持有效。还应该能够从当前范围返回接收到的对象,而不必担心它被释放。对于应用程序来说,对象的getter方法是否返回缓存的实例变量或计算值并不重要。重要的是,对象在你需要它的时候保持有效。

这个规则偶尔也有例外,主要分为两类。

1、从基本集合类之一中删除对象时。

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

当一个对象从一个基本集合类中移除时,它将被发送一个release(而不是autorelease)消息。如果集合是被删除对象的唯一所有者,则是立即释放被删除的对象(本例中的heisenObject)。

2、当释放“父对象”时。

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

在某些情况下,你从另一个对象检索一个对象,然后直接或间接地释放父对象。如果释放父进程导致它被释放,而父进程是子进程的唯一所有者,那么子进程(本例中的heisenObject)将同时被释放(假设它是在父进程的dealloc方法中被发送的,而不是自动释放消息)。

为了防止这些情况,你在收到heisenObject时保留它,并在使用完它时释放它。例如:

heisenObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
// Use heisenObject...
[heisenObject release];

不要使用dealloc来管理稀缺资源

通常不应该在dealloc方法中管理稀缺资源,比如文件描述符、网络连接和缓冲区或缓存。特别是,你不应该设计类以便在你认为将调用dealloc时调用它。dealloc的调用可能会被延迟或回避,原因可能是bug,也可能是应用程序崩溃。

相反,如果您有一个类,它的实例管理稀缺资源,那么你应该设计你的应用程序,使你知道什么时候不再需要资源,然后可以告诉实例在那时“清理”。你通常会随后释放实例,dealloc也会随之发送,但是如果它不这样做,你就不会遇到额外的问题。

如果你试图在dealloc之上利用资源管理,可能会出现问题。例如:

1、对象图的顺序依赖关系

对象图的拆卸机制本质上是无序的。尽管您通常可能期望—并得到—一个特定的顺序,但是你正在引入脆弱性。例如,如果一个对象是意外地autorelease而不是release的,那么拆解顺序可能会改变,这可能会导致意外的结果。

2、不回收稀缺资源。

内存泄漏是应该修复的bug,但通常不会立即致命。然而,如果稀有资源没有在你期望它们被释放的时候被释放,你可能会遇到更严重的问题。例如,如果你的应用程序耗尽了文件描述符,用户可能无法保存数据。

3、清除在错误线程上执行的逻辑。

如果一个对象在一个意外的时间被自动释放,它将被释放到它所在的线程的自动释放池块上。对于只能从一个线程访问的资源来说,这可能是致命的。

集合拥有它们所包含的对象

当你将对象添加到集合(例如数组、字典或集合)时,集合拥有该对象的所有权。当对象从集合中移除或集合本身被释放时,集合将放弃所有权。因此,例如,如果你想创建一个数字数组,你可以做以下任何一种:

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

在本例中没有调用alloc,因此不需要调用release。不需要保留新的数字(encenumber),因为数组会这样做。

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

在这种情况下,你需要在for循环的范围内发送allocedNumber一个释放消息来平衡alloc。由于数组在由addObject:添加时保留了编号,所以在数组中它不会被释放。

要理解这一点,请将你自己置于实现collection类的人的位置。你要确保没有任何你要照看的对象从你的脚下消失,所以当它们被传递进来时,你要向它们发送一条retain的消息。如果它们被删除,你必须发送一个对应的释放消息,并且在你自己的dealloc方法期间,任何剩余的对象都应该被发送一个释放消息。

使用Retain count实现所有权策略

所有权策略是通过引用计数实现的——通常在retain方法之后称为“retain count”。每个对象都有一个retain count。

  • 创建对象时,它的retain count为1。
  • 当你向对象发送retain消息时,其retain计数递增1。
  • 当你向对象发送release消息时,其retain count减少1。
  • 当你向对象发送autorelease消息时,其retain count在当前自动释放池块的末尾递减1。
  • 如果一个对象的retain count减少到零,则释放它。

重要:没有理由显式地询问对象它的retain count是什么(参见retainCount)。结果常常是误导,因为你可能不知道什么框架对象保留了你所感兴趣的对象。在调试内存管理问题时,你应该只关心确保代码符合所有权规则。

使用自动释放池块

自动释放池块提供了一种机制,你可以通过这种机制放弃对象的所有权,但避免立即释放(例如从方法返回对象时)。通常,你不需要创建自己的自动释放池块,但是在某些情况下,你必须这样做,或者这样做是有益的。

关于自动释放池块

自动释放池块使用@autoreleasepool标记,如下面的示例所示:

@autoreleasepool {
    // Code that creates autoreleased objects.
}

在autorelease池块的末尾,在块中接收到autorelease消息的对象将被发送一个release消息——每当在块中接收到一个autorelease消息时,对象都会接收一个release消息。

像任何其他代码块,自动释放池块可以嵌套:

@autoreleasepool {
    // . . .
    @autoreleasepool {
        // . . .
    }
    . . .
}

(通常不会看到与上面完全一样的代码;通常,一个源文件中的自动释放池块中的代码会调用另一个源文件中的代码,该源文件包含在另一个自动释放池块中。对于给定的autorelease 消息,在发送autorelease消息的autorelease 块的末尾发送相应的release消息。

Cocoa总是期望代码在一个自动释放池块中执行,否则自动释放的对象不会被释放,你的应用程序会泄漏内存。(如果您在autorelease池块之外发送一个autorelease消息,Cocoa会记录一个适当的错误消息。)AppKit和UIKit框架在一个自动释放池块中处理每个事件循环迭代(如鼠标向下事件或点击)。因此,你通常不必自己创建一个自动释放池块,甚至不必查看用于创建该块的代码。然而,在三种情况下,你可能会使用自己的自动释放池块:

  • 如果你正在编写的程序不基于UI框架,例如命令行工具。
  • 如果你编写一个循环来创建许多临时对象
    • 你可以在循环中使用autorelease池块在下一次迭代之前处理这些对象。在循环中使用自动释放池块有助于减少应用程序的最大内存占用。
  • 如果生成一个辅助线程
    • 一旦线程开始执行,你必须创建自己的自动释放池块;否则,应用程序将泄漏对象。(有关详细信息,请参阅自动释放池块和线程。)

使用本地自动释放池块来减少峰值内存占用

许多程序都创建了自动释放的临时对象。这些对象会增加程序的内存占用,直到块结束。在许多情况下,允许临时对象累积到当前事件循环迭代结束时,不会导致过多的开销;然而,在某些情况下,你可能会创建大量临时对象,这些临时对象会大量增加内存占用,你希望更快地处理它们。在后一种情况下,你可以创建自己的自动释放池块。在块的末尾,临时对象被释放,这通常会导致释放它们,从而减少程序的内存占用。

下面的示例演示如何在for循环中使用本地自动释放池块。

NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {
 
    @autoreleasepool {
        NSError *error;
        NSString *fileContents = [NSString stringWithContentsOfURL:url
                                         encoding:NSUTF8StringEncoding error:&error];
        /* Process the string, creating and autoreleasing more objects. */
    }
}

for循环一次处理一个文件。任何对象(如filecontent) autorelease池内生成自动发送一个消息块释放的块。

autorelease池块后,应该将任何对象生成的块内”处理“不要将消息发送给该对象或返回到调用程序的方法。如果你必须使用一个临时对象除了一个autorelease池块,可以通过发送一个保留的信息块中的对象,然后把它生成块后,如本例中所示:

– (id)findMatchingObject:(id)anObject {
 
    id match;
    while (match == nil) {
        @autoreleasepool {
 
            /* Do a search that creates a lot of temporary objects. */
            match = [self expensiveSearchForObject:anObject];
 
            if (match != nil) {
                [match retain]; /* Keep match around. */
            }
        }
    }
 
    return [match autorelease];   /* Let match go and return it. */
}

在自动释放池块中发送retain到match会阻塞,并在自动释放池块扩展了match的生存期后向它发送autorelease,允许它在循环之外接收消息并返回给findMatchingObject的调用者:。

自动释放池块和线程

Cocoa应用程序中的每个线程都维护自己的自动释放池块堆栈。如果你正在编写一个仅限Foundation的程序,或者正在分离一个线程,那么你需要创建自己的自动释放池块。

如果你的应用程序或线程是长生命周期的,并且可能会生成大量的自动释放对象,那么你应该使用自动释放池块(如在主线程上使用AppKit和UIKit);否则,自动释放的对象会累积,内存占用会增加。如果分离的线程不进行Cocoa调用,则不需要使用自动释放池块。

注意:如果你使用POSIX线程api,而不是NSThread来创建辅助线程,则不能使用Cocoa,除非Cocoa处于多线程模式。Cocoa只有在分离它的第一个NSThread对象后才进入多线程模式。要在次要POSIX线程上使用Cocoa,你的应用程序必须首先分离至少一个NSThread对象,该对象可以立即退出。你可以使用NSThread类方法ismultithreading来测试Cocoa是否处于多线程模式。

赞(0)
未经允许不得转载:srcmini » Objective-C内存管理完全解读

评论 抢沙发

评论前必须登录!