iOS 内存管理MRC

1. 什么是内存管理

  • 程序在运行的过程中通常通过以下行为,来增加程序的的内存占用
    • 创建一个OC对象
    • 定义一个变量
    • 调用一个函数或者方法
  • 而一个移动设备的内存是有限的,每个软件所能占用的内存也是有限的
  • 当程序所占用的内存较多时,系统就会发出内存警告,这时就得回收一些不需要再使用的内存空间。比如回收一些不需要使用的对象、变量等
  • 如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退现象,影响用户体验

所以,我们需要对内存进行合理的分配内存、清除内存,回收那些不需要再使用的对象。从而保证程序的稳定性。

那么,那些对象才需要我们进行内存管理呢?

  • 任何继承了NSObject的对象需要进行内存管理
  • 而其他非对象类型(int、char、float、double、struct、enum等) 不需要进行内存管理

这是因为

  • 继承了NSObject的对象的存储在操作系统的里边。
  • 操作系统的:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表
  • 非OC对象一般放在操作系统的里面
  • 操作系统的:由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈(先进后出)
  • 示例:
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;
}
复制代码
图片1.png

2. 内存管理模型

提供给Objective-C程序员的基本内存管理模型有以下3种:

  • 自动垃圾收集(iOS运行环境不支持)
  • 手工引用计数和自动释放池(MRC)
  • 自动引用计数(ARC)

3.MRC 手动管理内存(Manual Reference Counting)

1. 引用计数器

系统是根据对象的引用计数器来判断什么时候需要回收一个对象所占用的内存

  • 引用计数器是一个整数
  • 从字面上, 可以理解为”对象被引用的次数”
  • 也可以理解为: 它表示有多少人正在用这个对象
  • 每个OC对象都有自己的引用计数器
  • 任何一个对象,刚创建的时候,初始的引用计数为1
    • 当使用alloc、new或者copy创建一个对象时,对象的引用计数器默认就是1
  • 当没有任何人使用这个对象时,系统才会回收这个对象, 也就是说
    • 当对象的引用计数器为0时,对象占用的内存就会被系统回收
    • 如果对象的计数器不为0,那么在整个程序运行过程,它占用的内存就不可能被回收(除非整个程序已经退出 )

2. 引用计数器操作

  • 为保证对象的存在,每当创建引用到对象需要给对象发送一条retain消息,可以使引用计数器值+1 ( retain 方法返回对象本身)
  • 当不再需要对象时,通过给对象发送一条release消息,可以使引用计数器值-1
  • 给对象发送retainCount消息,可以获得当前的引用计数器值
  • 当对象的引用计数为0时,系统就知道这个对象不再需要使用了,所以可以释放它的内存,通过给对象发送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;
}
复制代码

3. dealloc方法

  • 当一个对象的引用计数器值为0时,这个对象即将被销毁,其占用的内存被系统回收
  • 对象即将被销毁时系统会自动给对象发送一条dealloc消息(因此,从dealloc方法有没有被调用,就可以判断出对象是否被销毁)
  • dealloc方法的重写
    • 一般会重写dealloc方法,在这里释放相关资源,dealloc就是对象的遗言
    • 一旦重写了dealloc方法,就必须调用[super dealloc],并且放在最后面调用
- (void)dealloc
{
    NSLog(@"Person dealloc");
    // 注意:super dealloc一定要写到所有代码的最后
    // 一定要写在dealloc方法的最后面
    [super dealloc]; 
}
复制代码
  • 使用注意
    • 不能直接调用dealloc方法
    • 一旦对象被回收了, 它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针错误)

4. 野指针和空指针

  • 只要一个对象被释放了,我们就称这个对象为 "僵尸对象(不能再使用的对象)"
  • 当一个指针指向一个僵尸对象(不可用内存),我们就称这个指针为野指针
  • 只要给一个野指针发送消息就会报错(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;
}
复制代码
  • 为了避免给野指针发送消息会报错,一般情况下,当一个对象被释放后我们会将这个对象的指针设置为空指针
  • 空指针
    • 没有指向存储空间的指针(里面存的是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;
}
复制代码

5. 内存管理规律

单个对象内存管理规律

  • 谁创建谁release :
    • 如果你通过alloc、new、copy或mutableCopy来创建一个对象,那么你必须调用release或autorelease
  • 谁retain谁release:
    • 只要你调用了retain,就必须调用一次release
  • 总结一下就是
    • 有加就有减
    • 曾经让对象的计数器+1,就必须在最后让对象计数器-1

多个对象内存管理规律

因为多个对象之间往往是联系的,所以管理起来比较复杂。这里用一个玩游戏例子来类比一下。

游戏可以提供给玩家(A类对象) 游戏房间(B类对象)来玩游戏。

  • 只要一个玩家想使用房间(进入房间),就需要对这个房间的引用计数器+1
  • 只要一个玩家不想再使用房间(离开房间),就需要对这个房间的引用计数器-1
  • 只要还有至少一个玩家在用某个房间,那么这个房间就不会被回收,引用计数至少为1
