iOS - 老生常谈内存管理(二):从 MRC 提及

前言

  MRC全称Manual Reference Counting,也称为MRRmanual retain-release),手动引用计数内存管理,即开发者须要手动控制对象的引用计数来管理对象的内存。
  在MRC年代,咱们常常须要写retainreleaseautorelease等方法来手动管理对象内存,然而这些方法在ARC是禁止调用的,调用会引发编译报错。
  下面咱们从MRC提及,聊聊iOS内存管理。html

简介

关于内存管理

  应用程序内存管理是在程序运行时分配内存,使用它并在使用完后释放它的过程。编写良好的程序将使用尽量少的内存。在 Objective-C 中,它也能够看做是在许多数据和代码之间分配有限内存资源全部权的一种方式。掌握内存管理知识,咱们就能够很好地管理对象生命周期并在再也不须要它们时释放它们,从而管理应用程序的内存。
  虽然一般在单个对象级别上考虑内存管理,但实际上咱们的目标是管理对象图,要保证在内存中只保留须要用到的对象,确保没有发生内存泄漏。
  下图是苹果官方文档给出的 “内存管理对象图”,很好地展现了一个对象 “建立——持有——释放——销毁” 的过程。 编程

Objective-C 在iOS中提供了两种内存管理方法:数组

  1. MRC,也是本篇文章要讲解的内容,咱们经过跟踪本身持有的对象来显式管理内存。这是使用一个称为 “引用计数” 的模型来实现的,由 Foundation 框架的 NSObject 类与运行时环境一块儿提供。缓存

  2. ARC,系统使用与MRC相同的引用计数系统,可是它会在编译时为咱们插入适当的内存管理方法调用。使用ARC,咱们一般就不须要了解本文章中描述的MRC的内存管理实现,尽管在某些状况下它可能会有所帮助。可是,做为一名合格的iOS开发者,掌握这些知识是颇有必要的。安全

良好的作法可防止与内存相关的问题

  • 不正确的内存管理致使的问题主要有两种:
    ① 释放或覆盖仍在使用的数据
    这会致使内存损坏,而且一般会致使应用程序崩溃,甚至损坏用户数据。
    ② 不释放再也不使用的数据会致使内存泄漏
    内存泄漏是指没有释放已分配的内存,即便它再也不被使用。内存泄漏会致使应用程序不断增长内存使用量,进而可能致使系统性能降低或应用程序被终止。网络

  • 可是,从引用计数的角度考虑内存管理一般会拔苗助长,由于你会倾向于根据实现细节而不是实际目标来考虑内存管理。相反,你应该从对象全部权和对象图的角度考虑内存管理。多线程

  • Cocoa 使用简单的命名约定来指示你什么时候持有由方法返回的对象。(请参阅 《内存管理策略》 章节)app

  • 尽管内存管理基本策略很简单,可是你能够采起一些措施来简化内存管理,并帮助确保程序保持可靠和健壮,同时最大程度地减小其资源需求。(请参阅 《实用内存管理》 章节)框架

  • 自动释放池块提供了一种机制,你能够经过该机制向对象发送 “延迟”release消息。这在须要放弃对象全部权但又但愿避免当即释放对象的状况下颇有用(例如从方法返回对象时)。在某些状况下,你可能会使用本身的自动释放池块。(请参阅 《使用 Autorelease Pool Blocks》 章节)工具

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

为了在编译时发现代码问题,可使用 Xcode 内置的 Clang Static Analyzer。 若是仍然出现内存管理问题,则可使用其余工具和技术来识别和诊断问题。

内存管理策略

NSObject 协议中定义的内存管理方法与遵照这些方法命名约定的自定义方法的组合提供了用于引用计数环境中的内存管理的基本模型。NSObject 类还定义了一个dealloc方法,该方法在对象被销毁时自动调用。

基本内存管理规则

  在MRC下,咱们要严格遵照引用计数内存管理规则。
  内存管理模型基于对象全部权。任何对象均可以拥有一个或多个全部者。只要一个对象至少拥有一个全部者,它就会继续存在。若是对象没有全部者,则运行时系统会自动销毁它。为了确保你清楚本身什么时候拥有和不拥有对象的全部权,Cocoa 设置了如下策略:

