Advanced Memory Management Programming Guide

261 阅读21分钟

Advanced Memory Management Programming Guide

Memory Management Policy

通过NSObject协议中定义的方法和标准方法命名约定的组合, 可以提供用于引用计数环境中的内存管理的基本模型. NSObject类还定义了一个dealloc方法,该方法在对象被释放时自动调用. 本文介绍了在Cocoa程序中正确管理内存所需的所有基本规则, 并提供了一些正确用法的示例

Basic Memory Management Rules

内存管理模型基于对象所有权. 任何对象可能具有一个或多个所有者. 只要一个对象至少具有一个所有者它就会继续存在. 如果对象没有所有者则运行时系统会自动销毁它. 为了确保清楚拥有和不拥有对象的所有权, Cocoa设置了以下策略

  • 自己拥有自己创建的任何对象
    • 使用以alloc, new, copy, mutableCopy开头的方法创建对象
    • 例如 alloc, newObject, mutableCopy
  • 可以使用retain来保留对象
    • 通常可以保证接收到的对象在接收该对象的方法中保持有效, 并且该方法还可以安全地将该对象返回给其调用者
    • 在两种情况下使用retain
      • 在实现访问器方法或init方法时, 要获取要存储的对象的所有权作为属性值
      • 防止对象由于其他操作的副作用而失效
  • 当不再需要它时, 必须放弃对拥有的对象的所有权
    • 通过发送对象release消息或autorelease消息来放弃对象的所有权. 因此,在Cocoa术语中放弃对象的所有权通常称为"释放"对象
  • 不能释放没有拥有所有权的对象
    • 这只是先前明确规定的策略规则的推论

A Simple Example

为了说明该策略请思考以下代码片段

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

Person对象实例创建调用了alloc方法, 不在使用的时候调用了release. person实例的name没有使用任何拥有方法, 所以不需要发送release消息. 请注意该示例使用release而不是autorelease

Use autorelease to Send a Deferred release

使用autorelease来达到延迟release, 通常在一个方法返回一个对象的时候. 示例如下

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

使用alloc创建则可以拥有string的所有权. 为了遵守内存管理规则, 必须放弃对字符串的所有权然后再丢失对该字符串的引用. 如果使用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方法的调用方拥有返回的字符串, 因此调用方没有理由释放返回的字符串, 因此它将被泄露.

You Don’t Own Objects Returned by Reference

Cocoa 中的某些方法指定通过引用返回对象(例如用ClassName **或者id *做参数的). 常见的模式是使用NSError对象, 该对象包含有关错误(如果发生错误)的信息. 在以下示例中, 在这些情况下适用与已描述相同的规则. 调用这些方法中的任何一个时不会创建 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];

Implement dealloc to Relinquish Ownership of Objects

NSObject定义了dealloc方法, 当对象没有拥有者并且回收其内存时自动调用, 在Cocoa术语中就是freed或者deallocated. 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
  • 不应将系统资源的管理与对象生命周期结合起来
  • 当应用程序终止时,可能不会向对象发送dealloc消息. 由于进程的内存在退出时会自动清除,因此操作系统清理资源比调用所有内存管理方法更为有效

Core Foundation Uses Similar but Different Rules

Core Foundation对象也有类似的内存管理规则(详情查看Memory Management Programming Guide for Core Foundation). 但是CocoaCore Foundation的命名约定不同. 特别是Core Foundation的"创建规则"不适用于返回Objective-C对象的方法

Practical Memory Management

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

Use Accessor Methods to Make Memory Management Easier

如果类具有对象属性, 则必须确保在使用它, 给对象属性赋值的对象不会被销毁. 所以在赋值对象属性, 应该获取赋值对象的所有权, 释放之前赋值对象的所有权

有时可能看起来很乏味或繁琐, 如果始终使用访问器方法, 则内存管理出现问题的机会会大大减少. 如果在整个代码中对实例变量使用retainrelease, 则几乎可以肯定会出错

