黑马程序员——OC的内存管理——MRC

本文详细介绍了Objective-C的MRC(Manual Reference Counting)内存管理机制,包括基本概念、手动开启MRC、内存管理原则、野指针处理、对象作为实例变量的内存管理以及autorelease的使用。通过实例解析了对象生命周期中的retain、release和dealloc操作,强调了正确管理内存以避免内存泄漏的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

———-android培训、Java培训、iOS培训,期待与您交流———-

一、基本概念

1. 为什么要进行内存管理?

由于对象存储在内存的堆中,而除了对象之外的其他局部变量存储在栈中,当当前代码块结束时,系统会自动回收存储在栈中的数据,指向对象的指针也会被自动回收。此时没有指针指向对象,而对象依然存在在内存中,会造成内存泄漏。

内存管理

如上图所示,在OC中所要管理的内存,就是存储在堆区的对象实例。

2. OC提供的三种内存管理方式:

  • Mannual Reference Counting(MRC,手动管理,在iOS4.1之前必须使用MRC)
  • Automatic Reference Counting(ARC,自动引用计数,iOS4.1之后推出)
  • Garbage Collection(垃圾回收,iOS不支持,仅在开发Mac app时使用)
    注:开发中尽量使用ARC

3. 关于引用计数(Reference Counting)

每个对象都有一个与之相关联的整数,被称作它的引用计数。当某段代码需要访问一个对象时,这个对象的引用计数就加1。当这段代码结束对象访问时,这个对象的引用计数就减1。当对象的引用计数为0时,就表示不再有代码访问该对象了,因此他将被删除,所占用的内存会被系统回收。

创建一个新的对象时(包括使用alloc、new和copy),该对象的引用计数被设置为1。使用retain(返回值为当前对象)方法可以使引用计数加1,使用release(返回值为空)则可以使引用计数减1,使用retainCount可以获得对象当前的引用计数值(返回值的类型为NSUInteger)。

当一个对象的引用计数为0时,会自动调用dealloc方法,这样,该对象就能被从内存中释放掉。

二、手动内存管理(MRC)

1. 开启MRC

由于目前的OC项目默认是关闭MRC的,所以需要手动开启。具体方法如下:
选中项目 -> Build Setting -> Basic -> levels -> 搜索auto -> 把ARC改为NO。

2. 基本应用举例

假设已经有对象Car。在main.m中:

#import <Foundation/Foundation.h>
#import "Car.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Car *c = [Car new];
        NSUInteger count = [c retainCount];
        NSLog(@"%lu",count);//->1
        [c retain];
        count = [c retainCount];
        NSLog(@"%lu",count);//->2
        [c release];
        count = [c retainCount];
        NSLog(@"%lu",count);//->1
    }
    return 0;
}

3. 重写dealloc

当对象的引用计数为0时,系统会自动向对象发送一条dealloc消息。一般会重写dealloc方法,在这里释放相关的资源。dealloc就像是对象的“临终遗言”。 一旦重写了dealloc方法就必须在dealloc中调用[super dealloc],并且放在代码块的最后调用(不能直 接调用dealloc方法)。一旦对象被回收了,那么他所占据的存储空间就不再可用,坚持使用会导致程序崩溃(野指针错误)。
重写dealloc举例:

#import "Car.h"

@implementation Car
- (void)dealloc
{
    NSLog(@"对象已经被释放");
    [super dealloc];
}
@end

此时如果在第二小节的main函数中继续release,则会自动调用dealloc:

[c release];//->对象已经被释放

注意,不要直接调用dealloc,当对象的引用计数为0时会自动调用

4. 内存管理的原则

  • 基本原则
    只要还有人在使用某个对象,那么这个对象就不会被回收;
    只要你想使用这个对象,那么就应该让这个对象的引用计数器+1;
    当你不想使用这个对象时,应该让对象的引用计数器-1;
  • 谁创建,谁release
    (1)如果通过alloc,new,copy来创建了一个对象,那么就必须调用release或者autorelease方法
    (2)不是你创建的就不用你去负责
  • 谁retain,谁release
    只要调用了retain,无论这个对象时如何生成的,都要调用release
  • 总结
    有始有终,有加就应该有减。曾经让某个对象计数器加1,就应该让其在最后-1.

