【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。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值