9 内存管理

9.1 对象生命周期

说明:4个过程

  1. 诞生:通过allocnew方法实现
  2. 生存:接收消息并执行操作
  3. 交友:通过复合以及向方法传递参数
  4. 死去:被释放掉

9.1.1 引用计数

说明:也叫保留计数,每个对象都有一个与之想关联的整数,被称作它的引用计数器保留计数器

相关操作 说明 相关方法
引用计数器初始化 被设置为1 allocnewcopy
引用计数器+1 新增对象引用时 retain
引用计数器-1 引用生命周期结束或引用被断开时 release
销毁对象 释放掉已经分配的全部相关资源 dealloc

retain实例方法

说明:增加对象的引用计数器的值。
注意:Objective-C 会在需要的时候自动调用它。
原型:id

1
2
3
4
/**
* @return {id} 接收消息的对象
*/

- (id) retain;

release实例方法

说明:减少对象的引用计数器的值。
注意:Objective-C 会在需要的时候自动调用它。
技巧:因为retain方法返回一个id类型的值,可以在接收其他消息的同时进行retain调用,增加对象的引用计数器的值并执行其他操作。
原型:id

1
- (oneway void) release;

retainCount实例方法

说明:获取对象的引用计数器的值。
原型:id

1
2
3
4
/**
* @return {NSUInteger} 引用计数器当前值
*/

- (NSUInteger) retaonCount;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#import <Foundation/Foundation.h>
@interface RetainTracker : NSObject
@end // RetainTracker

@implementation RetainTracker
- (id)init
{
if(self = [super init])
{
NSLog (@"init: Retain count of %lu.", [self retainCount]);
}

return (self);
} // init

// 重写dealloc,当引用计数器的值为0时将被自动调用
- (void) dealloc
{
NSLog (@"dealloc called. Bye Bye.");
[super dealloc];

} // dealloc

@end // RetainTracker

int main(int argc, const char * argv[])
{
RetainTracker *tracker = [RetainTracker new];
// count: 1

[tracker retain]; // count: 2
NSLog (@"%lu", [tracker retainCount]);

[tracker retain]; // count: 3
NSLog (@"%lu", [tracker retainCount]);

[tracker release]; // count: 2
NSLog (@"%lu", [tracker retainCount]);

[tracker release]; // count: 1
NSLog (@"%lu", [tracker retainCount]);

[tracker retain]; // count 2
NSLog (@"%lu", [tracker retainCount]);

[tracker release]; // count 1
NSLog (@"%lu", [tracker retainCount]);

[tracker release]; // count: 0, dealloc it

return (0);
}

9.1.2 对象所有权

说明:如果一个对象(或函数)内有指向其他对象的实例变量,则称该对象(或函数)拥有这些对象,并负责确保对其拥有的对象进行清理。

9.1.3 访问方法中的保留和释放

说明:setEngine方法的第一个内存管理版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#import <Foundation/Foundation.h>
@class Engine;
@interface Car : NSObject
{
Engine *engine;
}
- (Engine *) engine;
- (void) setEngine: (Engine *) newEngine;

@end // Car


@implementation Car

- (id) init
{
if (self = [super init]) {
engine = [Engine new];
}
return (self);
} // init

- (Engine *) engine
{
return (engine);
} // engine


- (void) setEngine: (Engine *) newEngine
{
// newEngin引用计数器加1
[newEngin retain];
// engine应用计数器减1
[engine release];
engine = newEngine;
} // setEngine

@end

9.1.4 自动释放

说明:有些情况下,拥有对象的实体并不能负责清理拥有的对象,如下

1
2
3
4
5
6
// 不能在description中释放对象,因为先释放decription字符串对象再返回它,则保留计数器的值归0,对象马上被销毁。
- (NSString *)description {
NSString *description;
description = [[NSString alloc] initWithFormat: @"I'm %d years old", 4];
return (description);
}

解决办法:不够优雅

1
2
3
4
5
6
// 将返回的字符串赋在某个变量中
NSString *desc = [someObject description];
// 使用这个字符串
NSSLog(@"%@", desc);
// 销毁它
[desc release];

9.1.5 所有对象放入池中

关键字:@autoreleasepoolNSAutoreleasePool
说明:自动释放池是一个用来存放对象的池子(集合),并且能够自动释放。当自动释放池被销毁时,会想该池中所有的对象发送release消息。

autorelease实例方法

