【iOS】—— MRC

1. 引用计数

一个整数,表示为对象被引用的次数,系统需要根据对象的引用计数来判断对象是否需要被回收。

字面意义上可以把引用计数器理解为对象被引用的次数。

系统根据引用计数器的机制来判断对象是否需要被回收。在每次 RunLoop 迭代结束后,都会检查对象的引用计数器,如果引用计数器等于 0,则说明该对象没有地方继续使用它了,可以将其释放掉。

特点:

  • 每一个对象都有自己的引用计数器;
  • 任何一个对象,刚创建的时候,初识的引用计数为1;
    • 即使用alloc,new或者copy创建一个对象时,对象的引用计数器默认为1;
  • 当没有任何人使用这个对象的时候,系统才会回收这个对象:
    • 当对象的引用计数为0时,对象占用的内存就会被系统回收;
    • 如果对象的引用计数不为0的时候,在整个程序运行的过程中,占用的内存就不会被回收;

2. 引用计数的操作

  1. 为保证对象的存在,每当创建引用的对象需要给对象发送一条retain消息,可以使引用计数器值+1;
  2. 当不再需要对象时,通过给对象发送一条release消息,可以使引用计数器值-1;
  3. 给对象发送retainCount消息,可以获得当前的引用计数器值;
  4. 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(@"--------");
    }
    return 0;
}

3. dealloc方法

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

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. 内存管理思想

5.1 单个对象的内存管理思想

思想一:自己创建的对象,自己持有,自己负责释放

  • 通过 alloc、new、copy 或 mutableCopy 方法创建并持有对象。
  • 当自己持有的对象不再被需要时,必须调用 release 或 autorelease方法释放对象。
id obj = [[NSObject alloc] init];   // 自己创建的对象,自己持有
[obj release];

同样,new 方法也能持有对象:

id obj = [NSObject new];    // 自己创建的对象,自己持有
[obj release];

而由各类实现的 copyWithZone: 方法和 mutableCopyWithZone: 方法将生成并持有对象的副本。
另外,除了上面四种方法之外,由上面四种方法名称开头的方法名,也将生成并持有对象:

  • allocMyObject
  • newMyObjec
  • copyMyObject
  • mutableCopyMyObject

思想二:非自己创建的对象,自己也能持有

除了用上面方法(alloc / new / copy / mutableCopy 方法)所取得的的对象,因为非自己生成并持有,所以自己不是该对象的持有者。
通过调用 retain 方法,即便是非自己创建的对象,自己也能持有对象。
同样当自己持有的对象不再被需要时,必须调用 release 方法来释放对象。

id obj = [NSMutableArray array];    // 取得非自己生成的变量,但自己并不持有。
[obj retain];   // 通过 retain 方法持有对象
[obj release];

总结:

  • 无论是否是自己创建的对象,自己都可以持有,并负责释放。
  • 计数器有加就有减。
  • 曾经让对象的计数器 +1,就必须在最后让对象计数器 -1。

5.2 多个对象的内存管理思想

多个对象之间往往是通过setter方法产生联系的,让一个对象通过setter方法来引用另一个对象,从而改变另一个对象的自引用计数,其内存管理的方法也是在 setter 方法、dealloc方法中实现的。

接下来会以Person类和Room类来举例:

//Person类
#import <Foundation/Foundation.h>
#import "Room.h"
NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject {
    Room *_room;
}

- (void)setRoom: (Room *)room;
- (Room *)room;
@end
  
//Room类
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Room : NSObject
@property int no;
@end

NS_ASSUME_NONNULL_END

而多对象的内存管理可以分为下面这些情况:

  1. A对象没有持有B对象
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];    // 玩家 p
        Room *r = [[Room alloc] init];        // 房间 r
        r.no = 888;    // 房间号赋值
 
        [r release];    // 释放房间
        [p release];   // 释放玩家
    }
    return 0;
}