四条规则

  • 建立并持有对象
    使用 alloc/new/copy/mutableCopy 等方法(或者以这些方法名开头的方法)建立的对象咱们直接持有,其RC(引用计数,如下使用统一使用RC)初始值为 1,咱们直接使用便可,在不须要使用的时候调用一下release方法进行释放。
id obj = [NSObject alloc] init]; // 建立并持有对象,RC = 1
    /* * 使用该对象,RC = 1 */
    [obj release]; // 在不须要使用的时候调用 release,RC = 0,对象被销毁
复制代码

  若是咱们经过自定义方法 建立并持有对象,则方法名应该以 alloc/new/copy/mutableCopy 开头,且应该遵循驼峰命名法规则,返回的对象也应该由这些方法建立,如:

- (id)allocObject
{
    id obj = [NSObject alloc] init];
    retain obj;
}
复制代码

  能够经过retainCount方法查看对象的引用计数值。

NSLog(@"%ld", [obj retainCount]);
复制代码
  • 可使用 retain 持有对象
    咱们可使用retain对一个对象进行持有。使用上述方法之外的方法建立的对象,咱们并不持有,其RC初始值也为 1。可是须要注意的是,若是要使用(持有)该对象,须要先进行retain,不然可能会致使程序Crash。缘由是这些方法内部是给对象调用了autorelease方法,因此这些对象会被加入到自动释放池中。

  ① 状况一:iOS 程序中不手动指定@autoreleasepool
  当RunLoop迭代结束时,会自动给自动释放池中的对象调用release方法。因此若是咱们使用前不进行retain,若是RunLoop迭代结束,对象调用release方法其RC值就会变成 0,该对象就会被销毁。若是咱们这时候访问已经被销毁的对象,程序就会Crash

/* 正确的用法 */

    id obj = [NSMutableArray array]; // 建立对象但并不持有,对象加入自动释放池,RC = 1

    [obj retain]; // 使用以前进行 retain,对对象进行持有,RC = 2
    /* * 使用该对象,RC = 2 */
    [obj release]; // 在不须要使用的时候调用 release,RC = 1
    /* * RunLoop 可能在某一时刻迭代结束,给自动释放池中的对象调用 release,RC = 0,对象被销毁 * 若是这时候 RunLoop 还未迭代结束,该对象还能够被访问,不过这是很是危险的,容易致使 Crash */
复制代码

  ② 状况二:手动指定@autoreleasepool
  这种状况就更加明显了,若是@autoreleasepool做用域结束,就会自动给autorelease对象调用release方法。若是这时候咱们再访问该对象,程序就会崩溃EXC_BAD_ACCESS

/* 错误的用法 */

    id obj;
    @autoreleasepool {
        obj = [NSMutableArray array]; // 建立对象但并不持有,对象加入自动释放池,RC = 1
    } // @autoreleasepool 做用域结束,对象 release,RC = 0,对象被销毁
    NSLog(@"%@",obj); // EXC_BAD_ACCESS
复制代码
/* 正确的用法 */

    id obj;
    @autoreleasepool {
        obj = [NSMutableArray array]; // 建立对象但并不持有,对象加入自动释放池,RC = 1
        [obj retain]; // RC = 2
    } // @autoreleasepool 做用域结束,对象 release,RC = 1
    NSLog(@"%@",obj); // 正常访问
    /* * 使用该对象,RC = 1 */
    [obj release]; // 在不须要使用的时候调用 release,RC = 0,对象被销毁
复制代码

  若是咱们经过自定义方法 建立但并不持有对象,则方法名就不该该以 alloc/new/copy/mutableCopy 开头,且返回对象前应该要先经过autorelease方法将该对象加入自动释放池。如:

- (id)object
{
    id obj = [NSObject alloc] init];
    [obj autorelease];
    retain obj;
}
复制代码

  这样调用方在使用该方法建立对象的时候,他就会知道他不持有该对象,因而他会在使用该对象前进行retain,并在不须要该对象时进行release