假定要设置Counter对象的count值

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

property声明了存取器的两个方法, 通常应该要求编译器综合这些方法. 但是了解如何实现它们很有启发性

get方法中, 只需返回合成的实例变量. 不需要使用retainrelease

- (NSNumber *)count {
    return _count;
}

set方法中, 设置新的值的时候为了确保对象不被释放, 需要调用retain获取对象的所有权. 但是要释放之前旧值的所有权. 当设置的新值就是原来的旧值(同一个对象), 为了保证这个对象不会被释放, retain操作要在release操作之前

- (void)setCount:(NSNumber *)newCount {
    [newCount retain]; // 保留新值 引用计数修改操作
    [_count release];  // 释放新值 引用计数修改操作
    // Make the new assignment.
    _count = newCount; // 赋值操作
}

Use Accessor Methods to Set Property Values

假定要实现一个重置counter的方法, 有两种选择. 一是使用alloc创建对象, 保持平衡, 该对象调用release

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

二是使用便利构造方法创建对象, 此时没有对象的所有权, 不需要调用retain, 或者release

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

注意两种方式对象属性赋值的时候都是调用了set方法

以下内容几乎肯定可以在简单的情况下正常工作, 避免使用访问器方法可能很诱人, 但是这样做肯定会在某个阶段导致错误(例如忘记retainrelease, 又或者实例变量的内存管理语义发生更改)

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

注意如果使用了键值观察, 以上实现方式也不符合键值观察的规则

Don’t Use Accessor Methods in Initializer Methods and dealloc

在初始化(initializer)方法和dealloc方法中, 不应该调用存取器方法(getter和setter), init方法示例

- init {
    self = [super init];
    if (self) {
        // alloc开头创建的拥有所有权 retainCount+1
        _count = [[NSNumber alloc] initWithInteger:0]; 
    }
    return self;
}

initWithCount:方法示例

- initWithCount:(NSNumber *)startingCount {
    self = [super init];
    if (self) {
        // copy创建的拥有所有权 retainCount+1
        _count = [startingCount copy]; 
    }
    return self;
}

因为都拥有了赋值对象的所有权了, 在dealloc方法中则要调用release

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

Use Weak References to Avoid Retain Cycles

拥有对象所有权(retain操作)会对对象进行强引用, 对象只有在所有的强引用都释放的时候才会被销毁. 如果两个对象可能具有循环引用, 则可能会产生一个称为保留环的问题, 它们彼此之间有很强的引用关系.

下图图示就有潜在的循环引用问题. Document对象每个页面都有一个Page对象, 每个Page对象都有一个属性, 该属性可以记录它所在的Document, 如果Document对象和Page对象互相都有强引用, 则两者都不会被释放

An illustration of cyclical references

解决循环引用的办法就是使用弱引用. 弱引用是一种非所有权关系, 其中源对象不保留其具有引用的对象.

为了保持对象引用(路径)图完好无损, 在某处必须有强引用(如果只有弱引用, 上图中的page对象和paragraph对象都会因为没有引用者而被释放销毁). 因此Cocoa规定了一项约定Parent对象对children对象强引用持有, children对象则对Parent对象进行弱引用

所以Document对象对Page对象强引用, Page对象对Document对象弱引用.

Cocoa中的弱引用不止于此, 还有table data sources, outline view items, notification observers, and miscellaneous targets and delegates.

需要谨慎地将消息发送到仅持有弱引用的对象. 如果在释放对象后向其发送消息则应用程序将崩溃. 必须对对象何时有效具有明确定义的条件. 在大多数情况下弱引用对象知道另一个对象对其的弱引用, 就像循环引用一样, 并负责在销毁时时通知另一个对象. 例如, 当在通知中心注册对象时, 通知中心会存储对该对象的弱引用, 并在发布适当的通知时向其发送消息. 同样当释放委托delegate对象时, 需要通过向另一个对象发送带有nil参数的setDelegate:消息来删除委托链接. 这些消息通常是通过对象的dealloc方法发送的

