【iOS开发】——MRC(手动内存管理)的一些补充
前言
上学期的时候我总结过关于MRC的一些知识,具体可以看这篇:iOS开发——MRC(手动内存管理)
最近在复习MRC,发现当时总结的时候有几个点没有总结上,今天在写一篇补充记录一下
野指针与空指针
空指针:
- 空指针指的是没有指向存储空间的指针(里面存的是 nil, 也就是 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;
}
野指针:
- 只要一个对象被释放了,我们就称这个对象为「僵尸对象(不能再使用的对象)」。
- 当一个指针指向一个僵尸对象(不能再使用的对象),我们就称这个指针为「野指针」。
- 只要给一个野指针发送消息就会报错(EXC_BAD_ACCESS 错误)。
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;
}
多个对象内存管理的思想
多个对象之间往往是通过setter方法产生联系的,其内存管理的方法也是通过setter、delloc方法实现管理的。接下来我们学习一下setter方法的具体实现过程
我们可以举一个例子来帮助我们理解这个过程:
我记得很早以前,腾讯出过一个叫qq游戏大厅的功能好像,具体记不清了,反正就是有好几种游戏,比如斗地主,我们打麻将需要三个人和一个房间,所以我们可以定义房间为Room类对象,然后定义玩家为Person类对象,玩家对象拥有 _room 作为成员变量。
一个玩家对象,如果想要玩游戏,就要持有一个房间对象,并保证在使用房间期间,这个房间对象一直存在,并且在游戏房间没人的时候,还需要将这个房间对象释放。
那么房间具体的引用情况有哪些呢:
- 只要一个玩家想使用房间(进入房间),就需要对这个游戏房间的引用计数器 +1。
- 只要一个玩家不想再使用房间(离开房间),就需要对这个游戏房间的引用计数器 -1。
- 只要还有至少一个玩家在用某个房间,那么这个游戏房间就不会被回收,引用计数至少为 1。
- 只要没有玩家在房子里了,那么这个房间就会被回收
我们可以看到玩家三个玩家对象都持有房间对象,所以房间对象的引用为3。
我们将刚刚说的两个类对的代码写出来:
Room类:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Room : NSObject
@property int number;
@end
NS_ASSUME_NONNULL_END
Person类:
#import <Foundation/Foundation.h>
#import "Room.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
{
Room *_room;
}
- (void)setRoom:(Room *)room;
- (Room *)room;
@end
NS_ASSUME_NONNULL_END
玩家没有使用过房间
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Room.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
//1.创建两个对象
Room *r = [[Room alloc] init];
Person *p = [[Person alloc] init];
//给房间号赋值
r.number = 808;
//释放两个对象
[r release];
[p release];
}
return 0;
}
我们可以看到在上述代码中Person类创建的对象没有对房间进行持有,也就是玩家虽然创建出来了但是却没有使用过房间,上述代码运行时内存使用情况如图所示:
在这里复习两个知识点:
- 栈:存放基本类型 的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new出来的对象)或者常量池中(字符串常量对象存放的常量池中),局部变量【注意:(方法中的局部变量使用final修饰后,放在堆中,而不是栈中)】
- 堆:存放使用new创建的对象,全局变量
我们在来看上面的例子,通过上图可以发现Room 实例对象和 Person 实例对象之间没有相互联系,所以各自释放不会报错。等两个对象释放以后,内存的情况如图所示:
最后由于引用计数变为0了,各自实例对象的内存就会被系统回收。
一个玩家使用一个游戏房间的情况
在调用 setter 方法的时候,因为 Room 实例对象多了一个 Person 对象引用,所以应将 Room 实例对象的引用计数 +1 才对,即 setter 方法应该像下边一样,对 room 进行一次 retain 操作。
- (void)setRoom:(Room *)room { // 调用 room = r;
// 对房间的引用计数器 +1
[room retain];
_room = room;
}
然后我们在main函数里完成一下一个玩家使用一个游戏房间的情况:
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Room.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Room *r = [[Room alloc] init];
Person *p = [[Person alloc] init];
r.number = 808;
// 将房间赋值给玩家,表示玩家在使用房间
// 玩家需要使用这间房,只要玩家在,房间就一定要在
p.room = r;// [p setRoom:r]
[r release];
// 在这行代码之前,玩家都没有被释放,但是因为玩家还在,那么房间就不能销毁
[p release];
}
return 0;
}
此时我们的内存分配情况就应该为:
其实还是很好理解的,我们主要来理解一下引用计数这部分,Room创建实例对象引用计数➕1,然后Person创建实例对象Person的引用计数也➕1同时Person通过setter方法对Room实例对象进行了持有,所以此时Room的引用计数再➕1变为了2。
然后我们看,Room的实例对象释放了对应的Room的引用计数就要➖1,此时内存的分配情况为:
然后执行代码 [p release];,释放Person实例对象。这时候因为玩家不在房间里了,房间也没有用了,所以在释放玩家的时候,要把房间也释放掉,也就是在 delloc 里边对房间再进行一次 release 操作。
这样对房间对象来说,每一次 retain / alloc 操作都对应一次 release 操作。
- (void)dealloc {
// 人释放了, 那么房间也需要释放
[_room release];
NSLog(@"%s", __func__);
[super dealloc];
}
最终内存情况变为了:
一个玩家使用一个房间 r 后,换到另一个房间 r2 的情况
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Room.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Room *r = [[Room alloc] init];
Person *p = [[Person alloc] init];
r.number = 808;
// 将房间赋值给玩家,表示玩家在使用房间
// 玩家需要使用这间房,只要玩家在,房间就一定要在
p.room = r;
[r release];
Room *r2 = [[Room alloc] init];
r2.number = 404;
p.room = r2;
[r2 release]; // 释放房间 r2
// 在这行代码之前,玩家都没有被释放,但是因为玩家还在,那么房间就不能销毁
[p release];
}
return 0;
}
在第一个Room实例对象释放后,内存情况为:
接着我们进行了第二个房间的创建以及Person实例对象通过setter方法持有第二个Room实例对象。此时我们的内存情况变为了:
在我们执行完所有代码,我们可以发现内存情况变为了:
此时为什么r还持有Room的实例对象呢,原因其实很简单,我们调用了两次Person的setter方法但是只delloc了一次,问题出在哪呢?当r释放的时候,我们的p并没有释放,所以不会调用delloc方法,所以就造成了上述结果,那我们应该怎么办呢?我们可以在调用 setter 方法的时候,对之前的变量进行一次 release 操作。具体 setter 方法代码如下:
- (void)setRoom:(Room *)room { // room = r
// 将以前的房间释放掉 -1
[_room release];
// 对房间的引用计数器 +1
[room retain];
_room = room;
}
}
这样我们在第二次调用setter方法的时候会先将之前通过setter方法增加的引用计数减掉,就不会出现刚刚那种情况了。
所以内存情况就变为了:
一个玩家使用一个房间,不再使用房间,将房间释放掉之后,再次使用该房间的情况
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1. 创建两个对象
Person *p = [[Person alloc] init];
Room *r = [[Room alloc] init];
r.number = 808;
// 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.number = 808;
// 2.将房间赋值给人
p.room = r; // [p setRoom:r]
[r release]; // 释放房间 r
内存情况为:
然后再执行 p.room = r;,因为 setter 方法会将之前的 Room 实例对象先释放掉,所以此时内存表现为:
此时 _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最终的形式,这也是多个对象内存管理的思想。
MRC需要注意的一些知识点
@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;
自动释放池(AutoreleasePool)
以前学MRC的时候了解过一点自动释放池但是没有做过系统的总结,今天总结一下关于自动释放池的一些知识点
当我们不再使用一个对象的时候应该将其空间释放,但是有时候我们不知道何时应该将其释放。为了解决这个问题,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 实际上只是把对 release 的调用延迟了,对于每一个 autorelease,系统只是把该对象放入了当前的 autorelease pool 中,当该 pool 被释放时,该 pool 中的所有对象会被调用 release 方法。
autorelease 的创建方法
- 使用 NSAutoreleasePool 创建
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 创建自动释放池
[pool release]; // [pool drain]; 销毁自动释放池
- 使用 @autoreleasepool 创建
@autoreleasepool
{ // 开始代表创建自动释放池
} // 结束代表销毁自动释放池
autorelease的使用方法
NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
Person *p = [[[Person alloc] init] autorelease];
[autoreleasePool drain];
@autoreleasepool
{ // 创建一个自动释放池
Person *p = [[Person new] autorelease];
// 将代码写到这里就放入了自动释放池
} // 销毁自动释放池(会给池子中所有对象发送一条 release 消息)
autorelease 的注意事项
- 并不是放到自动释放池代码中,都会自动加入到自动释放池
@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 错误用法
- 不要连续调用 autorelease
@autoreleasepool {
// 错误写法, 过度释放
Person *p = [[[[Person alloc] init] autorelease] autorelease];
}
- 调用 autorelease 后又调用 release(错误)
@autoreleasepool {
Person *p = [[[Person alloc] init] autorelease];
[p release]; // 错误写法, 过度释放
}
MRC 中避免循环引用
定义两个类 Person 类和 Dog 类
Person 类:
#import <Foundation/Foundation.h>
@class Dog;
@interface Person : NSObject
@property(nonatomic, retain)Dog *dog;
@end
Dog类:
#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 对象永远无法释放。
那我们应该怎么办呢:
- 不要让 A retain B,B retain A,所以其中一方不要做retain方法
- 当两端互相引用时,应该一端用 retain,一端用 assign。