备注releaseautorelease的区别:

  • 调用release,对象的RC会当即 -1;
  • 调用autorelease,对象的RC不会当即 -1,而是将对象添加进自动释放池,它会在一个恰当的时刻自动给对象调用release,因此autorelease至关于延迟了对象的释放。
  • 再也不须要本身持有的对象时释放
    在不须要使用(持有)对象的时候,须要调用一下release或者autorelease方法进行释放(或者称为 “放弃对象使用权”),使其RC-1,防止内存泄漏。当对象的RC为 0 时,就会调用dealloc方法销毁对象。

  • 不能释放非本身持有的对象
    从以上咱们能够得知,持有对象有两种方式,一是经过 alloc/new/copy/mutableCopy 等方法建立对象,二是经过retain方法。若是本身是持有者,那么在不须要该对象的时候须要调用一下release方法进行释放。可是,若是本身不是持有者,就不能对对象进行release,不然会发生程序崩溃EXC_BAD_ACCESS,以下两种状况:

id obj = [[NSObject alloc] init]; // 建立并持有对象,RC = 1
    [obj release]; // 若是本身是持有者,在不须要使用的时候调用 release,RC = 0
    /* * 此时对象已被销毁,不该该再对其进行访问 */
    [obj release]; // EXC_BAD_ACCESS,这时候本身已经不是持有者,再 release 就会 Crash
    /* * 再次 release 已经销毁的对象(过分释放),或是访问已经销毁的对象都会致使崩溃 */
复制代码
id obj = [NSMutableArray array]; // 建立对象,但并不持有对象,RC = 1
    [obj release]; // EXC_BAD_ACCESS 虽然对象的 RC = 1,可是这里并不持有对象,因此致使 Crash
复制代码

  还有一种状况,这是不容易发现问题的状况。下面程序运行竟然不会崩溃?这是为何呢?这里要介绍两个概念,野指针僵尸对象

  • 野指针: 在 C 中是指没有进行初始化的指针,该指针指向一个随机的空间,它的值是个垃圾值;在 OC 中是指指向的对象已经被回收了的指针(网上不少都是这样解释,但我认为它应该叫 “悬垂指针” 才对)。
  • 僵尸对象: 指已经被销毁的对象,但这个对象所占的内存空间尚未分配给别人。

  以下这种状况,当咱们经过野指针去访问僵尸对象的时候,可能会有问题,也可能没有问题。若是僵尸对象所占的空间尚未分配给别人,这时候访问没有问题,若是已经分配给了别人,再次访问就会崩溃。

Person *person = [[Person alloc] init]; // 建立并持有对象,RC = 1
    [person release]; // 若是本身是持有者,在不须要使用的时候调用 release,RC = 0
    [person release]; // 这时候 person 指针为野指针,对象为僵尸对象
    [person release]; // 可能这时候僵尸对象所占的空间尚未分配给别人,因此能够正常访问
复制代码

  以上几个例子均可以用一句话总结:不能释放非本身持有的对象。

以上就是内存管理基本的四条规则,你对照上篇文章中讲的《办公室里的照明问题》,是否是就比较好理解了,你细品,你细细的品!

一个简单的例子

Person 对象是使用alloc方法建立的,所以在不须要该对象时发送一条release消息。

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

使用 autorelease 发送延迟 release

当你须要发送延迟release消息时,可使用autorelease,一般用在从方法返回对象时。例如,你能够像这样实现 fullName 方法:

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

根据内存管理规则,你经过alloc方法建立并持有对象,要在不须要该对象时发送一条release消息。可是若是你在方法中使用release,则return以前就会销毁 NSString 对象,该方法将返回无效对象。使用autorelease,就会延迟release,在 NSString 对象被释放以前返回。

你还能够像这样实现 fullName 方法:

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

根据内存管理规则,你不持有 NSString 对象,所以你不用担忧它的释放,直接return便可。stringWithFormat 方法内部会给 NSString 对象调用autorelease方法。

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

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

在 fullName 方法内部咱们经过alloc方法建立对象并持有,然而并无释放对象。而该方法名不以 alloc/new/copy/mutableCopy 等开头。在调用方看来,经过该方法得到的对象并不持有,所以他会进行retain并在他不须要该对象时release,在他看来这样使用该对象没有内存问题。然而这时候该对象的引用计数为 1,并无销毁,就发生了内存泄漏。

