动态内存管理

内存管理的必要性

当程序运行结束时,操作系统会释放为期分配的内存。如果是很小、运行时间很短的程序,就算内存没有释放也不会有问题,程序结束以后操作系统就会自动释放内存。但对于长时间运行的程序,如果没能妥善管理内存,导致需要释放的内存没有释放,反而不断分配新的内存,这样程序所占用的内存将持续升高,程序运行速度将越来越慢,甚至会因为内存耗尽而崩溃。
我们将程序未能释放的不再使用的内存的行为称为内存泄漏,而如果指针所指向的对象已经被释放或收回,该指针就被称为悬垂指针或野指针。继续使用这种指针同样会导致内存崩溃。
Objective-C会通过向类对象发送alloc消息来生成实例对象,alloc的作用就是分配内存。alloc方法的返回值是id类型,id类型实则就是指针类型,而其所指向的就是为实例对象分配的内存。生成的实例对象用完之后不被释放,就会发生内存泄漏。另一方面,如果给已经释放了的实例对象发消息,就有可能导致程序错误或异常终止。

引用计数、自动引用计数和自动垃圾回收

Cocoa环境的Objective-C提供了一种动态的内存管理模式,称为引用计数(reference counter)。这种方式会跟踪每个对象被引用的次数,当对象的引用次数为0时,系统就会释放这个对象所占用的内存。
比引用计数内存管理更高级一点的就是自动引用计数(Automatic Reference Counting, 简写为ARC)的内存管理。自动引用计数提供了自动评估对象生存期的功能,在编译期间会自动加入合适的内存管理的方法。
除了ARC外,Objective-C2.0还引入了另外一种自动内存管理机制——垃圾回收。使用垃圾回收时,就不再需要通过引用计数来管理创建的对象,系统会自动识别哪些对象仍在使用,哪些对象可以回收。

内存管理方式 难易度 Mac iOS 备注
手动引用计数 较难 支持|支持|默认方式
自动引用计数 容易 支持 支持 建议采用
垃圾回收 容易 支持 不支持

手动引用计数内存管理

引用计数

每个对象都有一个与之相关的整数,称作它的引用计数。当某段代码需要使用一个对象时,就将该对象的引用计数器值加1.当这段代码不再使用这个对象时,则将对象的引用计数器值减1.换言之,引用计数就是指程序中到底有多少个地方需要访问这个对象
使用alloc和初始化方法创建一个对象时,该对象的引用计数的初始值为1,假设有一个类A在尽心某些操作的过程中需要使用到实例B,为了防止实例B被别的对象随意释放,类A会事先给实例B发送一个retain消息。这样,每执行一次retain,实例B的引用计数就会加1.
反之,不再需要某个对象时,可以通过发送release消息,使对象的引用计数减1.
实际上,释放内存的并不是release方法,而是dealloc方法。同alloc不同,dealloc不是类方法,而是一个实例方法。当对象的引用计数值为0时,Objective-C会自动向对象发送一条dealloc消息来释放内存。通常并不允许在程序内直接调用dealloc方法。
让我们通过一个例子来看看引用计数到底如何工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import <Foundation/NSObject.h>
#import <stdio.h>

int main();
{
id obj = [[NSObject alloc] init];
printf("init: %d\n", (int)[obj retainCount]);
[obj retain];
printf("retain: %d\n", (int)[obj retainCount]);
[obj retain];
printf("retain: %d\n", (int)[obj retainCount]);

[obj release];
printf("release: %d\n", (int)[obj retainCount]);
[obj release];
printf("release: %d\n", (int)[obj retainCount]);
[obj release];

return 0;
}

释放对象的方法

在自定义类的时候,如果类的实例变量是一个对象类型,那么,在销毁类的对象的时候,也要给类的实例变量发送release消息。释放一个类的实例对象时,为了彻底释放该实例对象所保持的所有对象的所有权,需要为该重写dealloc方法,在其中释放所有已经分配的资源

1
2
3
4
5
- (void) dealloc {
// 这里通过release方法放弃子类中所有实例变量的所有权
// 其他用于释放前的最后操作也写在这里
[super dealloc];
}

访问方法和对象所有权

在通过访问方法等改变拥有实例变量所有权的对象时,必须注意实例变量引用计数的变化。合理安排release和retain的先后顺序。

1
2
3
4
- (void)setMyValue: (id)obj {
[myValue release];
myValue = [obj retain];
}

这么写绝大多数情况下都没有问题,只有一种情况下会有问题,就是当obj和myValue指向同一个对象时,率先release 就会导致对象被释放掉。