说明:当向一个对象发送autorelease消息时,实际上是将该对象添加到自动释放池中。
原型:id

1
2
3
4
/**
* @return {id} 接收对象
*/

- (id) autorelease;
1
2
3
4
5
6
- (NSString *)description {
NSString *description;
description = [[NSString alloc] initWithFormat: @"I'm %d years old", 4];
// 通过autorelease方法将字符串加入到自动释放池
return ([description autorelease]);
}
1
2
// NSLog函数的代码运行结束以后,自动释放池会被自动销毁(假设上下文存在已经创建好的自动释放池)
NSLog(@"%@", [someObject description]);

9.1.6 自动释放池的创建和销毁

说明:自动释放池应该什么时候创建?什么时候销毁?

9.1.6.1 创建

说明:有两种方式

自动释放池的创建 说明 备注
@autorelease{} {}内的代码都会被放入这个新池子中 定义在{}内的变量在外部无法使用
NSAutoreleasePool对象 创建和释放NSAutoreleasePool对象之间的代码会使用这个新的池子 性能不如@autorelease{}方式

扩展:可以使用drain方法清空自定释放池中的对象而不销毁自动释放池(Mac OS 10.4+)

9.1.6.2 销毁

说明:使用AppKit时,Cocoa定期自动地为你创建和销毁自动释放池,通常是在程序处理完当前事件(如鼠标单击或者键盘按下)以后执行这些操作。

9.1.7 自动释放池的工作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#import <Foundation/Foundation.h>

@interface RetainTracker : NSObject
@end // RetainTracker

@implementation RetainTracker

- (id) init
{
if (self = [super init])
{
NSLog (@"init: Retain count of %lu.", [self retainCount]);
}

return (self);
} // init


- (void) dealloc
{
NSLog (@"dealloc called. Bye Bye.");
[super dealloc];
} // dealloc

@end // RetainTracker

int main(int argc, const char * argv[])
{
/* 方式一:NSAutoreleasePool对象 */
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

RetainTracker *tracker = [RetainTracker new]; // count: 1

[tracker retain]; // count: 2
[tracker autorelease]; // count: still 2
[tracker release]; // count: 1

NSLog (@"releasing pool");
[pool release];// 销毁自动释放池
// gets nuked, sends release to tracker

/* 方式二:@autorelease代码块 */
@autoreleasepool
{
RetainTracker *tracker2 = [RetainTracker new]; // count: 1

[tracker2 retain]; // count: 2
[tracker2 autorelease]; // count: still 2
[tracker2 release]; // count: 1

NSLog (@"auto releasing pool");
}

return (0);
}

9.2 Cocoa 的内存管理规则

说明:Cocoa有一些内存管理约定,它们都是一些很简单的规则,可用用于整个工具集內。

  • 如果使用newalloccopy方法创建了一个对象:当不再使用该对象时,应该向该对象发送一条releaseautorelease消息
  • 如果通过其他方法获得一个对象:如果对象的保留计数器的值为1,而且已经被设置为自动释放,那么你不需要执任何操作来确保对象的到清理,除非打算在一段时间内拥有该对象,则需要保留它并确保在操作完成时释放它。
  • 如果保留了某个对象:需要(最终)释放自动释放该对象(即保持retain方法和release方法使用次数相等)。
    技巧:以上规则可以归结为下表(两个维度:对象的来历?用完就销毁还是保留?)
对象的来历 用完就销毁 和拥有者同命
allocnewcopy 不在使用时释放对象 dealloc方法中释放对象
其它方法 不需要执行任何操作 获得对象时保留,在dealloc方法中释放对象

9.2.1 临时对象

说明:临时对象指的是,在代码中使用某个对象,但是并未打算长期拥有该对象。
内存管理:如果是用newalloccopy方法获得这个对象,就需要安排好该对象的内存释放(通常使用release)。
alloc

1
2
3
4
5
6
7
8
9
// 维度1:通过alloc创建可变数组
// 维度2:临时使用
KSMutableArray *array;
array = [[NSMutableArray alloc] init];
// 使用array
// ...

// 不使用时销毁
[array release];

其它方法

1
2
3
4
5
6
// 维度1:其它方法(工厂方法arrayWithCapacity已经将该对象的引用计数器设置为1且设置了自动释放,即已经放在了自动释放池中)
// 维度2:临时使用
NSMutableArray *array;
array = [NSMutableArray arrayWithCapacity: 17];
// 使用array
// ...