你不持有经过引用返回的对象

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方法,该方法会在一个对象没有全部者(RC=0)而且它的内存被回收时由系统自动调用 —— 在 Cocoa 术语中称为freeddeallocateddealloc方法的做用是销毁对象自身的内存,并释放它持有的任何资源,包括任何实例变量的全部权。

如下举了一个在 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];
}
复制代码

注意:

  • 切勿直接调用另外一个对象dealloc的方法;
  • 你必须在实现结束时调用[super dealloc]
  • 你不该该将系统资源的管理与对象生命周期联系在一块儿,请参阅《不要使用 dealloc 管理稀缺资源》章节;
  • 当应用程序终止时,可能不会向对象发送dealloc消息。由于进程的内存在退出时会自动清除,因此让操做系统清理资源比调用全部对象的dealloc方法更有效。

Core Foundation 使用类似但不一样的规则

Core Foundation 对象有相似的内存管理规则(请参阅 《 Core Foundation 内存管理编程指南》)。可是,Cocoa 和 Core Foundation 的命名约定不一样。特别是 Core Foundation 的建立对象的规则(请参阅 《The Create Rule》)不适用于返回 Objective-C 对象的方法。例如如下的代码片断,你不负责放弃 myInstance 的全部权。由于在 Cocoa 中使用 alloc/new/copy/mutableCopy 等方法(或者以这些方法名开头的方法)建立的对象,咱们才须要对其进行释放。

MyClass * myInstance = [MyClass createInstance];
复制代码

实用内存管理

尽管内存管理基本策略很简单,可是你能够采起一些措施来简化内存管理,并帮助确保程序保持可靠和健壮,同时最大程度地减小其资源需求。

使用访问器方法让内存管理更轻松

若是类中有对象类型的属性,则你必须确保在使用过程当中该属性赋值的对象不被释放。所以,在赋值对象时,你必须持有对象的全部权,让其引用计数加 1。还必需要把当前持有的旧对象的引用计数减 1。

有时它可能看起来很乏味或繁琐,但若是你始终使用访问器方法,那么内存管理出现问题的可能性会大大下降。若是你在整个代码中对实例变量使用retainrelease,这确定是错误的作法。

如下在 Counter 类中定义了一个NSNumber对象属性。

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

@property会自动生成settergetter方法的声明,一般,你应该使用@synthesize让编译器合成方法。但若是咱们了解访问器方法的实现是有益的。

@synthesize会自动生成settergetter方法的实现以及下划线实例变量,详细的解释将在下一篇ARC文章中讲到。

getter方法只须要返回合成的实例变量,因此不用进行retainrelease

- (NSNumber *)count {
    return _count;
}
复制代码