Avoid Causing Deallocation of Objects You’re Using

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

此规则偶尔会有例外主要属于以下两类

  1. 从一个基本集合类中删除一个对象时
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject could now be invalid.

从一个基本集合类中删除一个对象后, 它会发送一个release(而不是autorelease)消息. 如果集合是移除对象的唯一拥有者, 当移除的时候, 移除的对象会立即释放

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

某些情况下, 获取某个对象的子对象(对象属性), 直接或者间接的释放了这个对象. 父对象被释放了, 并且父对象是子对象的唯一持有者, 子对象也会被释放.

为了防止这些情况, heisenObject可以执行retain操作, 进行持有, 使用完成之后调用release.

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

Don’t Use dealloc to Manage Scarce Resources

通常不应该在dealloc方法中管理稀缺资源, 例如文件描述符, 网络连接以及缓冲区或缓存. 尤其不要将类设计为想要调用dealloc就调用dealloc. 由于错误或应用程序崩溃, 对dealloc的调用可能会延迟或不执行

相反如果有一个其实例管理稀缺资源的类, 则应设计应用程序, 以使能够知道何时不再需要这些资源, 然后可以告诉实例在何时进行清理.

如果尝试在dealloc之上搭载资源管理则可能会出现问题, 例如:

  • 顺序依赖对象图的拆解
    • 对象图拆解机制本质上是无序的. 尽管可能通常期望并获得特定的顺序, 但是正在引入脆弱性。例如, 如果某个对象意外地autoreleased而不是released, 则拆卸顺序可能会更改, 这可能会导致意外结果.
  • 不回收稀缺资源
    • 内存泄漏是应该修复的错误, 但通常不会立即致命. 但是, 如果在期望释放稀缺资源时没有释放它们, 则可能会遇到更严重的问题. 例如如果应用程序用完了文件描述符, 则用户可能无法保存数据
  • 在错误的线程上执行清除逻辑

Object graph 对象图

在面向对象的程序中, 对象组通过彼此之间的关系(通过直接引用另一个对象或通过一系列中间引用)形成网络. 这些对象组称为对象图/ 对象图可能是大的, 简单的或复杂的. 包含单个字符串对象的数组对象表示一个小的简单对象图. 包含应用程序对象以及对窗口, 菜单及其视图以及其他支持对象的引用的一组对象可以表示一个大型的复杂对象图 有时可能希望将对象图(通常只是应用程序中完整对象图的一部分)转换成可以保存到文件或传输到另一进程或机器然后进行重构的形式.此过程称为存档 一些对象图可能是不完整的, 通常被称为部分对象图. 部分对象图具有占位符对象, 这些占位符对象代表图的边界, 并且可以在以后的阶段中进行填充. 一个示例是一个nib文件, 其中包含文件所有者的占位符

Collections Own the Objects They Contain

将对象添加到集合(例如array, dictionary, set)时,该集合将拥有它的所有权. 当集合移除对象后者集合对象自己释放的时候, 会释放对该对象的所有权. 示例如下

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

在上面的示例中, 没有调用alloc之类方法, 所以不用执行release, 不需要对新的number执行retain操作, 数组在添加的时候会做retain操作

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];
}

在上面的示例中, 需要给allocedNumber发送release消息, 在每次循环中平衡alloc消息, 对象在添加到数组中的时候会执行retain操作, 所以不用担心会被释放

要了解这一点, 假定自己去实现一个集合. 要确保添加进来的对象都不会提前被销毁掉, 所以在对象添加到集合的时候都要做一次retain操作, 为了保持平衡, 移除的时候要做release操作. 所有仍然持有的对象, 应当在自己的dealloc中发送release消息