其它方法(全局单例)

1
2
3
NSColor *color;
// blueColor方法返回一个全局单例对象,这个对象永远不会被销毁,也不需要手动销毁
color = [NSColor blueColor];

9.2.2 拥有对象

说明:拥有对象指的是希望在多个代码中一直拥有某个对象,比如

  • 将对象放进集合中(NSArrayNSDictionary等)
  • 作为其它对象的实例变量使用
  • 作为全局变量使用(比较罕见)

内存管理:9.2-技巧

  • 使用newalloccopy方法获得一个对象:只需保证在拥有者的dealloc方法中释放它
  • 其它方法获得一个对象:获得后保留该对象,并保证在拥有者的dealloc方法中释放它
    new、实例变量
1
2
3
4
5
6
7
8
9
10
11
- (void) doStuff {
// 通过new获得一个对象并赋值给实例变量
flonkArray = [NSMutableArray new];// count 1, autoreleased
[flonkArray retain];// count 2, autoreleased
}
- (void) dealloc {
// 释放实例变量指向的空间
[flonkArray release];// count 0
// 重写了dealloc别忘记调用超类的dealloc
[super dealloc];
}

其它方法、实例变量

1
2
3
4
5
6
7
8
9
10
- (void) doStuff {
// 通过new获得一个对象并赋值给实例变量
flonkArray = [NSMutableArray arrayWithCapacity: 17];// count 1
}
- (void) dealloc {
// 释放实例变量指向的空间
[flonkArray release];// count 0
// 重写了dealloc别忘记调用超类的dealloc
[super dealloc];
}

清理自动释放池

说明:自动释放池被清理的时间是完全确定的

  • 在代码中手动销毁
  • 使用AppKit时是在循环结束时销毁

原理:自动释放池存放在栈中,新建的自动释放池被添加到栈顶,接收autorelease消息的对象将被放入最顶端的自动释放池。
注意:自动释放池的分配和销毁操作代价很小,如果一个循环中会创建大量对象,可以创建在循环中常见自己的自动释放池,每创建一批就释放一批。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建自动释放池
NSAutoreleasePool *pool;
pool = [[NSAutoreleasePool alloc] init];
int i;
for (i = 0; i < 1000000; i++) {
// 创建字符串对象
id object = [someArray objectAtIndex: i];
NSString *desc = [object description];
if (i % 1000 == 0) {
// 每创建1000个对象清理到上一个自动释放池
[pool release];
// 并新建一个自动释放池
pool = [[NSAutoreleasePool alloc] init];
}
}
// 释放最后一个自动释放池
[pool release];

9.2.3 垃圾回收

说明:Objective-C2.0引入了自动内存管理机制,也称为垃圾回收
触发:类似自动释放池

  • 在时间循环结束时触发
  • 也可以自己触发(如果不是GUI程序)

开启:垃圾回收是一个可选择的是否启动的功能(项目信息窗口->Build Settings选项卡->Require[-fobjc-gc-only]选项)
限制:只支持OS X应用开发,无法在iOS应用程序上应用。
扩展:苹果对iOS开发的一些建议

  • 不要在自己的代码中使用autorelease方法
  • 不要使用会返回自动释放对象的一些便利方法(比如NSString中以stringWith开头的工厂方法)

9.2.4 自动引用计数

背景:iOS不支持垃圾回收,因为移动设备比电脑更加私人化、资源更少,垃圾回收存在潜在的体验问题。但苹果提供了另外一个方案来祢补,那就是自动引用计数(automatic regerence counting, ARC)
说明:ARC不是垃圾回收器,它是在编译期(而不是运行期)工作的,它在代码中插入了合适的retainrelease语句。
注意:ARC是一个可选的功能,必需明确地启用或禁用。
开发环境限制:以下是编写(或运行)ARC代码所需的条件

  • Xcode4.2 以上的版本
  • Apple LLVM 3.0 以上版本的编译器
  • OS X 10.7以上版本的系统

运行环境限制:以下是运行移动设备必需满足的条件

  • ios 4.0以上的移动设备或OS X10.6以上版本的64位系统的电脑
  • 归零弱引用需要iOS 5.0OS X 10.7以上版本的系统

作用对象限制:ARC只对可保留的对象指针(ROPs)有效

  • 代码块指针
  • Objective-C对象指针
  • 通过_attribute((NSObject))类型定义的指针