5. 关于野指针

当一个对象的引用计数为0时,系统会将它所占的内存空间释放。但是,如果在释放后立即使用该对象,仍然可行,举例如下(Car对象已经定义test方法打印“Hello”):

#import <Foundation/Foundation.h>
#import "Car.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Car *c = [Car new];
        [c release];
        [c test];//->Hello
    }
    return 0;
}

造成上述情况的原因是,虽然名为c的Car对象的引用计数已经为0,但是所谓的内存释放并不是将存储该对象的内存擦除,而只是将这部分内存标记为可用,当系统需要为其他数据分配内存时,这段内存会被认为是可以使用的。但是本例中由于释放对象之后并没有向内存中写入新的数据,所以这段内存的内容仍然没有改变。这样的使用是不安全的,因为如果在这段内存被系统重新使用过后再调用已经释放了的对象会造成程序崩溃。这种已经被释放了的对象就是僵尸对象。为了防止这种情况发生,Xcode提供了僵尸对象检测功能。开启方式如下:

在切换Target的菜单中点击Edit Scheme->Run Debug->Diagnostics->Enable Zombie Objects

当开启僵尸指针检测后,使用僵尸指针会造成程序报错。

6. 对象作为其他对象实例变量的内存管理

建立两个对象:Car和Engine,Car拥有Engine,代码如下

/****Car.h****/
#import <Foundation/Foundation.h>
#import "Engine.h"
@interface Car : NSObject
{
    Engine *_Engine;
}
-(void)setEngine:(Engine *) newEngine;
-(Engine *)Engine;
@end
/****Car.m****/
#import "Car.h"

@implementation Car
-(void)setEngine:(Engine *) newEngine{
    _Engine = [newEngine retain];
}
-(Engine *)Engine{
    return _Engine;
}
- (void)dealloc
{
    NSLog(@"Car 已经被销毁");
    [super dealloc];
}
@end
/****Engine.h****/
#import <Foundation/Foundation.h>

@interface Engine : NSObject
{
    int _speed;
}
-(void)setSpeed:(int)speed;
-(int)speed;
@end
/****Engine.m****/
#import "Engine.h"

@implementation Engine
-(void)setSpeed:(int)speed{
    _speed = speed;
}
-(int)speed{
    return _speed;
}
- (void)dealloc
{
    NSLog(@"Engine已经被销毁");
    [super dealloc];
}
@end
/****main.m****/
#import <Foundation/Foundation.h>
#import "Car.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Car *c = [Car new];
        Engine *e = [Engine new];
        e.speed = 120;
        [c setEngine:e];//retainCount=2
    }
    return 0;
}

如上代码,Car和Engine分别拥有set/get方法,重写了dealloc。在main函数中,分别创建了Car对象和Engine对象。

  1. 此时程序出现了一个问题,如果要为car赋值一个新的Engine,接上文的main函数代码如下:
[e release];//retainCount=1
Engine *e2 = [Engine new];
[c setEngine:e2];

此时,由于e还没有销毁,而c的Engine指向了e2,所以会造成内存泄漏,正确解决该问题的方法是改写Car类的set方法,代码如下:

-(void)setEngine:(Engine *) newEngine{
    [newEngine retain];
    [_Engine release];
    _Engine = newEngine;

}

即先对新的Engine对象retain一次,然后release旧的Engine,最后将新的Engine赋给car。

  1. 第二个问题是,按照常识,当car对象销毁时,作为他的实例变量的Engine也应该自动销毁,而在上面的代码中,必须先对Engine进行release,然后对car进行release才能正常销毁两个对象。如果想要在销毁car的时候自动销毁Engine,则可以通过改写Car类的dealloc方法实现,代码如下:
- (void)dealloc
{
    NSLog(@"Car 已经被销毁");
    [_Engine release];
    [super dealloc];
}

即在car销毁之前,先对Engine执行一次release。这样也符合内存管理中,谁创建谁release的原则。

7. 关于autorelease

通过autorelease方法可以将对象放进autoreleasepool(自动释放池)中,当自动释放池的代码段结束时会向该对象发送一条release消息。

autorelease只是在自动释放池结束时自动发送一条release,而不是将对象销毁,因此如果在自动释放池结束之前对象的引用计数不为1,则会造成内存泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值