Ownership Policy Is Implemented Using Retain Counts

所有权策略是通过引用计数来实现的 -- retain count, 每一个对象都有retain count

  • 当创建一个对象的时候, retain count = 1
  • 给对象发送retain消息后, 引用计数加1, retain count += 1
  • 给对象发送release消息后, 引用计数减1, retain count -= 1
  • 给对象发送autorelease消息后, 在当前自动释放池块的末尾, 引用计数减1, retain count -= 1
  • 如果一个对象的引用计数为0, retain count = 0, 则对象销毁被回收

重要 不要去查询对象的retain count是多少, 因为结果通常会误导. 应用中的框架不知在何时何处会引用目标对象, 在调试内存管理问题时, 应确保代码符合所有权规则

Using Autorelease Pool Blocks

自动释放池块提供了一种机制, 可以通过该机制做到(释放对象的所有权, 但可以避免将其立即释放的可能性例如从方法返回对象时). 一般是不用创建自动释放池的. 但是在某些情况下, 使用自动释放池可以达到一些优化

About Autorelease Pool Blocks

使用@autoreleasepool标记自动释放池块

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

在自动释放池块的末尾, 向在该块内接收到自动释放消息的对象发送release消息. 每次向对象发送块中的autorelease消息时, 对象都会收到release消息

自动释放池可以被嵌套

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

Cocoa始终希望代码在自动释放池块中执行, 否则应该自动释放的对象将不会被释放并且应用程序会泄漏内存(如果在自动释放池块之外发送autorelease消息,Cocoa会记录适当的错误消息). AppKitUIKit框架处理自动释放池块中的每个事件循环迭代(例如,鼠标按下事件或轻击). 因此通常不必自己创建一个自动释放池块, 甚至不必查看用于创建一个自动释放池的代码. 但是在三种情况下可能会使用自己的自动释放池块

  • 编写不基于UI框架的程序例如命令行工具
  • 编写一个创建许多临时对象的循环
    • 可以在循环内使用自动释放池块在下一次迭代之前处理这些对象, 在循环中使用自动释放池块有助于减少应用程序的最大内存占用
  • 如果生成辅助线程/次线程
    • 必须在线程开始执行后立即创建自己的自动释放池块, 否则应用程序可能存在内存泄漏

Use Local Autorelease Pool Blocks to Reduce Peak Memory Footprint

许多程序会创建自动释放的临时对象. 这些对象会增加程序的内存占用量直到块结束为止. 在许多情况下, 允许临时对象累积到当前事件循环迭代结束之前不会导致过多的开销. 但是在某些情况下, 可能会创建大量临时对象, 这些对象会显着增加内存占用量, 并且希望更快地对其进行处理. 在后一种情况下, 可以创建自己的自动释放池块. 在块的最后释放临时对象, 从而减少了程序的内存占用量.

以下示例显示了如何在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循环一次处理一个文件. 在自动释放池块内发送自动释放消息的任何对象(例如fileContents)都在该块的末尾释放

在自动释放池块之后, 应该将在该块内自动释放的任何对象都视为"已处置", 不要向该对象发送消息或将其返回给方法的调用者. 如果必须在自动释放池块之外使用临时对象,则可以通过向该块内的对象发送retain消息, 然后在该块之后将其自动释放, 来发送此消息. 如示例所示

– (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. */
}

Autorelease Pool Blocks and Threads

Cocoa应用程序中的每个线程都维护自己的自动释放池块堆栈. 如果正在编写Foundation-only的程序或分离线程, 则需要创建自己的自动释放池块

如果应用程序或线程是长期存在的, 并且有可能生成许多自动释放的对象, 则应使用自动释放池块(例如在主线程上使用AppKit和UIKit), 否则自动释放的对象会堆积, 并且内存占用也会增加. 如果分离线程未进行Cocoa调用, 则无需使用自动释放池块