​ 对于这种情况,两个对象之间并没有引用或者持有的关系,所以两个对象释放的时候,各自释放各自的。

  1. A对象持用B对象
 int main(int argc, const char * argv[]) {
    @autoreleasepool {
        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;
}

​ 但是当执行到p.room = r的时候,因为调用了setter方法,将Room实例对象赋值给Person的成员变量,不做其他操作的话,就会出现下面问题(这种做法不对):

img
​ 因为在此过程中调用了setter方法使得Room实例对象多了一个Person对象引用,应该Room实例对象的引用计数+1,所以要修改一下setter方法。

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

​ 然后执行到p.room = r时内存表现为:
img

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

img 当执行到[p release]时,释放玩家。当玩家释放之后,房间也没用了,因此在释放玩家的时候,也要把房间释放了。也就是在delloc里面对房间再进行一次release操作。

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

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

img

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

  1. 对象持有A对象之后释放,然后持有B对象
 int main(int argc, const char * argv[]) {
    @autoreleasepool {
        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;
}

当执行到下面操作的时候:

 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

之后的内存表现为:
img

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

// 3. 换房间
Room *r2 = [[Room alloc] init];
r2.no = 444;
p.room = r2; 

内存表现为:
img
最后执行完代码:

[r2 release];    // 释放房间 r2
[p release];    // 释放玩家 p

内存表现为:
img

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

- (void)setRoom:(nonnull Room *)room {
    // 将以前的房间释放掉 -1
    [_room release];
    // 对房间的引用计数器 +1
    [room retain];

    _room = room;
}

在执行完 p.room = r2; 之后就会将 房间 r 释放掉,最终内存表现为:
img
4. A对象释放B对象之后,再次持有B对象

 int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        Room *r = [[Room alloc] init];
        r.no = 888;

        p.room = r; // [p setRoom:r]
        [r release];    // 释放房间 r
 
        p.room = r;
        [r release];    // 释放房间 r
        [p release];    // 释放玩家 p
    }
    return 0;
}

执行下面代码:

Person *p = [[Person alloc] init];
Room *r = [[Room alloc] init];
r.no = 888;

p.room = r; // [p setRoom:r]
[r release];    // 释放房间 r

之后的内存表现为:
img
然后执行p.room = r;,因为setter方法将之前的Room实例对象先释放掉,此时内存表现为:
img
此时,_room,r已经变成了野指针,之后再对野指针 r 发出 retain消息,程序就会崩溃。我们在进行 setter 方法的时候,要先判断一下是否是重复赋值,如果是同一个实例对象,就不需要重复进行 release 和 retain。则 setter方法具体代码如下:

 - (void)setRoom:(Room *)room { // room = r
    // 只有房间不同才需用 release 和 retain
    if (_room != room) {    // 0ffe1 != 0ffe1
        // 将以前的房间释放掉 -1
        [_room release];
 
        _room = [room retain];
    }
}

6. @property参数

在成员变量前加上 @property,系统就会自动帮我们生成基本的 setter / getter 方法,但是不会生成内存管理相关的代码。

 @property (nonatomic) int val;

同样如果在 property 后边加上 assign,系统也不会帮我们生成 setter 方法内存管理的代码,仅仅只会生成普通的 getter / setter 方法,默认什么都不写就是 assign。

 @property(nonatomic, assign) int val;

我们生成 getter / setter 方法内存管理的代码,但是仍需要我们自己重写 dealloc 方法。

 @property(nonatomic, retain) Room *room;

7. 自动释放池

当我们不再需要一个对象的时候,应该将其空间释放,但是有时候不知道该什么时候来释放空间,为了解决这个问题,oc中有autorelease方法。

autorelease是一种支持自引用计数的内存管理方式,只要给对象发送一条autorelease消息,会将对象放到一个自动释放池中,当自动释放池销毁的时候,里面的所有对象都会进行一次release操作。

注意:这里只是发送 release 消息,如果当时的引用计数(reference-counted)依然不为 0,则该对象依然不会被释放。