1
2
3
4
5
- (void)setMyValue: (id)obj {
[obj retain];
[myValue release];
myValue = obj;
}

自动释放

在实际编程的时候,会遇到很多只用一次就不再使用的对象。而这种对象需要逐一释放,那将是一件很麻烦的事情。
Cocoa环境的Objective-C提供了一种对象自动释放(autorelease)的机制。这种机制的基本思想是把所有需要发送release消息的对象记录下来,等到需要释放这些对象时,会给这些对象一起发送release消息。其中,类NSAutoreleasePool(自动释放池)就起到了记录的作用。
首先我们生成了一个NSAutoreleasePool的实例对象。当像一个对象发送autorelease消息时,实际上就会将该对象添加到NSAutoreleasePool中,将它标记为以后释放。这个时候,因为这个对象没有被释放,所以还可以继续使用。但是当自动释放池被销毁时,池中记录的所有对象都会被发送release消息。这种在自动释放池里记录的对象也被称为临时对象

1
2
3
4
5
6
7
8
9
10
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

// 创建一个对象并发送 autorelease 消息
NSString *autoReleasedString = [[NSString stringWithFormat:@"Hello, World!"] autorelease];

// 在这里可以继续使用 autoReleasedString
// ...

// 当池被销毁时,autoReleasedString 会自动接收到 release 消息
[pool release];

需要注意的是,在ARC环境下,需要使用drain消息来销毁自动释放池

临时对象的生成

除了这种标准的创建对象的方法外,还有一种创建临时对象的方法。通过这种方法创建的对象都是临时对象,生成之后会被直接加入到内部的自动释放池,你不需要关心如何销毁它
例如,Cocoa里用于处理字符串的类NSString,由UTF-8编码的C风格字符串生成NSString对象的方法有两个

1
2
3
4
- (id) initWithUTF8String: (const char *) bytes
// alloc生成的实例对象的初始化方法,生成的实例对象的初始化引用计数为1
+ (id) stringWithUTF8String: (const char *) bytes
// 生成临时变量的类方法,生成的实例对象会被自动加入到自动释放池中

Objective-C中的很多类都提供这种生成临时对象的类方法。这种类方法的命名规则是,不以init开头,而以要生成的对象类型作为开头。
这种生成临时对象的类方法,在Objective-C中称为便利构造函数便利构造器

常量对象

内存中常量对象(类对象,常量字符串对象等)的空间分配与其他对象不同,他们没有引用计数机制,永远不能释放这些对象。给这些对象发送retainCount后,返回的是NSUIntegerMax(最大的无符号数)
常量对象的生成和释放操作和一般对象有所不同,有时需要重写retain和release方法的实现。但考虑到ARC和垃圾回收中无法重写这些方法,因此,从兼容性的角度来看,不建议重写retain和release方法。

ARC概要

什么是ARC

ARC(Automatic Reference Counting,自动引用计数) 是一个编译期计数,利用此技术可以简化Objective-C在内存管理方面的工作量。ARC通过在编译期间添加合适的retain/release/autorelease等函数,来确保对象被正确的释放。编译器会根据传入的变量是局部变量还是引用变量,返回对象的方法是不是初始化方法等信息来推断应当在何处加入retain/release/autorelease等函数。ARC只能管理Objective-C的对象,不能管理通过malloc申请的内存
下面我们通过例子来理解ARC是如何工作的

1
s = w;

赋值操作之后,s原来指向的对象不能通过s来访问,s需要放弃该对象的所有权。与此同时,s需要获得新复制的对象w的所有权。编译以上代码的时候,ARC相当于生成了以下代码

1
2
3
4
[w retain];
id _old = s;
s = w;
[_old release];

使用ARC需要注意的

在ARC有效的程序里,不能调用以下这些跟引用计数相关的方法

  • retain
  • release
  • autorelease
  • retainCount
    当然也不能使用这些函数的selector(例如@selector(retain)
    同时ARC中禁止使用NSAutoReleasePool,而是使用新语法@autoreleasepool来管理自动释放池
    1
    2
    3
    4
    @autoreleasepool {
    // 在次进行一些列操作
    // 可以使用break、return、goto等语句
    }
    另外@autoreleasepool在非ARC模式下也能使用,且效率比使用NSAutoReleasePool性能更好

在ARC中,未指定初始值的变量(包括局部变量)都会被初始化为nil。
但是对于用_autoreleasing)_unsafe_unretained修饰的变量来说,初始值是未定的。
而对象以外的变量的初值则和以前一样(为0或NULL)。