技巧:如果想在代码中使用ARC,必需满足以下三个条件

  • 能够确定哪些对象需要进行内存管理
  • 能够表明如何去管理对象:就是说必需能够对某个对象的引用计数器的值进行加1和减1的操作(NSObject的子类都可以)
  • 有可行的办法传递对象的所有权(在调用者和接受者之间)

注意:如果使用的指针不支持ARC,那么你不得不亲自手动管理它们。

弱引用

说明:通常变量和Objective-C对象之间都是强引用,可以通过对属性使用了assign特性声明为弱引用
用途:处理保留循环(retain cycle)带来的内存泄漏。
限制:弱引用指向的对象有可能被提前释放,直接使用会导致问题。
Alt text

归零弱引用

说明:在指向的对象被释放之后,这种弱引用就会被设置为nil,然后就可以像平常的指针一样被处理了。
语法:

方式 说明 备注
_weak关键字 声明变量时使用_weak修饰 _weak NSString *myString;
@property(weak) 对属性使用weak特性 @property(weak) NSString *myString;
_unsafe_unretained关键字和unsafe_unretained特性 告诉ARC这个特殊的引用是弱引用 前提是:不支持弱引用、使用了ARC

兼容性:iOS 5+OS X 10.7+
属性命名限制:使用ARC的时候,有两种命名规则

  • 属性名称不能以new开头
  • 属性不能只有一个read-only而没有内存管理特性(除非启用了ARC功能)

注意:内存管理的关键字特性是不能一起使用的,两者相互排斥。
扩展:强引用也有自己的_strong关键字和strong特性

将已有的项目转换成支持ARC

前提:必须确保垃圾回收机制没有启动(垃圾回收ARC是无法一同使用的)。
说明:ARC默认是启动了的,如果一个项目没有是在没有启动ARC的情况下开发的,可以转换(通过Edit->Convert->To Objective-C ARC…)

桥接转换

指针分两类:ROPnon-ROP

分类 说明 是否被ARC(启用了的话)管理
可保留对象指针(ROP) NSobject的所有子类
不可保留对象指针(non-ROP) C语言集合类型

拥有者权限:这个概念只有在启用了ARC的情况下才有意义。指的是ROPnon-ROP相互转换时指针所有权情况,用来告诉ARC如何工作

用途:通过桥接转换,可以在转换类型的同时控制拥有者权限
说明:有3种类型的桥接转换

关键字 语法 对象的保留计数器 备注
_bridge non-ROP变量=(__bridge)ROP变量;ROP变量=(__bridge)non-ROP变量; 不变化 指针的所有权仍会留在原变量
_bridge_restained non-ROP变量=(__bridge_restained)ROP变量; 加1
_bridge_transfer ROP变量=(__bridge_transfer)non-ROP变量; 减1

技巧:

  • 结构体(struct)和集合体(union)不能使用ROP作为成员。可以通过使用void *桥接转换来解决这个问题
  • 有时需要释放不支持ARC的对象或执行其他清理操作,所以仍要实现dealloc方法,但是不能直接调用[super dealloc]

注意:ARC中的代码存在如下限制

内存管理方法 不能调用 不能重写
retain - [x] - [x]
retainCount - [x] - [x]
release - [x] - [x]
autorelease - [x] - [x]
dealloc - [x]
1
2
3
4
5
6
7
8
9
10
11
12
// ROP
NSString *theString = @"Learn Objective-C";
// 结构体
struct {
int32_t foo;
char *bar;
NSString *baz;
} MyStruct;
// 转成C语言提供的类型(void *),并指定所有权
MyStruct.baz = (_bridge_restained void *)theString;
// non-ROP 转 ROP
NSString *myString = (_bridge_transfer NSString *)MyStruct.baz;

9.3 异常

说明:异常就是异常事件,比如数组溢出,如果捕捉处理,就会痰乱程序流程。

  • 异常对象:Cocoa中使用NSException类来表示异常,可以创建NSException子类作为自己的异常(如果通过其它类型的对象来抛出异常Cocoa不会处理它们)
  • 抛出异常:在运行时系统中创建并处理异常的行为
  • 捕捉异常:处理被抛出的异常的行为

注意:

  • 如果要项目支持异常特性,要确保-fobj-exceptions(Enable Objecytive-C Exception)项被打开
  • Cocoa框架处理错误的方式通常是退出程序
  • 如果一个异常被抛出但没有被捕捉,程序会在异常断点处停止运行并通知有这个异常