调用autorelease方法之后对象的计数器不变。

Person *p = [Person new];
p = [p autorelease];
NSLog(@"count = %lu", [p retainCount]); // 计数还为 1

7.1 autorelease的优点

  1. 不再关心对象的释放时间。
  2. 不再关心什么时候释放对象。

7.2 autorelease的原理

实际上只是把release方法调用延迟,对于每一个autorelease,系统只是将对象放入到当前的 autorelease pool中,当pool释放的时候,里面的所有对象都会调用一次release方法。

7.3 autorelease的创建方法

  1. 使用NSAutoreleasePool 创建
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 创建自动释放池
[pool release]; // [pool drain]; 销毁自动释放池

  1. 使用@autoreleasepool 创建
 @autoreleasepool
{ // 开始代表创建自动释放池
 
} // 结束代表销毁自动释放池

7.4 autorelease的使用方法

NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
Person *p = [[[Person alloc] init] autorelease];
[autoreleasePool drain];

@autoreleasepool { 
  // 创建一个自动释放池
  Person *p = [[Person new] autorelease];
  // 将代码写到这里就放入了自动释放池
} // 销毁自动释放池(会给池子中所有对象发送一条 release 消息)

7.5 autorelease的注意事项

  1. 不是放到自动释放池中就自动加入到自动释放池。
 @autoreleasepool {
    // 因为没有调用 autorelease 方法,所以对象没有加入到自动释放池
    Person *p = [[Person alloc] init];
    [p run];
}

  1. 在自动释放池外部发送autorelease不会加入到自动释放池中。
  2. 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];
}

7.6 自动释放池的嵌套

自动释放池以栈的形式和存在,所以调用autorelease会将对象放到栈顶的自动释放池。

栈顶就是离调用 autorelease 方法最近的自动释放池。

 @autoreleasepool { // 栈底自动释放池
    @autoreleasepool {
        @autoreleasepool { // 栈顶自动释放池
            Person *p = [[[Person alloc] init] autorelease];
        }
        Person *p = [[[Person alloc] init] autorelease];
    }
}

自动释放池不宜存放占内存较大的对象。

尽量避免对大内存使用该方法,不要把大量循环操作放到同一个@autorelease中,会导致内存峰值的上升。

 // 内存暴涨
@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];
    }
}

7.7 autorelease错误用法

  1. 不要连续调用autorelease
 @autoreleasepool {
 // 错误写法, 过度释放
    Person *p = [[[[Person alloc] init] autorelease] autorelease];
 }
  1. 调用autorelease之后又调用release
 @autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    [p release]; // 错误写法, 过度释放
}

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-17 07:20:04       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-17 07:20:04       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-17 07:20:04       58 阅读
  4. Python语言-面向对象

    2024-07-17 07:20:04       69 阅读

热门阅读

  1. Github07-16 Python开源项目日报 Top10

    2024-07-17 07:20:04       22 阅读
  2. 用于图像增强的学习型可控ISP

    2024-07-17 07:20:04       28 阅读
  3. 掌握Xcode的魔术:自定义Storyboard的创建与管理

    2024-07-17 07:20:04       29 阅读
  4. Telegram Bot、小程序开发(三)Mini Apps小程序

    2024-07-17 07:20:04       23 阅读
  5. Google 地图参考手册

    2024-07-17 07:20:04       26 阅读
  6. 通过swagger自动生成jmeter脚本

    2024-07-17 07:20:04       24 阅读
  7. B树(B-tree)

    2024-07-17 07:20:04       28 阅读
  8. 081、Python 关于方法重写

    2024-07-17 07:20:04       22 阅读
  9. 【linux 100条命令】

    2024-07-17 07:20:04       24 阅读
  10. Emacs: 可扩展的编辑器之神

    2024-07-17 07:20:04       28 阅读
  11. 如何在VSCode中配置Python环境

    2024-07-17 07:20:04       28 阅读