在手动管理内粗你的情况下,当变量的引用计数为0的时候,dealloc就会被调用并释放内存。
在ARC中,对象被释放的时候,dealloc函数也会被调用,你可以通过dealloc函数释放一些资源,但是不要释放实例变量,也不要调用父类的dealloc,因为这些编译器都会自动处理。如果你在dealloc函数中调用了[super dealloc],就会出现编译错误(error: ARC forbids explicit message send of 'dealloc')

如果想启用ARC编译代码,就不能使用gcc而需要使用clang,且需要在编译选项中加上-fobjc-arc,与此想法,可以使用 -fno-obj-arc来明确告诉编译器不需要使用ARC

方法族

采用引用计数方式管理内存时,创建对象时就会拥有这个对象的所有权。例如,使用以alloc开头的类方法生成对象,并且使用以init开头的类方法来初始化对象的时候,就会获得这个对象的所有权。另外,使用名称中包含new、copy或utableCopy的方法复制对象的时候,也会获得这个对象的所有权。
采用引用计数方式管理内存时,如果不使用alloc/init/new/copy/mutableCopy这些方法、或者不使用retain来保留一个对象,就不能成为对象的所有者。另外,只有使用release或autorelease,才能够放弃这个对象的所有权。
这些规定被叫做所有权策略(ownership policy).

由于ARC允许混合链接手动内存管理和自动内存管理的代码,所以为了使编译器能明确哪些方法跟对象的生成有关,所有可用于对象生成的方法集合叫做方法族
一个方法要属于某个方法族,除了需要满足返回值和方法的类别方面的要求外,也需要满足以下命名规则,即选择器同方法族名相同(开头的_下划线可以忽略),或选择器的名字由方法族名加上非小写字母开头的字符串构成。
Objective-C目前一共定义了5个方法族:

  • alloc方法族:
    以alloc开头的方法表示调用者对被创建的对象拥有所有权,返回的对象必须是可以被retain的
  • copy方法族:
    以copy开头的方法表示调用者对被创建的对象拥有所有权,返沪爹对象必须是可以retain的
  • mutableCopy方法族:
    以mutableCopy开头的方法表示调用者对被创建的对象拥有所有权,返回的对象必须是可以被retain的
  • new方法族:
    以new开头的方法表示调用者对被创建的对象拥有所有权,返回的对象必须是可以被retain的
  • init方法族:
    以init开头的方法必须被定义为实例方法,它一定要返回id类型或父、子的指针类型

除了以init开头的方法之外,以alloc/new/copy/mutableCopy开头的方法都既可以是类方法也可以是实例方法。依照Objective-C中的命令习惯,调用以alloc/new/copy/mutableCopy/init开头的方法时,需要将对象所有权返回给调用端,由调用端release生成的对象。另一方面,如果你想由调用端来release一个方法返回的对象,而这个方法的名字又不是以上关键字开头的话,ARC就有可能不会释放这个对象,从而造成内存泄漏。
所以必须严守内存管理相关的函数命名规则

循环引用和弱引用

循环引用

让我以一个简单的类People为例来说明一下什么是循环引用。为了便于理解,这里没有使用ARC而是使用了手动内存管理的方法,但两者的原理是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface People : NSObject {
id friend;
}
- (void) setFriend: (id)obj;
@end

@implementation People
- (void)setFriend:(id)obj {
id tmp = friend;
[obj retain];
friend = obj;
[tmp release];
}
- (void)dealloc {
[friend release];
[super dealloc];
}

[A setFriend: B];
[B setFriend: A];

在这中情况下,A持有B,B又持有A,只有等B释放掉才能释放掉A,否则A的引用计数至少为1。同理,只有释放掉A,才能释放掉B。当双方都在等待对方释放的时候,就形成了循环引用,结果就是两个对象都得不到释放,从而造成内存泄漏。虽然可以额通过设定A的friend为nil,手动打破A和B的循环引用,但是这种做法比较麻烦,而且容易出错。

弱引用

到目前为止,之前提到过的ARC中的实例变量都是拥有所有权的实例变量(强引用类型,默认)。但为了避免循环引用,ARC中引入了弱引用(weak reference)的概念。弱引用是通过存储一个指向那个对象的指针创建的,且不保留对象。Objetive-C中有用__weak修饰符来定义弱引用

1
__weak NSObject *obj;

声明也可以加上__strong来现实声明该变量是强引用类型。
在弱引用情况下,无论是变量赋值还是解除引用,变量的引用计数都不会改变。强引用和弱引用都会被隐式地初始化为nil。
这种用于修饰指针类型变量的修饰符被叫做生命周期修饰符或所有权修饰符。生命周期修饰符一共有四种:__weak__strong__autoreleasing__unsafe_unretained
弱引用会在其指向的实例对象呗释放后自动变成nil,这就是弱引用的自动nil化功能。

