内存管理总结-ARC和非ARC

序言

       ObjC内存管理分为两个阶段:

       Xcode4之前,ObjC的内存管理就需要由开发人员手动维护,内存的管理需要开发人员自己调用内存管理的方法,所以被称为MRC(Manual Reference Counting),即非ARC。

       Xcode4之后,ObjC的内存管理就不需要由开发人员手动维护,系统会自定在合适的时机或位置帮助开发人员调用内存管理的方法,所以被称为ARC(Automatic Reference Counting)。

       我们知道在程序运行过程中要创建大量的对象,和其他高级语言类似,在ObjC中对象时存储在堆中的,系统并不会自动释放堆中的内存(注意基本类型是由系统自己管理的,放在栈上)。如果一个对象创建并使用后没有得到及时释放那么就会占用大量内存。其他高级语言如C#、Java都是通过垃圾回收来(GC)解决这个问题的。

       在OjbC中并没有类似的垃圾回收机制。Xcode4之前ObjC的内存管理通过非ARC方式来管理。Xcode4之后通过ARC来管理,这样看来Xcode为我们做了很多事情,这种机制确实为我们节省了不少的麻烦。为什么还要了解非ARC呢?因为非ARC机制可以帮我们了解到内存管理的真正实现。

       既然OjbC没有类似的垃圾回收机制,那么它是如何来管理内存的呢?程序员在ARC和非ARC下又是通过什么来管理内存的呢?下面引入一个概念。

引用计数器

       其实在ObjC中内存的管理是依赖对象引用计数器来进行的:在ObjC中每个对象内部都有一个与之对应的整数(retainCount),叫“引用计数器”,当一个对象在创建之后它的引用计数器为1,当调用这个对象的alloc、retain、new、copy方法之后引用计数器自动在原来的基础上加1(ObjC中调用一个对象的方法就是给这个对象发送一个消息),当调用这个对象的release方法之后它的引用计数器减1,如果一个对象的引用计数器为0,则系统会自动调用这个对象的dealloc方法来销毁这个对象。

       可以简单的理解引用计数器为:有哪些对象还在引用这某个对象或者有哪些对象还在使用某个对象。

内存管理原则

       谁创建谁释放,谁引用谁管理

非ARC(MRC)

       在非ARC环境下,程序员就是通过引用计数器来判断一个对象是否被释放的。下面通过代码来进行非ARC环境下的内存管理。将项目改成非ARC:Build Settings -> Objective-C Automatic Reference Counting改为NO。

       现在有一个人类(HXPerson),一个宠物狗类(HXDog)。

HXPerson类定义

       HXPerson.h

#import <Foundation/Foundation.h>

@interface HXPerson : NSObject {
    int _age;
}
- (void)speak;
@end
       HXPerson.m

#import "HXPerson.h"
@implementation HXPerson

- (instancetype)init
{
    self = [super init];
    if (self) {
        _age = 30;
    }
    return self;
}

- (void)speak {
    NSLog(@"%d的人在说话", _age);
}

- (void)dealloc {
    NSLog(@"%d的人被销毁", _age);
    [super dealloc];
}
@end
       HXDog.h

#import <Foundation/Foundation.h>

@interface HXDog : NSObject {
    NSString *_name;
}
- (void)bark;
@end
       HXDog.m

#import "HXDog.h"

@implementation HXDog
- (instancetype)init
{
    self = [super init];
    if (self) {
        _name = @"小强";
    }
    return self;
}

- (void)bark {
    NSLog(@"%@的狗在吠", _name);
}

- (void)dealloc {
    NSLog(@"%@的狗被销毁", _name);
    [super dealloc];
}
@end

单个对象的内存管理

       现在我们在main函数中创建并初始化一个HXPerson对象

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