setter方法中,若是其余全部人都遵循相同的规则,那么其余人极可能随时让新对象 newCount 的引用计数减 1,从而致使 newCount 被销毁,因此你必须对其retain使其引用计数加 1。你还必须对旧对象release以放弃对它的持有。因此,先对新对象进行retain,再对旧对象进行release,而后再进行赋值操做。(在Objective-C中容许给nil发送消息,且这样会直接返回不作任何事情。因此就算是第一次调用,_count 变量为nil,对其进行 release也没事。能够参阅《深刻浅出 Runtime(三):消息机制》

注意: 你必须先对新对象进行retain,再对旧对象进行release。顺序颠倒的话,若是新旧对象是同一对象,则可能会发生意外致使对象dealloc

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

以上是苹果官方的作法,该作法在性能上略有不足,若是新旧对象是同一个对象,就存在没必要要的方法调用。

更好的作法以下:先判断新旧对象是不是同一个对象,若是是的话就什么都不作;若是新旧对象不是同一个对象,则对旧对象进行release,对新对象进行retain并赋值给合成的实例变量。

- (void)setCount:(NSNumber *)newCount {
    if (_count != newCount) {
        [_count release];
        _count = [newCount retain];
    }
}
复制代码

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

假设咱们要重置以上count属性的值。有如下两种方法:

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

对于简单的状况,咱们还能够像下面这样直接操做_count变量,但这样作早晚会发生错误(例如,当你忘记retainrelease,或者实例变量的内存管理语义(即属性关键字)发生更改时)。

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

另外请注意,若是使用KVO,则以这种方式更改变量不会触发KVO监听方法。关于KVO我作了比较全面的总结,能够参阅《iOS - 关于 KVO 的一些总结》

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

你不该该在初始化方法和dealloc中使用访问器方法来设置实例变量,而是应该直接操做实例变量。

例如,咱们要在初始化 Counter 对象时,初始化它的count属性。正确的作法以下:

- init {
    self = [super init];
    if (self) {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}
复制代码
- initWithCount:(NSNumber *)startingCount {
    self = [super init];
    if (self) {
        _count = [startingCount copy];
    }
    return self;
}
复制代码

因为 Counter 类具备实例变量,所以还必须实现dealloc方法。在该方法中经过向它们发送release消息来放弃任何实例变量的全部权,并在最后调用super的实现:

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

以上是苹果官方的作法。推荐作法以下,在release以后再对 _count 赋值nil

备注
先解释一下nilrelease的做用:nil是将一个对象的指针置为空,只是切断了指针和内存中对象的联系,并无释放对象内存;而release才是真正释放对象内存的操做。
之因此在release以后再对 _count 赋值nil,是为了防止 _count 在被销毁以后再次被访问而致使Crash

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

咱们也能够在dealloc经过self.count = nil;一步到位,由于一般它至关于[_count release];_count = nil;两步操做。可是苹果说了,不建议咱们在dealloc中使用访问器方法。

- (void)dealloc {
    self.count = nil;
    [super dealloc];
}
复制代码

Why? 为何初始化方法中须要self = [super init]

  • 先大概解释一下selfsuperself是对象指针,指向当前消息接收者。super是编译器指令,使用super调用方法是从当前消息接收者类的父类中开始查找方法的实现,但消息接收者仍是子类。有关selfsuper的详细解释能够参阅《深刻浅出 Runtime(四):super 的本质》
  • 调用[super init],是子类去调用父类的init方法,先完成父类的初始化工做。要注意调用过程当中,父类的init方法中的self仍是子类。
  • 执行self = [super init],若是父类初始化成功,接下来就进行子类的初始化;若是父类初始化失败,则[super init]会返回nil并赋值给self,接下来if (self)语句的内容将不被执行,子类的init方法也返回nil。这样作能够防止由于父类初始化失败而返回了一个不可用的对象。若是你不是这样作,你可能你会获得一个不可用的对象,而且它的行为是不可预测的,最终可能会致使你的程序发生Crash

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

  • 在初始化方法和dealloc中,对象的存在与否还不肯定,它可能还未初始化完毕,因此给对象发消息可能不会成功,或者致使一些问题的发生。
    • 进一步解释,假如咱们在init中使用setter方法初始化实例变量。在init中,咱们会调用self = [super init]对父类的东西先进行初始化,即子类先调用父类的init方法(注意: 调用的父类的init方法中的self仍是子类对象)。若是父类的init中使用setter方法初始化实例变量,且子类重写了该setter方法,那么在初始化父类的时候就会调用子类的setter方法。而此时只是在进行父类的初始化,子类初始化还未完成,因此可能会发生错误。
    • 在销毁子类对象时,首先是调用子类的dealloc,最后调用[super dealloc](这与init相反)。若是在父类的dealloc中调用了setter方法且该方法被子类重写,就会调用到子类的setter方法,但此时子类已经被销毁,因此这也可能会发生错误。
    • 《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》书中的第 31 条 —— 在 dealloc 方法中只释放引用并解除监听 一文中也提到:在 dealloc 里不要调用属性的存取方法,由于有人可能会覆写这些方法,并于其中作一些没法在回收阶段安全执行的操做。此外,属性可能正处于 “键值观测”(Key-Value Observation,KVO)机制的监控之下,该属性的观察者(observer)可能会在属性值改变时 “保留” 或使用这个即将回收的对象。这种作法会令运行期系统的状态彻底失调,从而致使一些莫名其妙的错误。
    • 综上,错误的缘由由继承和子类重写访问器方法引发。在初始化方法和 dealloc 中使用访问器方法的话,若是存在继承且子类重写了访问器方法,且在方法中作了一些其它操做,就颇有可能发生错误。虽然通常状况下咱们可能不会同时知足以上条件而致使错误,可是为了不错误的发生,咱们仍是规范编写代码比较好。
  • 性能降低。特别是,若是属性是atomic的。
  • 可能产生反作用。如使用KVO的话会触发KVO等。

不过,有些状况咱们必须破例。好比:

  • 待初始化的实例变量声明在父类中,而咱们又没法在子类中访问此实例变量的话,那么咱们在初始化方法中只能经过setter来对实例变量赋值。

使用弱引用来避免 Retain Cycles

retain对象会建立对该对象的强引用(即引用计数 +1)。一个对象在release它的全部强引用以后(即引用计数 =0)才会dealloc。若是两个对象相互retain强引用,或者多个对象,每一个对象都强引用下一个对象直到回到第一个,就会出现 “Retain Cycles(循环引用)” 问题。循环引用会致使它们中的任何对象都没法dealloc,就产生了内存泄漏。

举个例子,Document 对象中有一个属性 Page 对象,每一个 Page 对象都有一个属性,用于存储它所在的 Document。若是 Document 对象具备对 Page 对象的强引用,而且 Page 对象具备对 Document 对象的强引用,则它们都不能被销毁。

Retain Cycles” 问题的解决方案是使用弱引用。弱引用是非持有关系,对象do not retain它引用的对象。

MRC中,这里的 “弱引用” 是指do not retain,而不是ARC中的weak

可是,为了保持对象图无缺无损,必须在某处有强引用(若是只有弱引用,则 Page 对象和 Paragraph 对象可能没有任何全部者,所以将被销毁)。所以,Cocoa 创建了一个约定,即父对象应该对其子对象保持强引用(retain),而子对象应该对父对象保持弱引用(do not retain)。

所以,Document 对象具备对其 Page 对象的强引用,但 Page 对象对 Document 对象是弱引用,以下图所示:

Cocoa 中弱引用的示例包括但不限于 table data sources、outline view items、notification observers 以及其余 targets 和 delegates。

当你向只持有弱引用的对象发送消息时,须要当心。若是在对象销毁后向其发送消息就会Crash。你必须定义好何时对象是有效的。在大多数状况下,弱引用对象知道其它对象对它的弱引用,就像循环引用的状况同样,你要负责在弱引用对象销毁时通知其它对象。例如,当你向通知中心注册对象时,通知中心会存储对该对象的弱引用,并在发布相应的通知时向其发送消息。在对象要销毁时,你须要在通知中心注销它,以防止通知中心向已销毁的对象发送消息。一样,当 delegate 对象销毁时,你须要向委托对象发送setDelegate: nil消息来删除 delegate 引用。这些消息一般在对象的 dealloc 方法中发送。

避免致使你正在使用的对象被销毁

Cocoa 的全部权策略指定,对象做为方法参数传入,其在调用的方法的整个范围内保持有效,也能够做为方法的返回值返回,而没必要担忧它被释放。对于应用程序来讲,对象的 getter 方法返回缓存的实例变量或计算值并不重要。重要的是对象在你须要的时间内保持有效。

此规则偶尔会有例外状况,主要分为两类。

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

当一个对象从一个基本集合类中移除时,它将被发送一条release(而不是autorelease)消息。若是集合是移除对象的惟一全部者,则移除的对象(示例中的 heisenObject)将当即被销毁。

  1. 当 “父对象” 被销毁时。
id parent = <#create a parent object#>;
    // ...
    heisenObject = [parent child] ;
    [parent release]; // Or, for example: self.parent = nil;
    // heisenObject could now be invalid.
复制代码

在某些状况下,你经过父对象得到子对象,而后直接或间接release父对象。若是release父对象致使它被销毁,而且父对象是子对象的惟一全部者,则子对象(示例中的 heisenObject)将同时被销毁(假设在父对象的dealloc方法中,子对象被发送一个release而不是一个autorelease消息)。

为了防止这些状况发生,在获得 heisenObject 时retain它,并在完成后release它。例如:

heisenObject = [[array objectAtIndex:n] retain];
    [array removeObjectAtIndex:n];
    // Use heisenObject...
    [heisenObject release];
复制代码

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

你一般不该该在dealloc方法中管理稀缺资源,如文件描述符,网络链接和缓冲区或缓存等。特别是,你不该该设计类,以便在你想让系统调用dealloc时就调用它。因为bug或应用程序崩溃,dealloc的调用可能会被延迟或未调用。

相反,若是你有一个类的实例管理稀缺的资源,你应该在你再也不须要这些资源时让该实例释放这些资源。而后,你一般会release该实例,紧接着它dealloc。若是该实例的dealloc没有被及时调用或者未调用,你也不会遇到稀缺资源不被及时释放或者未释放的问题,由于此前你已经释放了资源。

若是你尝试在dealloc上进行资源管理,则可能会出现问题。例如:

  1. 依赖对象图的释放机制。
    对象图的释放机制本质上是无序的。尽管一般你但愿能够按照特定的顺序释放,可是会让程序变得很脆弱。若是对象被autorelease而不是release,则释放顺序可能会改变,这可能会致使意外的结果。

  2. 不回收稀缺资源。
    内存泄漏是应该被修复的bug,但它们一般不会当即致命。然而,若是在你但愿释放稀缺资源时没有释放,则可能会遇到更严重的问题。例如,若是你的应用程序用完了文件描述符,则用户可能没法保存数据。

  3. 释放资源的操做被错误的线程执行。
    若是一个对象在一个意外的时间调用了autorelease,它将在它碰巧进入的任何一个线程的自动释放池块中被释放。对于只能从一个线程触及的资源来讲,这很容易致命。

集合持有它们包含的对象

将对象添加到集合(例如arraydictionaryset)时,集合将得到对象的全部权。当从集合中移除对象或集合自己被销毁时,集合将放弃对象的全部权。所以,例如,若是要建立一个存储numbers的数组,能够执行如下任一操做:

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

在这种状况下,NSNumber对象不是经过alloc等建立,所以无需调用release。也不须要对NSNumber对象进行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];
    }