ARC编程时其他一些注意事项

像通常指针一样使用的对象

使用ARC的时候,如果既不想保持赋值的对象,也不想赋值的对象在释放后自动设置为nil,可以使用生命周期修饰符__unsafe_unretained__unsafe_unretained所修饰的变量称为非nil化的弱指针,也就是说,如果所指向的内存区域被释放了,这个指针就是一个野指针。
和C语言中的指针一样,在没有被初始化的情况下,__unsafe_unretained类型的变量的初始值时不确定的,释放之后变量的指向也是不确定。
还有一点要注意的事,手动内存管理时也有可能在赋值时没对变量进行retain操作,这就相当于在ARC中使用_weak__unsafe_unretained修饰了变量的行为。因此i,将这种代码迁移到ARC环境的时候一定要小心,需要重新考虑所有权方面的问题。
有一些类的实例不能使用自动化nil的弱引用(NSTextView、NSFont、NSImage等)。如果属性变量是这些类的实例,可以使用__unsafe_unretained来代替_weak,但要注意这类的变量是危险的,因为当其指向的对象被释放的时候,指针不会被自动置为空,它会变成野指针。
使用ARC的程序中,id类型和void*类型之间不能进行转换。就算加了__unsafe_unretained修饰符,转换操作在编译时也会报错。这是因为,iOS世界中主要有两种对象:Objective-C对象和Core Foundation对象。其中,Core Foundation类型的对象不在ARC的管理范畴内。因此,当转换这种类型是,就需要告诉编译器怎么处理对象的所有权。为了解决这一问题,可以使用__bridge修饰符来实现id类型与void*类型的相互转换。

通过函数的参数返回结果对象

当一个函数或方法有多个返回值时,我们可以通过函数或方法的参数传入一个指针,将返回值写入指针所指向的空间。C语言中把这种方法叫做按引用传递 。Objective-C的ARC中也有类似的方法,但采用了和C语言不同的实现方式,叫做写回传
写回传经常被用于当一个方法在处理过程中出现错误时, 通过指向NSError的二重指针返回错误的种类和原因。
ARC的编译器会自动为函数的二重指针变量加上__autoreleasing修饰符。__autoreleasing的根本目的是获得一个延迟释放的对象。只可以把nil或临时变量的指针用于写回传,不可以把静态变量的指针、数组首地址的指针或内部变量的指针用于写回传。
但是有时候我们定义一个函数参数为二重指针类型,并不用于写回传,如果我们不需要ARC编译器自动增加__autoreleasing修饰符,可以采用通过添加其他修饰符的方式。

ARC对结构体的一些限制

ARC有效的情况下,不可以在C语言的结构体(或共用体)中定义Objective-C的对象。原因是编译器不能自动释放结构体内部的Objective-C的对象。一种比较常见的方法是使用Objective-C中的类来代替结构体。但是如果非要使用结构体,可以使用__unsafe_unretained修饰符来修饰结构体中的Objective-C变量。这样一来,编译器就不会管理这个变量的内存,所以需要完全手动管理内存。

1
2
3
4
5
6
static struct {
const __unsafe_unretained NSString *name;
int age;
} initialData[] = {
{@"Laura", 17}, {@"Donna", 20},
};

提示编译器进行特别处理

未使用ARC的时候你可能没有按照ARC中的命名规则来为方法起名,而这种情况下因为某种元英无法修改这些方法的名字,那么将这些代码迁移到ARC环境中就会有问题。这时可以通过给方法加上事先定义好的一些宏来告诉编译器应该如何对这个方法的返回值进行内存管理。不过这种情况主要是为了应对ARC和手动引用计数内存管理混合编程的需求,否则直接使用ARC进行内存管理就行了。
NS_RETURENS_RETAINED:
指明这个方法和init或copy开通的方法一样,由调用端负责释放返回的对象。
NS_RETURNS_NOT_RETAINED:
指明这个方法不属于内存管理方面的方法,调用端无需释放返回的对象。

宏的声明需要放置在方法的末尾

1
+ (FishingDate *)newMoon NS_RETURNS_NOT_RETAINED;

但需要注意的是,还是应该尽可能的修改方法名以使其符合方法族的命名规则,实在无法更改的情况下才考虑使用这种方式。
编译时,这些宏会被替换为注释。宏被定义在NSObjCRuntime.h文件中。可以使用条件编译来区分ARC是否有效,编译器可以按条件编译程序中的不同部分,生成不同的目标文件。区分ARC是否有效的编译条件是#if(__has_feature(objc_ar)),结果为真则表明ARC有效