int main(int argc, const char * argv[]) {
    
    HXPerson *person = [[HXPerson alloc] init];// 引用计数:1
    NSLog(@"引用计数:%ld %p", [person retainCount], person);
    /*
     person是一个指针变量,该变量指向HXperson类型。这里指向内存中的HXPerson对象。内存中HXPerson对象有一个变量指向(引用)它,此时它的引用计数器
     就是1。我们可以使用该指针指向的对象发送消息
     */
    [person speak];// 发送消息
    
    return 0;
}
        通过打印可以看出,person指针指向的对象的引用计数器为1。在ObjC中创建一个对象的时候,该对象的引用计数器就为1。当再次调用/new/copyretain方法时,该对象的引用计数器会在原来的基础上加1。

       上面说过,只有当对象的引用计数器为0的时候,系统才会调用对象的dealloc方法来销毁对象。这里的person指针指向的对象的引用计数器一直为1,并没谁来让该对象的引用计数器为0,也就是说该对象没有被释放(程序未退出的情况下)。

       想象一下,如何该main函数会一直运行着,这样的话,HXPerson对象就会一直占用内存,如何有很多这样的对象,占用内存就会很大,这显然是不合适的。所有我们要来管理该对象的内存。在这里我们通过内存管理的另外一个方法(release方法)来使得该对象的引用计数器减1。还有,我们要在合适的时机来将对象的引用计数器减1。我们不能在该对象使用期间就把它的引用计数器减1。所有找到合适的时机也很重要。根据内存管理原则,代码修改如下:

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

int main(int argc, const char * argv[]) {
    
    HXPerson *person = [[HXPerson alloc] init];// 引用计数:1
    NSLog(@"引用计数:%ld %p", [person retainCount], person);
    /*
     person是一个指针变量,该变量指向HXperson类型。这里指向内存中的HXPerson对象。内存中HXPerson对象有
     一个变量指向(引用)它,此时它的引用计数器就是1。我们可以想该指针指向的对象发送消息
     */
    [person speak];
    /*
     person是一个指针变量,该变量指向HXperson类型。这里指向内存中的HXPerson对象。内存中HXPerson对象有一个变量指向(引用)它,此时它的引用计数器就是1。
     因为该对象是我们手动创建的(通过alloc的形式),根据内存管理的原则,就应该我们来释放,所有在不使用该对象的时候,要调用release方法来释放该对象所
     占用的内存
     */
    [person release];
    /*
     此时内存中HXPerson对象的引用计数器是0。此时系统会自动调用HXPerson对象的dealloc方法,来释放该对象所占用的内存。但是,指针变量person还存在,而且
     此时person指针并没有指向任何对象,也就是所说的野指针。这个时候对该对象调用方法(发送消息)就会有一个
     经典报错:message sent to deallocated instance 0x1004044b0
     */
//    [person speak];// 此时调用该方法会报错
    /*
     为了避免在对象销毁的之后,还向该对象发送消息,我们在该对象销毁之后,将指向该对象的指针置为nil。此时,如果再向该指针指向的对象发送消息就不会报错。
     */
    person = nil;
    [person speak];
    return 0;
}
       修改后,当程序运行到[person release];代码后,person指针执行的对象就是被释放,根据打印结果可以看出此时重写HXPerson对象的dealloc方法被调用了,说明该对象被销毁了。对象释放后还有一些注意点,就是将指向被释放对象的指针置为nil。在注释中也有说明。

多个对象(两个)的内存管理

       上面介绍完了单个对象的内存管理,然而在实际开发中,项目中的对象是相互引用的,它们之间的内存管理和单个对象的内存管理有点小小的区别。本质还是一样的。我们来看看多个对象内存管理(这里为了说明只是两个对象)。

       利用上的人(HXPerson)类和宠物类(HXDog)

狗类的定义没有改变

人类的定义如下

HXPerson.h

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

@interface HXPerson : NSObject {
    int _age;
    HXDog *_dog;
}
- (instancetype)initWithDog:(HXDog *)dog;
- (void)speak;
- (void)lookMyDog;
@end
HXPerson.m

#import "HXPerson.h"

@implementation HXPerson

- (instancetype)initWithDog:(HXDog *)dog
{
    self = [super init];
    if (self) {
        _age = 30;
        _dog = dog;
    }
    return self;
}

- (void)speak {
    NSLog(@"%d的人在说话", _age);
}

- (void)lookMyDog {
    NSLog(@"看看我的狗--%@", _dog);
}

- (void)dealloc {
    NSLog(@"%d的人被销毁", _age);
    
    [super dealloc];
}

@end
       我们在main函数中,首先创建一个狗对象(dog指针指向的对象),再创建一个人对象(person指针指向的对象),不过在初始化person指针指向的对象的时候,将狗对象赋值给person的_dog变量。

#import <Foundation/Foundation.h>
#import "HXDog.h"
#import "HXPerson.h"