复制代码

在这种状况下,你就须要对NSNumber对象进行release。数组会在addObject:时对NSNumber对象进行retain,所以在数组中它不会被销毁。

要理解这一点,能够站在实现集合类的人的角度。你要确保在集合中它们不会被销毁,因此你在它们添加进集合时给它们发送一个retain消息。若是删除了它们,则必须给它们发送一个release消息。在集合的dealloc方法中,应该向集合中全部剩余的对象发送一条release消息。

全部权策略是经过使用 Retain Counts 实现的

全部权策略经过引用计数实现的,引用计数也称为“retain count”。每一个对象都有一个retain count

  • 建立对象时,其retain count为 1。
  • 向对象发送retain消息时,其retain count将 +1。
  • 向对象发送release消息时,其retain count将 -1。
  • 向对象发送autorelease消息时,其retain count在当前自动释放池块结束时 -1。
  • 若是对象的retain count减小到 0,它将dealloc

重要提示: 不该该显式询问对象的retain count是多少。结果每每会产生误导,由于你可能不知道哪些系统框架对象retain了你关注的对象。在调试内存管理问题时,你只须要遵照内存管理规则就好了。

备注: 关于这些方法的具体实现,请参阅《iOS - 老生常谈内存管理(四):源码分析内存管理方法》

