本文是 「iOS 开发:彻底理解 iOS 内存管理」系列的「MRC 篇」。 用来对 Objective-C 语法中,手动管理内存 MRC 相关知识进行讲解。
程序在运行的过程中,往往涉及到创建对象、定义变量、调用函数或方法,而这些行为都会增加程序的内存占用。
而一个移动设备的内存是有限的,每个软件所能占用的内存也是有限的。
当程序所占用的内存较多时,系统就会发出内存警告,这时就得回收一些不需要再使用的内存空间。比如回收一些不需要再使用的对象、变量等。
如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退现象,影响用户体验。
所以,我们需要对 「内存」 进行合理的分配内存、清除内存,回收不需要再使用的对象。从而保证程序的稳定性。
在 iOS 中,我们通常将内存分为五大部分:
从上边内存的各个部分说明可以看出:只有堆区存放的数据需要由程序员分配和释放。
堆区存放的,主要是继承了 NSObject 的对象,需要由程序员进行分配和释放。其他非对象类型(int、char、float、double、struct、enum 等)则存放在栈区,由系统进行分配和释放。
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10; // 栈
int b = 20; // 栈
// p : 栈
// Person 对象(计数器 == 1): 堆
Person *p = [[Person alloc] init];
}
// 经过上面代码后, 栈里面的变量 a、b、p 都会被回收
// 但是堆里面的 Person 对象还会留在内存中,因为它是计数器依然是 1
return 0;
}
image
移动端的内存管理机制,主要有三种:
其中 iOS 运行环境不支持自动垃圾收集机制(GC)。苹果公司使用的是手工引用计数(MRC)和自动引用计数(ARC)机制。
在自动引用计数(ARC)出现机制之前,一直是通过手工引用计数(MRC)机制这种手写大量管理代码的方式来管理内存。后来苹果公司开发了自动引用计数(ARC)技术,把这部分工作交给了编译器来完成,从而大大简化了开发工作。但是 ARC 依然还是需要注意循环引用的问题。
下面来详细讲解一下「手工引用计数(MRC)」和「自动引用计数(ARC)」。
引用计数器: 一个整数,表示为「对象被引用的次数」。系统需要根据对象的引用计数器来判断对象是否需要被回收。
从字面意义上,可以把引用计数器理解为「对象被引用的次数」,也可以理解为: 「有多少人正在用这个对象」。
系统根据引用计数器的机制来判断对象是否需要被回收。在每次 RunLoop 迭代结束后,都会检查对象的引用计数器,如果引用计数器等于 0,则说明该对象没有地方继续使用它了,可以将其释放掉。
关于「引用计数器」,有以下几个特点:
alloc
、new
或者 copy
创建一个对象时,对象的引用计数器默认就是 1。retain
消息,可以使引用计数器值 +1 ( retain
方法返回对象本身)。release
消息,可以使引用计数器值 -1。retainCount
消息,可以获得当前的引用计数器值。dealloc
消息发起这个过程。release
并不代表销毁 / 回收对象,仅仅是将计数器 -1。int main(int argc, const char * argv[]) {
@autoreleasepool {
// 只要创建一个对象默认引用计数器的值就是 1。
Person *p = [[Person alloc] init];
NSLog(@"retainCount = %lu", [p retainCount]); // 打印 1
// 只要给对象发送一个 retain 消息, 对象的引用计数器就会 +1。
[p retain];
NSLog(@"retainCount = %lu", [p retainCount]); // 打印 2
// 通过指针变量 p,给 p 指向的对象发送一条 release 消息。
// 只要对象接收到 release 消息, 引用计数器就会 -1。
// 只要对象的引用计数器为 0, 系统就会释放对象。
[p release];
// 需要注意的是: release 并不代表销毁 / 回收对象, 仅仅是将计数器 -1。
NSLog(@"retainCount = %lu", [p retainCount]); // 1
[p release]; // 0
NSLog(@"--------");
}
// [p setAge:20]; // 此时对象已经被释放
return 0;
}
dealloc
消息(因此,从 dealloc
方法有没有被调用,就可以判断出对象是否被销毁)dealloc
方法的重写(注意是在 MRC 中) dealloc
方法,在这里释放相关资源,dealloc
就是对象的遗言dealloc
方法,就必须调用 [super dealloc]
,并且放在最后面调用。- (void)dealloc {
NSLog(@"Person dealloc");
// 注意:super dealloc 一定要写到所有代码的最后面
[super dealloc];
}
dealloc
使用注意:
dealloc
方法。int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init]; // 执行完引用计数为 1。
[p release]; // 执行完引用计数为 0,实例对象被释放。
[p release]; // 此时,p 就变成了野指针,再给野指针 p 发送消息就会报错。
[p release]; // 报错
}
return 0;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init]; // 执行完引用计数为 1。
[p release]; // 执行完引用计数为 0,实例对象被释放。
p = nil; // 此时,p 变为了空指针。
[p release]; // 再给空指针 p 发送消息就不会报错了。
[p release];
}
return 0;
}
alloc
、new
、copy
或 mutableCopy
方法创建并持有对象。release
或 autorelease
方法释放对象。id obj = [[NSObject alloc] init]; // 自己创建的对象,自己持有
[obj release];
同样,new
方法也能持有对象:
id obj = [NSObject new]; // 自己创建的对象,自己持有
[obj release];
而由各类实现的 copyWithZone:
方法和 mutableCopyWithZone:
方法将生成并持有对象的副本。
另外,除了上面四种方法之外,由上面四种方法名称开头的方法名,也将生成并持有对象:
allocMyObject
newMyObject
copyMyObject
mutableCopyMyObject
alloc
/ new
/ copy
/ mutableCopy
方法)所取得的的对象,因为非自己生成并持有,所以自己不是该对象的持有者。retain
方法,即便是非自己创建的对象,自己也能持有对象。release
方法来释放对象。id obj = [NSMutableArray array]; // 取得非自己生成的变量,但自己并不持有。
[obj retain]; // 通过 retain 方法持有对象
[obj release];
多个对象之间往往是通过 setter
方法产生联系的,其内存管理的方法也是在 setter
方法、dealloc
方法中实现的。所以只有了解了 setter
方法是如何实现的,我们才能了解到多个对象之间的内存管理思想。接下来我们将从零开始,一步步实现 setter
方法,了解多个对象之间的内存管理思想。
我们用一个线上斗地主游戏例子来类比一下。假如有一款斗地主游戏,游戏大厅有不同的游戏房间,可供玩家选择。我们定义游戏房间为 Room 类对象,定义玩家为 Person 类对象,玩家对象拥有 _room
作为成员变量。
一个玩家对象,如果想要玩游戏,就要持有一个房间对象,并保证在使用房间期间,这个房间对象一直存在,并且在游戏房间没人的时候,还需要将这个房间对象释放。
根据上面的描述,我们可以制定以下规则:
image
下面来定义两个类 玩家类:Person 和 房间类:Room。
#import <Foundation/Foundation.h>
@interface Room : NSObject
@property int no; // 房间号
@end
#import <Foundation/Foundation.h>
#import "Room.h"
@interface Person : NSObject
{
Room *_room;
}
- (void)setRoom:(Room *)room;
- (Room *)room;
@end
现在我们通过几个玩家使用房间的不同应用场景来逐步深入理解内存管理。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1. 创建两个对象
Person *p = [[Person alloc] init]; // 玩家 p
Room *r = [[Room alloc] init]; // 房间 r
r.no = 888; // 房间号赋值
[r release]; // 释放房间
[p release]; // 释放玩家
}
return 0;
}
上述代码执行完第 4~6 行,即:
// 1.创建两个对象
Person *p = [[Person alloc] init]; // 玩家 p
Room *r = [[Room alloc] init]; // 房间 r
r.no = 888; // 房间号赋值
之后在内存中的表现如下图所示:
image
可见,Room 实例对象和 Person 实例对象之间没有相互联系,所以各自释放不会报错。执行完第 8~9 行代码,即:
[r release]; // 释放房间
[p release]; // 释放玩家
后,将房间对象和玩家对象各自释放掉,在内存中的表现如下图所示:
image
最后各自实例对象的内存就会被系统回收。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1. 创建两个对象
Person *p = [[Person alloc] init]; // 玩家 p
Room *r = [[Room alloc] init]; // 房间 r
r.no = 888; // 房间号赋值
// 将房间赋值给玩家,表示玩家在使用房间
// 玩家需要使用这间房,只要玩家在,房间就一定要在
p.room = r; // [p setRoom:r]
[r release]; // 释放房间
// 在这行代码之前,玩家都没有被释放,但是因为玩家还在,那么房间就不能销毁
NSLog(@"-----");
[p release]; // 释放玩家
}
return 0;
}
上边代码执行完第 4~6 行的时候和之前在内存中的表现一样,如图所示:
image
当执行完第 10 行代码 p.room = r;
时,因为调用了 setter
方法,将 Room 实例对象赋值给了 Person 的成员变量,不做其他设置的话,在内存中的表现如下图(做法不对):
image
在调用 setter
方法的时候,因为 Room 实例对象多了一个 Person 对象引用,所以应将 Room 实例对象的引用计数 +1 才对,即 setter
方法应该像下边一样,对 room
进行一次 retain
操作。
- (void)setRoom:(Room *)room { // 调用 room = r;
// 对房间的引用计数器 +1
[room retain];
_room = room;
}
那么执行完第 10 行代码 p.room = r;
,在内存中的表现为:
image
继续执行第 12 行代码[r release];
,释放房间,Room 实例对象引用计数 -1,在内存中的表现如下图所示:
image
然后执行第 17 行代码 [p release];
,释放玩家。这时候因为玩家不在房间里了,房间也没有用了,所以在释放玩家的时候,要把房间也释放掉,也就是在 delloc
里边对房间再进行一次 release
操作。
这样对房间对象来说,每一次 retain
/ alloc
操作都对应一次 release
操作。
- (void)dealloc {
// 人释放了, 那么房间也需要释放
[_room release];
NSLog(@"%s", __func__);
[super dealloc];
}
那么在内存中的表现最终如下图所示:
image
最后实例对象的内存就会被系统回收
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1. 创建两个对象
Person *p = [[Person alloc] init]; // 玩家 p
Room *r = [[Room alloc] init]; // 房间 r
r.no = 888; // 房间号赋值
// 2. 将房间 r 赋值给玩家 p,表示玩家 p 在使用房间 r
p.room = r; // [p setRoom:r]
[r release]; // 释放房间 r
// 3. 换房
Room *r2 = [[Room alloc] init];
r2.no = 444;
p.room = r2;
[r2 release]; // 释放房间 r2
[p release]; // 释放玩家 p
}
return 0;
}
执行下边几行代码:
// 1. 创建两个对象
Person *p = [[Person alloc] init]; // 玩家 p
Room *r = [[Room alloc] init]; // 房间 r
r.no = 888; // 房间号赋值
// 2. 将房间 r 赋值给玩家 p,表示玩家 p 在使用房间 r
p.room = r; // [p setRoom:r]
[r release]; // 释放房间 r
之后的内存表现为:
image
接着执行换房操作而不进行其他操作的话,即:
// 3. 换房
Room *r2 = [[Room alloc] init];
r2.no = 444;
p.room = r2;
后的内存表现为:
image
最后执行完代码:
[r2 release]; // 释放房间 r2
[p release]; // 释放玩家 p
后的内存表现为:
image
可以看出房间 r 并没有被释放,这是因为在进行换房的时候,并没有对房间 r 进行释放。所以应在调用 setter
方法的时候,对之前的变量进行一次 release
操作。具体 setter
方法代码如下:
- (void)setRoom:(Room *)room { // room = r
// 将以前的房间释放掉 -1
[_room release];
// 对房间的引用计数器 +1
[room retain];
_room = room;
}
}
这样在执行完 p.room = r2;
之后就会将 房间 r 释放掉,最终内存表现为:
image
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1. 创建两个对象
Person *p = [[Person alloc] init];
Room *r = [[Room alloc] init];
r.no = 888;
// 2. 将房间 r 赋值给玩家 p
p.room = r; // [p setRoom:r]
[r release]; // 释放房间 r
// 3. 再次使用房间 r
p.room = r;
[r release]; // 释放房间 r
[p release]; // 释放玩家 p
}
return 0;
}
执行下面代码:
// 1.创建两个对象
Person *p = [[Person alloc] init];
Room *r = [[Room alloc] init];
r.no = 888;
// 2.将房间赋值给人
p.room = r; // [p setRoom:r]
[r release]; // 释放房间 r
之后的内存表现为:
image
然后再执行 p.room = r;
,因为 setter
方法会将之前的 Room 实例对象先释放掉,此时内存表现为:
image
此时 _room
、r
已经变成了一个野指针。之后再对野指针 r
发出 retain
消息,程序就会崩溃。所以我们在进行 setter
方法的时候,要先判断一下是否是重复赋值,如果是同一个实例对象,就不需要重复进行 release
和 retain
。换句话说,如果我们使用的还是之前的房间,那换房的时候就不需要对这个房间再进行 release
和 retain
。则 setter
方法具体代码如下:
- (void)setRoom:(Room *)room { // room = r
// 只有房间不同才需用 release 和 retain
if (_room != room) { // 0ffe1 != 0ffe1
// 将以前的房间释放掉 -1
[_room release];
// 对房间的引用计数器+1
[room retain];
_room = room;
}
}
因为 retain
不仅仅会对引用计数器 +1, 而且还会返回当前对象,所以上述代码可最终简化成:
- (void)setRoom:(Room *)room { // room = r
// 只有房间不同才需用 release 和 retain
if (_room != room) { // 0ffe1 != 0ffe1
// 将以前的房间释放掉 -1
[_room release];
_room = [room retain];
}
}
以上就是 setter
方法的终极形式。通过上面多个例子,我们也理解了多个对象之间的内存管理思想。
@property
参数@property
,系统就会自动帮我们生成基本的 setter
/ getter
方法,但是不会生成内存管理相关的代码。@property (nonatomic) int val;
property
后边加上 assign
,系统也不会帮我们生成 setter
方法内存管理的代码,仅仅只会生成普通的 getter
/ setter
方法,默认什么都不写就是 assign
。@property(nonatomic, assign) int val;
property
后边加上 retain
,系统就会自动帮我们生成 getter
/ setter
方法内存管理的代码,但是仍需要我们自己重写 dealloc
方法。@property(nonatomic, retain) Room *room;
当我们不再使用一个对象的时候应该将其空间释放,但是有时候我们不知道何时应该将其释放。为了解决这个问题,Objective-C 提供了 autorelease
方法。
autorelease
是一种支持引用计数的内存管理方式,只要给对象发送一条 autorelease
消息,会将对象放到一个自动释放池中,当自动释放池被销毁时,会对池子里面的「所有对象」做一次 release
操作。注意:这里只是发送
release
消息,如果当时的引用计数(reference-counted)依然不为 0,则该对象依然不会被释放。
autorelease
方法会返回对象本身,且调用完 autorelease
方法后,对象的计数器不变。Person *p = [Person new];
p = [p autorelease];
NSLog(@"count = %lu", [p retainCount]); // 计数还为 1
autorelease
实际上只是把对 release
的调用延迟了,对于每一个 autorelease
,系统只是把该对象放入了当前的 autorelease pool
中,当该 pool
被释放时,该 pool
中的所有对象会被调用 release
方法。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 创建自动释放池
[pool release]; // [pool drain]; 销毁自动释放池
@autoreleasepool
{ // 开始代表创建自动释放池
} // 结束代表销毁自动释放池
NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
Person *p = [[[Person alloc] init] autorelease];
[autoreleasePool drain];
@autoreleasepool
{ // 创建一个自动释放池
Person *p = [[Person new] autorelease];
// 将代码写到这里就放入了自动释放池
} // 销毁自动释放池(会给池子中所有对象发送一条 release 消息)
@autoreleasepool {
// 因为没有调用 autorelease 方法,所以对象没有加入到自动释放池
Person *p = [[Person alloc] init];
[p run];
}
autorelease
不会被加入到自动释放池中 autorelease
是一个方法,只有在自动释放池中调用才有效。@autoreleasepool {
}
// 没有与之对应的自动释放池, 只有在自动释放池中调用autorelease才会放到释放池
Person *p = [[[Person alloc] init] autorelease];
[p run];
// 正确写法
@autoreleasepool {
Person *p = [[[Person alloc] init] autorelease];
}
// 正确写法
Person *p = [[Person alloc] init];
@autoreleasepool {
[p autorelease];
}
autorelease
会将对象放到栈顶的自动释放池。栈顶就是离调用
autorelease
方法最近的自动释放池。
@autoreleasepool { // 栈底自动释放池
@autoreleasepool {
@autoreleasepool { // 栈顶自动释放池
Person *p = [[[Person alloc] init] autorelease];
}
Person *p = [[[Person alloc] init] autorelease];
}
}
@autoreleasepool
之间,这样会造成内存峰值的上升。// 内存暴涨
@autoreleasepool {
for (int i = 0; i < 99999; ++i) {
Person *p = [[[Person alloc] init] autorelease];
}
}
// 内存不会暴涨
for (int i = 0; i < 99999; ++i) {
@autoreleasepool {
Person *p = [[[Person alloc] init] autorelease];
}
}
autorelease
。@autoreleasepool {
// 错误写法, 过度释放
Person *p = [[[[Person alloc] init] autorelease] autorelease];
}
autorelease
后又调用 release
(错误)。@autoreleasepool {
Person *p = [[[Person alloc] init] autorelease];
[p release]; // 错误写法, 过度释放
}
定义两个类 Person 类和 Dog 类
#import <Foundation/Foundation.h>
@class Dog;
@interface Person : NSObject
@property(nonatomic, retain)Dog *dog;
@end
#import <Foundation/Foundation.h>
@class Person;
@interface Dog : NSObject
@property(nonatomic, retain)Person *owner;
@end
执行以下代码:
int main(int argc, const char * argv[]) {
Person *p = [Person new];
Dog *d = [Dog new];
p.dog = d; // retain
d.owner = p; // retain assign
[p release];
[d release];
return 0;
}
就会出现 A 对象要拥有 B 对象,而 B 对应又要拥有 A 对象,此时会形成循环 retain,导致 A 对象和 B 对象永远无法释放。
那么如何解决这个问题呢?