int main(int argc, const char * argv[]) {
    
    HXDog *dog = [[HXDog alloc] init];// 引用计数:1
    HXPerson *person = [[HXPerson alloc] initWithDog:dog];// 引用计数:1
    NSLog(@"人的引用计数:%ld,狗的引用计数:%ld", [person retainCount], [dog retainCount]);
    
    // 此时人和狗的引用计数器都为1,说明我们可以使用这两个对象
    // 使用这两个对象
    [dog bark];
    
    [person speak];
    [person lookMyDog];

 /*************************************/
    // 现在将dog释放
    [dog release];
    // 此时狗的引用计数器为0,系统自动调用HXDog的dealloc方法回收内存。根据上面的例子,我们需要将dog指针置为nil。人的引用计数器没变,
    dog = nil;
    NSLog(@"release--人引用计数:%ld,狗的引用计数:%ld", [person retainCount], [dog retainCount]);
    // 此时再使用这两个对象(OC对nil发送消息是允许的)
    [dog bark];
    
    [person speak];
    [person lookMyDog];
    return 0;
}
       根据打印可以看出,创建dog指向的对象并初始化,再创建person指向的对象并初始化后,调用相应的方法,都是正常的,注释中解释了原因。但是当dog指向的对象被释放之后,再调用相应的方法的时候,在执行到[person lookMyDog];方法时候,发生了错误。我们来分析一下原因:

       我们通过断点单步调试,错误信息指向了下面这个方法:

- (void)lookMyDog {
    NSLog(@"看看我的狗--%@", _dog);
}
       在这个方法里面我们只是访问了person指向的对象的_dog变量,既然知道是访问_dog变量出错,那么我们来看看这个_dog变量是怎么来的。我们知道在创建并初始化person指向的对象的时候,我们用的是initWithDog:方法,我们在这里给person指向的对象赋值的。来看看该方法的实现,该方法的实现如下:

- (instancetype)initWithDog:(HXDog *)dog
{
    self = [super init];
    if (self) {
        _age = 30;
        _dog = dog;
    }
    return self;
}
       在初始化HXPerson对象的时候,我们只是简单的将传进来dog指针赋值给HXPerson对象的_dog变量。也就是说,_dog和dog指向同一个HXDog对象。但是HXDog对象在执行[dog release];方法后,已经被销毁,所以_dog和dog都指向了一块无对象的内存区域。当执行[person lookMyDog];方法的时候,要访问_dog指针指向的对象,这才报错。

       如何解决这个问题?

       我们可以在给_dog赋值的时候,拷贝一份dog指针。这里用到了另外一个内存管理的方法(retain)具体实现:

- (instancetype)initWithDog:(HXDog *)dog
{
    self = [super init];
    if (self) {
        _age = 30;
        _dog = [dog retain];
    }
    return self;
}
       这个时候,在运行程序,发现没有报错了。但是我们又发现一个问题,就是第一次打印person指向对象和dog指向对象的引用计数器的时候,发现dog指向的对象的引用计数器变成了2。上面说过当一个对象创建完后,引用计数器为1,再调用内存管理方法copy/retain等方法的时候,对象的引用计数器会在原来的基础上加1。这里就验证了这个结论。
       dog指向的对象引用计数器为2,在执行完[dog release];后,dog指向的对象的引用计数器减1,此时HXDog对象的引用计数器为1。这样的话就又出现了HXDog对象没有释放的情况了。它仍占用着内存。这个时候我们如何销毁HXDog对象呢?还记得内存管理原则吧?(谁创建谁释放,谁引用谁管理)

       我们来分析HXDog对象的引用计数器加1是在哪里发生的?我们修改代码的时候只是将_dog = dog;修改为_dog = [dog retain];,可以看出就是这行代码让HXDog对象的引用计数器加1的。本着谁引用谁管理的原则,我们应该在HXPerson对象里来释放HXDog对象,(因为是在HXPerson里retain的)。

       这里释放的时机就是当HXPerson对象被释放的时候,才释放HXDog对象,因为_dog是HXPerson对象的变量。只要HXPerson对象被释放,HXDog对象就会被释放。这样的话,最合适的时机就是在HXPerson对象的dealloc方法里释放。所有HXPerson对象的dealloc方法修改如下:

- (void)dealloc {
    [_dog release];
    _dog = nil;// 别忘记置为nil
    NSLog(@"%d的人被销毁", _age);
    
    [super dealloc];
}
       我们再次运行代码,仍然没有报错。但是我们没有看到HXPerson对象里的_dog被释放的情况。现在为了我们能看到_dog被释放,我们在HXPerson的dealloc方法中添加打印语句。

- (void)dealloc {
    [_dog release];
    _dog = nil;
    NSLog(@"这个人的狗引用计数:%ld", [_dog retainCount]);
    NSLog(@"%d的人被销毁", _age);
    
    [super dealloc];
}

再在main函数的后面加上这些代码:

// 现在将person释放
    [person release];
    person = nil;// 别忘记置为nil
    NSLog(@"人的引用计数:%ld,狗的引用计数:%ld", [person retainCount], [dog retainCount]);
    

       现在我们来运行代码吧!现在才是正确的结果!!!打印结果如下:


       其实对内存的管理还有很多注意的地方,这里只是简单介绍了非ARC下是如何管理内存的。

ARC

       Xcode4之后引入了ARC机制。在ARC环境下,程序员也是通过引用计数器来判断一个对象是否被释放的。但是不需要程序员来手动调用内存管理的方法(调用retain/release方法)。下面通过代码来进行ARC环境下的内存管理。由于一些内存管理的操作都是系统帮我们完成的,所有代码演示很简单。只要了解非ARC环境下的内存管理就差不多了。

       现在我们将项目的运行环境再修改回来:Build Settings -> Objective-C Automatic Reference Counting改为YES。仍然使用上面的两个类。

       此时我们先编译(command + B)一下项目,会发现之前写的内存管理方法全部报错了,这也就是ARC环境下,系统会帮我们调用这些方法来管理内存,所以才叫ARC嘛。

单个对象内存管理

       在main函数中其实就是简单的两行代码

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

int main(int argc, const char * argv[]) {
    HXPerson *person = [[HXPerson alloc] init];
    [person speak];
    
    /*OK了!!!*/
    
    return 0;
}
       由于是系统帮我们管理内存,这里就不能通过打印来查看对象的引用计数器了,但是我们可以通过重写HXPerson对象的dealloc方法来查看对象有没有被销毁。运行代码,打印结果如下:

       可以看出,我们没有写任何的内存管理代码,HXPerson对象在使用完之后仍然被销毁了。一切内存管理的操作都是系统帮我们做的。

多个对象(两个)内存管理

       跟上面一样这里也通过两个对象的使用来演示。还是上面的两个类。演示代码

#import <Foundation/Foundation.h>
#import "HXDog.h"
#import "HXPerson.h"

int main(int argc, const char * argv[]) {
    HXDog *dog = [[HXDog alloc] init];
    HXPerson *person = [[HXPerson alloc] initWithDog:dog];
    
    // 使用对象(调用对象方法)
    [dog bark];
    [person speak];
    
    /*OK了!!!*/
    
    return 0;
}
       还是看看打印情况:


       在使用完HXPerson对象和HXDog对象后,两个对象都被释放了。

总结

这里只是总结本文中涉及到的知识点。

相同点:

内存管理的原理:都是通过引用计数器来管理内存,当对象的引用计数器为0,会调用对象的dealloc方法来销毁对象。

内存管理的方法:对象创建时,引用计数器为1,当再次调用copy/new/retain方法时,引用计数器在原来的基础上加1。当调用release方法时,引用计数器在原来的基础上减1。

不同点:

非ARC:手动管理内存,手动调用内存管理方法,如alloc/copy/new/retain/release等。

ARC:自动管理内存,系统在合适的时机和位置自动调用内存管理方法。

非ARC下重写dealloc方法时,需调用父类的dealloc方法,在本类实例释放之前一些释放操作需要在调用父类dealloc方法之前执行。

ARC下重写dealloc方法,不需要调用父类的dealloc方法。

       在非ARC环境下对内存管理的时候,我们不仅要考虑在什么时机去调用release,还要考虑谁创建谁释放,谁引用谁管理的原则。有时候一不小心程序就会报错,给开发人员带来不少麻烦。但在ARC环境下,ARC机制帮我们减少了这些麻烦,让我们有更多的时间去做别的事情。

       注意我说的是减少了一些麻烦,也就是说还有一个麻烦还是存在的,即在ARC环境下也又可以需要我们来管理内存,这也就是为什么有些人说ARC是一种的半自动管理机制了。不管怎么说,ARC机制是非常棒的!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值