使用 Autorelease Pool Blocks

自动释放池块提供了一种机制,让你能够放弃对象的全部权,但避免当即释放它(例如从方法返回对象时)。一般,你不须要建立本身的自动释放池块,但在某些状况下,你必须这样作或者这样作是有益的。

关于 Autorelease Pool Blocks

Autorelease Pool Blocks 使用@autoreleasepool标记,示例以下:

@autoreleasepool {
        // Code that creates autoreleased objects.
    }
复制代码

@autoreleasepool的末尾,在块中接收到autorelease消息的对象将被发送一条release消息。对象在块内每接收一次autorelease消息,就会被发送一条release消息。 与任何其余代码块同样,@autoreleasepool能够嵌套,可是你一般不会这样作。

@autoreleasepool {
        // . . .
        @autoreleasepool {
            // . . .
        }
        . . .
    }
复制代码

MRC下还可使用NSAutoreleasePool建立自动释放池。不过建议使用@autoreleasepool,苹果说它比NSAutoreleasePool快大约六倍。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    // Code benefitting from a local autorelease pool.
    [pool release]; // [pool drain]
复制代码

Cocoa 老是但愿代码在@autoreleasepool中执行,不然autorelease对象不会被release,致使内存泄漏。若是你在@autoreleasepool以外发送autorelease消息,Cocoa 会打印一个合适的错误消息。AppKit 和 UIKit 框架会在RunLoop每次事件循环迭代中建立并处理@autoreleasepool,所以,你一般没必要本身建立@autoreleasepool,甚至不须要知道建立@autoreleasepool的代码怎么写。