图片2.png

下面来定义两个类 玩家类:Person 和 房间类:Room

房间类:Room,房间类中有房间号

#import <Foundation/Foundation.h>

@interface Room : NSObject
@property int no; // 房间号
@end

复制代码

玩家类:Person

#import <Foundation/Foundation.h>
#import "Room.h"

@interface Person : NSObject
{
    Room *_room;
}

- (void)setRoom:(Room *)room;

- (Room *)room;
@end

复制代码

现在我们通过几个玩家使用房间的不同应用场景来逐步深入理解内存管理。

1. 玩家没有使用房间,玩家和房间之间没有联系的情况

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;
}
复制代码

上述代码执行完前3行

        // 1.创建两个对象
        Person *p = [[Person alloc] init];    // 玩家 p
        Room *r = [[Room alloc] init];        // 房间 r
        r.no = 888;    // 房间号赋值
复制代码

之后在内存中的表现如下图所示:

图片3.png

可见,Room实例对象和Person实例对象之间没有相互联系,所以各自释放不会报错。执行完4、5行代码

        [r release];    // 释放房间      
        [p release];   // 释放玩家
复制代码

后,将房间对象和玩家对象各自释放掉,在内存中的表现如下图所示:

图片4.png

最后各自实例对象的内存就会被系统回收

2. 一个玩家使用一个游戏房间,玩家和房间之间相关联的情况

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;
}

复制代码

上边代码执行完前3行的时候和之前在内存中的表现一样,如图

图片3.png

当执行完第4行代码p.room = r;时,因为调用了setter方法,将Room实例对象赋值给了Person的成员变量,不做其他设置的话,在内存中的表现如下图(做法不对):

图片5.png

在调用setter方法的时候,因为Room实例对象多了一个Person对象引用,所以应将Room实例对象的引用计数+1才对,即setter方法应该像下边一样,对room进行一次retain操作。

- (void)setRoom:(Room *)room // room = r
{
    // 对房间的引用计数器+1
    [room retain];
    _room = room;
}

复制代码

那么执行完第4行代码p.room = r;,在内存中的表现为:

图片6.png

继续执行第5行代码[r release];,释放房间,Room实例对象引用计数-1,在内存中的表现如下图所示:

图片5.png

然后执行第6行代码[p release];,释放玩家。这时候因为玩家不在房间里了,房间也没有用了,所以在释放玩家的时候,要把房间也释放掉,也就是在delloc里边对房间再进行一次release操作。

这样对房间对象来说,每一次retain/alloc操作都对应一次release操作。

- (void)dealloc
{
    // 人释放了, 那么房间也需要释放
    [_room release];
    NSLog(@"%s", __func__);

    [super dealloc];
}

复制代码

那么在内存中的表现最终如下图所示:

图片7.png

最后实例对象的内存就会被系统回收

3. 一个玩家使用一个游戏房间r后,换到另一个游戏房间r2,玩家和房间相关联的情况

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.将房间赋值给玩家,表示玩家在使用房间
        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.将房间赋值给玩家,表示玩家在使用房间
        p.room = r; // [p setRoom:r]
        [r release];    // 释放房间 r

复制代码

之后的内存表现为:

图片8.png

接着执行换房操作而不进行其他操作的话,

        // 3. 换房
        Room *r2 = [[Room alloc] init];
        r2.no = 444;
        p.room = r2;
复制代码

内存的表现为:

图片9.png

最后执行完

        [r2 release];    // 释放房间 r2
        [p release];    // 释放玩家 p
复制代码

内存的表现为:

图片10.png

可以看出房间 r 并没有被释放,这是因为在进行换房的时候,并没有对房间 r 进行释放。所以应在调用setter方法的时候,对之前的变量进行一次release操作。具体setter方法代码如下:

- (void)setRoom:(Room *)room // room = r
{
        // 将以前的房间释放掉 -1
        [_room release];     

        // 对房间的引用计数器+1
        [room retain];

        _room = room;
    }
}

复制代码

这样在执行完p.room = r2;之后就会将 房间 r 释放掉,最终内存表现为:

图片11.png

4. 一个玩家使用一个游戏房间,不再使用游戏房间,将游戏房间释放掉之后,再次使用该游戏房间的情况

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1.创建两个对象
        Person *p = [[Person alloc] init];
        Room *r = [[Room alloc] init];
        r.no = 888;

        // 2.将房间赋值给人
        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  
复制代码

之后的内存表现为:

图片12.png

然后再执行p.room = r;,因为setter方法会将之前的Room实例对象先release掉,此时内存表现为:

图片13.png

此时_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方法的最终形式。



转自:https://www.jianshu.com/p/48665652e4e4


猜你喜欢

转载自juejin.im/post/5c98ad516fb9a070d90f6c3a