9.3.1 与异常有关的关键字

说明:都以@开头

关键字 作用
@try 定义可能包含异常的代码块
@catch 定义处理已抛出异常的代码块,接收一个参数,通常是NSException或其子类
@finally 定义无论是够有抛出异常都会执行的代码块
@throw 抛出异常

语法:@try-catch-finally

1
2
3
4
5
6
7
8
9
@try {
// ...
}
@catch (NSException *exception) {
// ...
}
@finally {
// ...
}

9.3.2 捕捉不同类型的异常

说明:可以根据需要处理的异常类型过使用多个@catch代码块。处理代码应该按照从具体到抽象的顺序排序,并在最后使用一个通用的处理代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@try {
// ...
}
@catch (myCustomException) {
// ...
}
@catch (NSException *exception) {
// ...
}
@catch (id value) {
// ...
}
@finally {
// ...
}

注意:C语言程序员经常会在异常处理代码中使用setjmplongjmp语句。在@try中则不可以,但可以使用gotoreturn语句退出异常处理代码。

9.3.3 抛出异常

说明:异常的抛出分两种,自动手动,后者有2中方式

手动抛出异常 异常对象类型
@throw 异常对象 id
向某个异常对象发送raise消息 NSException或其子类

注意:@try@catch中都可以抛出异常,后者会引发下一个异常处理调用(@finally会在@throw之前被调用)
扩展:Objective-C的异常机制与C++的异常机制兼容。
性能问题:@try建立异常不会产生消耗,但捕捉异常会消耗大量资源并影响程序运行的速度。

1
2
NSExceptionn *theException = [NSException exceptionWithName: _];
@throw theException;// 或者[theException raise]
1
2
3
4
5
6
7
8
@try {
NSException *e = _;
@throw e;
}
@catch (NSException *e) {
// 可以不指定异常对象(默认重复抛出)
@throw;
}

9.3.4 异常也需要内存管理

说明:如果代码出现了异常,程序会被中断,原本没有内存问题的代码或许会因此出现内存泄漏。

问题描述

1
2
3
4
5
6
7
8
- (void)mySimpleMethod {
// 创建一个字典
NSDictionary *dictionary = [[NSDictionary alloc] initWith_.];
// 对字典进行操作,假设操作中出现了异常,则程序会从方法中跳出寻找异常处理代码
[self processDictionary: dictionary];
// 释放字典:由于方法已经退出来了,所以字典没有释放
[dictionary release];
}

解决方式:@try-@finally

1
2
3
4
5
6
7
8
9
10
11
12
- (void)mySimpleMethod {
// 创建一个字典
NSDictionary *dictionary = [[NSDictionary alloc] initWith_.];
@try {
// 对字典进行操作,假设操作中出现了异常,则程序会从方法中跳出寻找异常处理代码
[self processDictionary: dictionary];
}
@finally {
// 释放字典:由于方法已经退出来了,所以字典没有释放
[dictionary release];
}
}

9.3.5 异常和自动释放池

说明:通常,开发人员并不知道异常对象何时释放,所以异常几乎总是作为自动释放对象创建。
注意:@finally代码块会在@catch中的@throw语句执行之前被调用,因此如果在@finally将自动释放池销毁,那么就会导致僵尸异常

僵尸异常示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)myMechod {
// 自动释放池
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 字典
NSDictionary *myDictionary = [[NSDictionary alloc] initWithObjectsAndKey: @"asdfads", nil];
@try {
// 操作字典
[self processDictionaty: myDictionary];
}
@catch (NSException *e) {
// pool在下面的@throw被调用之前就被释放了,释放池中的异常对象随之被销毁,导致僵尸异常
@throw;
}
@finally {
// 销毁自动释放池
[pool release];
}
}

避免僵尸异常:在自动释放池保留异常对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)myMechod {
id savedException = nil;
// 自动释放池
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 字典
NSDictionary *myDictionary = [[NSDictionary alloc] initWithObjectsAndKey: @"asdfads", nil];
@try {
// 操作字典
[self processDictionaty: myDictionary];
}
@catch (NSException *e) {
// 通过retain方法,将异常对象放入当前池而不是pool中,因为savedException定义的位置在pool定义之前。
savedException = [e retain];
// pool在下面的@throw被调用之前就被释放了
@throw;
}
@finally {
// 销毁自动释放池
[pool release];
}
}

9.4 小结