可是,有三种状况可能会使用你本身的@autoreleasepool

  • ① 若是你编写的程序不是基于 UI 框架的,好比说命令行工具;
  • ② 若是你编写的循环中建立了大量的临时对象; 你能够在循环内使用@autoreleasepool在每次循环结束时销毁这些对象。这样能够减小应用程序的最大内存占用。
  • ③ 若是你建立了辅助线程。 一旦线程开始执行,就必须建立本身的@autoreleasepool;不然,你的应用程序将存在内存泄漏。(有关详细信息,请参阅《Autorelease Pool Blocks 和线程》章节。

关于@autoreleasepool的底层原理,能够参阅《iOS - 聊聊 autorelease 和 @autoreleasepool》

使用 Local Autorelease Pool Blocks 来减小峰值内存占用量

许多程序建立autorelease的临时对象。这些对象将添加到程序的内存占用空间,直到块结束。在许多状况下,容许临时对象累积直到当前事件循环迭代结束时,而不会致使过多的开销。可是,在某些状况下,你可能会建立大量临时对象,这些对象会大大增长内存占用,而且你但愿更快地销毁这些对象。在这时候,你就能够建立本身的@autoreleasepool。在块结束时,临时对象被release,这可让它们尽快dealloc,从而减小程序的内存占用。

如下示例演示了如何在 for 循环中使用 local autorelease pool block。

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 循环一次处理一个文件。在@autoreleasepool内发送autorelease消息的任何对象(例如 fileContents)在块结束时release

@autoreleasepool以后,你应该将块中任何autorelease对象视为 “已销毁”。不要向该对象发送消息或将其返回给你的方法调用者。若是你须要某个autorelease的临时对象在@autoreleasepool结束以后依然可用,能够经过在块内对该对象发送retain消息,而后在块以后将对其发送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. */
}
复制代码

@autoreleasepool中给match对象发送一条retain消息,并在@autoreleasepool以后给其发送一条autorelease消息,延长了match对象的生命周期,容许它在while循环外接收消息,而且能够返回给findMatchingObject:方法的调用方。

Autorelease Pool Blocks 和线程

Cocoa 应用程序中的每一个线程都维护本身的 autorelease pool blocks 栈。若是你写的是一个仅基于 Foundation 的程序或者若是你使用子线程,则须要建立本身的@autoreleasepool。 若是你的应用程序或线程长期存在而且可能会产生大量的autorelease对象,则应使用@autoreleasepool(如 AppKit 和 UIKit 就在主线程建立了@autoreleasepool);不然,autorelease对象会不断累积,致使你的内存占用量不断增长。若是你在子线程上没有进行 Cocoa 调用,则不须要使用@autoreleasepool

注意: 若是你使用pthreadPOSIX thread)而不是使用NSThread建立子线程,那么你就不能使用 Cocoa 除非 Cocoa 处于多线程模式。Cocoa 只有在detach它的第一个NSThread对象以后才会进入多线程模式。要想在pthread建立的子线程上使用 Cocoa,你的应用程序必须先detach至少一个能够当即退出的NSThread对象。你可使用NSThread的类方法isMultiThreaded测试 Cocoa 是否处于多线程模式。

相关文章
相关标签/搜索