Effective Objective 2.0 内存管理介绍

ARC

由于ARC会自动执行retain, release, autorelease, dealloc等操作, 所以直接调用这些内存管理方法是非法的.

ARC调用这些方法时, 并不是通过Objective-C的消息派发机制, 而是直接调用其C语言版本

ARC包含运行时组件. ARC会运用运行时来做一些优化.

_myPerson = [EOCPerson personWithName:@"Bob Smith"];

调用personWithName:方法返回一个EOCPerson对象, 在返回对象之前, 此方法会为其调用autorelease方法. 由于实例变量是个强引用, 所以编译器在为其赋值的时候还要做一次保留操作, 等效于下面代码

EOCPerson *tmp = [EOCPerson personWithName:@"Bob Smith"];
[tmp retain];
_myPerson = tmp;

此时可以看出来autoreleaseretain都是多余的, 为提升性能, 可将二者删除. 但是在ARC环境编译代码时, 要考虑向后兼容, 考虑那些不是同ARC的代码. 这样的操作破会兼容性.

不过ARC可以运用运行时检测到这一多余操作, 为了优化代码可以执行一些特殊的函数

+ (EOCPerson *)personWithName:(NSString* )name {
    EOCPerson *person = [[EOCPerson alloc] init];
    person.name = name;
    objc_autoreleaseReturnValue(person);
}


EOCPerson *tmp = [EOCPerson personWithName:@"Bob Smith"];
_myPerson = objc_retainAutoreleaseReturnValue(tmp);

id objc_autoreleaseReturnValue(id object) {
    if (/* caller will retain object */) {
        set_flag(object);
        return object; ///< No Autorelease
    } else {
        return [object autorelease];
    }
}

id objc_retainAutoreleaseReturnValue(id object) {
    if (get_flag(object)) {
        clear_flag(object);
        return object; ///< No Retain
    } else {
        return [object retain];
    }
}

objc_autoreleaseReturnValue此函数会检测当前方法执行之后将要执行的那段代码, 若发现要执行的代码要对返回的对象做retain操作, 则设置全局数据结构中的一个标志位(数据结构的内容因处理器而异). 如果返回的对象之后执行的代码不进行retain操作, 则调用autorelease. 之后的代码在赋值的时候, 则调用objc_retainAutoreleaseReturnValue进行赋值, 则会检查标志位, 作相应的操作. 检测标志位要比调用autorelease, retain

objc_autoreleaseReturnValue函数如何检测接下来执行的代码是否要执行retain操作取决于处理器, 只有编译器的作者才能实现此函数.

将内存管理交由编译器和运行时组件来做, 可使得代码得到多种优化. 上面所述只是其中的一种.

ARC下变量的内存管理语义

在应用程序中可用下列修饰符来改变局部变量与实例变量的语义

  • __strong 默认语义, 保留此值 retain count +1
  • __unsafe_unretained 不保留此值 retain count不变, 不安全, 再次使用对象的时候可能已经释放
  • __weak 不保留此值 retain count不变, 变量可以安全使用, 如果系统把对象内存给回收了, 变量也会自动清空
  • __autoreleaseing 把对象"按引用传递"给方法时(常见NSError做参数, error:(NSError** )error), 使用这个特殊的修饰符, 此值在方法返回自动释放

ARC下清理实例变量

MRC下的dealloc

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

使用ARC之后, 不需要再编写这种dealloc方法了, 因为ARC会借助Objective-C++的一项特性来生成清理例程. 回收Objective-C++对象的时候, 待回收对象会调用所有C++对象的析构函数, 编译器如果发现了某个对象含有C++对象, 就会生成.cxx_destruct的方法. 而ARC借助此特性, 在此方法中生成清理内存所需的代码.

不过非Objective-C对象, 比如CoreFoundation中的对象是由malloc分配的, 那么仍然需要自己进行清理

理解如有错误 望指正 转载请说明出处