内存管理(MRC、ARC)

2021-04-15  本文已影响0人  NJKNJK

本文用来对 Objective-C 语法中,内存管理(MRC、ARC)相关知识进行讲解。

一、 什么是内存管理

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

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

这是因为

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

二、 内存管理模型

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


三、MRC 手动管理内存(Manual Reference Counting)

1. 引用计数器

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

2. 引用计数器操作

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方法

- (void)dealloc
{
    NSLog(@"Person dealloc");
    // 注意:super dealloc一定要写到所有代码的最后
    // 一定要写在dealloc方法的最后面
    [super dealloc]; 
}

4. 野指针和空指针

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

5. 内存管理规律

单个对象内存管理规律
多个对象内存管理规律

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

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

图片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方法的最终形式。

6. @property参数

@property (nonatomic) int val;
@property(nonatomic, retain) Room *room;
@property(nonatomic, retain) int val;

7. 自动释放池

当我们不再使用一个对象的时候应该将其空间释放,但是有时候我们不知道何时应该将其释放。为了解决这个问题,Objective-C提供了autorelease方法。

Person *p = [Person new];
p = [p autorelease];
NSLog(@"count = %lu", [p retainCount]); // 计数还为1
1. 使用AUTORELEASE有什么好处呢
2. AUTORELEASE的原理实质上是什么?

autorelease实际上只是把对release的调用延迟了,对于每一个autorelease,系统只是把该对象放入了当前的autorelease pool中,当该pool被释放时,该pool中的所有对象会被调用release。

3. AUTORELEASE的创建方法
  1. 使用NSAutoreleasePool来创建
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 创建自动释放池
[pool release]; // [pool drain]; 销毁自动释放池
  1. 使用@autoreleasepool创建
@autoreleasepool
{ //开始代表创建自动释放池

} //结束代表销毁自动释放池
4. AUTORELEASE的使用方法
NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
Person *p = [[[Person alloc] init] autorelease];
[autoreleasePool drain];
@autoreleasepool
{ // 创建一个自动释放池
        Person *p = [[Person new] autorelease];
        // 将代码写到这里就放入了自动释放池
} // 销毁自动释放池(会给池子中所有对象发送一条release消息)
5. AUTORELEASE的注意事项
@autoreleasepool {
    // 因为没有调用 autorelease 方法,所以对象没有加入到自动释放池
    Person *p = [[Person alloc] init];
    [p run];
}
@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];
}
6. 自动释放池的嵌套使用
@autoreleasepool { // 栈底自动释放池
    @autoreleasepool {
        @autoreleasepool { // 栈顶自动释放池
            Person *p = [[[Person alloc] init] autorelease];
        }
        Person *p = [[[Person alloc] init] 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. AUTORELEASE错误用法
@autoreleasepool {
 // 错误写法, 过度释放
    Person *p = [[[[Person alloc] init] autorelease] autorelease];
 }
@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    [p release]; // 错误写法, 过度释放
}

8. MRC中避免循环retain

定义两个类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对象永远无法释放

那么如何解决这个问题呢?


四、ARC 自动管理内存(Automatic Reference Counting)

1\ ARC的判断原则

ARC判断一个对象是否需要释放不是通过引用计数来进行判断的,而是通过强指针来进行判断的。那么什么是强指针?

Person *p1 = [[Person alloc] init];
__strong  Person *p2 = [[Person alloc] init];
__weak  Person *p = [[Person alloc] init];

ARC如何通过强指针来判断?

2. ARC的使用

int main(int argc, const char * argv[]) {
    // 不用写release, main函数执行完毕后p会被自动释放
    Person *p = [[Person alloc] init];

    return 0;
}

3. ARC的注意点

4. ARC下单对象内存管理

int main(int argc, const char * argv[]) {
   @autoreleasepool {
        Person *p = [[Person alloc] init];
    } // 执行到这一行局部变量p释放
    // 由于没有强指针指向对象, 所以对象也释放
    return 0;
}
int main(int argc, const char * argv[]) {
   @autoreleasepool {
        Person *p = [[Person alloc] init];
        p = nil; // 执行到这一行, 由于没有强指针指向对象, 所以对象被释放
    }
    return 0;
}
int main(int argc, const char * argv[]) {
   @autoreleasepool {
        // p1和p2都是强指针
        Person *p1 = [[Person alloc] init];
        __strong Person *p2 = [[Person alloc] init];
    }
    return 0;
}
int main(int argc, const char * argv[]) {
   @autoreleasepool {
        // p是弱指针, 对象会被立即释放
        __weak Person *p1 = [[Person alloc] init];
    }
    return 0;
}

5. ARC下多对象内存管理

@interface Person : NSObject
// MRC写法
//@property (nonatomic, retain) Dog *dog;

// ARC写法
@property (nonatomic, strong) Dog *dog;
@end

6. ARC下@property参数

7. ARC下循环引用问题

@interface Person : NSObject
@property (nonatomic, strong) Dog *dog;
@end

@interface Dog : NSObject
// 错误写法, 循环引用会导致内存泄露
//@property (nonatomic, strong) Person *owner;

// 正确写法, 当如果保存对象建议使用weak
@property (nonatomic, weak) Person *owner;
@end
上一篇 下一篇

猜你喜欢

热点阅读