美文网首页IOS开发基础(oc)
ios开发基础学习笔记(八)--MRC的引用计数

ios开发基础学习笔记(八)--MRC的引用计数

作者: miloluo | 来源:发表于2018-05-24 17:47 被阅读0次

    前言

    大家好,我是milo。iOS5.0开始,Apple有了ARC(Auto Reference Counting),ARC不同于MRC(Manual Reference Counting),它使得大部分类和自定义类不需要手动进行内存管理,它会在适当的时候回收内存,就像栈内存一样。但是作为一个ios开发者,我们需要通过MRC下的内存管理学习,加强对底层的理解。今天这篇文章讲的是ios内存管理的知识--引用计数。

    堆和栈

    我们知道内存是有堆和栈的,但是它们负责存储的东西不同。

    栈负责存储非oc对象,也就是不继承NSOject的那些对象,和非对象类型如(int、char、float、double、struct、enum)等 ;

    堆负责存储oc对象,也就是继承自NSObject的对象,包括其自带的许多类和包(UIKit等),以及自定义的类等等。

    需要注意的是:栈内存是系统自动回收的,不需要程序员去管理,不存在内存泄漏的问题;堆内存是需要程序员区管理的,存在内存泄漏的问题,所以本篇文章“引用计数”是围着堆内存(前提是MRC)管理展开的。

    对象的本质

    在介绍下面的引用计数前,我们还需要了解对象的本质。

    举个小小的例子说明:

     JJPerson *person = [[JJPerson alloc] init];//  创建一个对象
    

    上面这段代码是最常用的创建对象的方式,通过 [[JJPerson alloc] init] 创建出一个person对象。

    但实际上,person这个东西,不是对象,它只是一个指向堆内存的指针。我们见下图:


    真实情况.png

    等式的右边:JJPerson是一个继承NSObject的类,当它通过 [[JJPerson alloc] init] 创建一个对象的时候,就会在堆中开辟一段内存存储创建出来的JJPerson对象。(注:我在图中标注的地址是随意写的)

    等式的左边: JJPerson *person 创建一个指针指向等式右边的内存。而在这以后就可以通过 person 这个指针自由的修改对象了。

    引用计数

    一、[xx retain] 和 [xx release]

    每个OC对象都有自己的引用计数器,它是一个整数,从字面上, 可以理解为”对象被引用的次数”,也可以理解为: 它表示有多少人正在用这个对象。

    那么上面的真实情况.png就变成了下面这样:

    真・真实情况.png

    oc对象被创建出来的时候,他的引用计数为1,所以图中JJPerson对象的retainCount = 1。需要注意的是,当引用计数为0 时,当前的对象的堆内存就会被系统回收

    下面我们通过一些代码和代码的解释,来更加深入地理解引用计数。

    #import <Foundation/Foundation.h>
    #import "JJPerson.h"
    
    int main(int argc, const char * argv[]) {
       
    //    创建一个对象,则对象的引用计数为1
        JJPerson *person = [[JJPerson alloc] init];
        NSLog(@"%zd",[person retainCount]);
        
        return 0;
    }
    

    [person retainCount] 方法的作用是获取对象的引用次数,但是你会发现,编译器并没有提示你这个方法,而且会报错。这是因为iOS5.0以后默认都是开启ARC的,所以我们要做的是关闭ARC。

    关闭ARC.png

    关闭以后我们的程序就可以正常编译通过了,打印如下:
    2018-05-24 15:57:03.812508+0800 引用计数[963:61201] 1
    刚好印证了刚才的“oc对象被创建出来的时候,他的引用计数为1”这句话。

    接下来我们将调用 [person retain] 和 [person release] 两种方法,前一个方法的作用是让对象的引用计数+1,后一个方法的作用是让对象的引用计数-1。见如下代码:

    (打印结果我就直接在 NSLog 后面以注释的形式给出)

    #import <Foundation/Foundation.h>
    #import "JJPerson.h"
    
    int main(int argc, const char * argv[]) {
       
    //    创建一个对象,则对象的引用计数为1
        JJPerson *person = [[JJPerson alloc] init];
        NSLog(@"%zd",[person retainCount]);//    打印“1”
    
        
    //    一次retain操作,引用计数+1
        [person retain];
        NSLog(@"%zd",[person retainCount]); //    打印“2”
    
    //    一次release操作,引用计数-1
        [person release];
        NSLog(@"%zd",[person retainCount]);//    打印“1”
    
        return 0;
    }
    

    小结:
    1、创建一个对象,则对象的引用计数为1
    2、一次retain操作,引用计数+1
    3、一次release操作,引用计数-1

    二、野指针 和 空指针

    当我们将引用次数为1的对象再做一次release操作,再打印,会怎么样?
    答:person指针所指的对象,也就是堆内存上的对象会被回收,此时的person称为野指针,调用任何方法会使程序崩溃。

    我们接下去看代码:

    #import <Foundation/Foundation.h>
    #import "JJPerson.h"
    
    int main(int argc, const char * argv[]) {
       
    //    创建一个对象,则对象的引用计数为1
        JJPerson *person = [[JJPerson alloc] init];
        NSLog(@"%zd",[person retainCount]);//    打印“1”
    
    //    一次retain操作,引用计数+1
        [person retain];
        NSLog(@"%zd",[person retainCount]); //    打印“2”
    
    //    一次release操作,引用计数-1
        [person release];
        NSLog(@"%zd",[person retainCount]);//    打印“1”
    
    //    一次release操作,引用计数-1
        [person release];
        NSLog(@"%zd",[person retainCount]);//   打印“1”,为什么?
    
        return 0;
    }
    

    为什么和我刚刚说的不一样?不仅没报错而且还打印“1”?
    答: 默认情况下,Xcode是不会管僵尸对象(已经被销毁的对象)的,使用一块被释放的内存也不会报错。为了方便调试,应该开启僵尸对象监控。


    第一步.png 第二步.png

    然后程序就“正常的”如我们所料的报错了。
    2018-05-24 16:09:28.674424+0800 引用计数[1100:75090] *** -[JJPerson test]: message sent to deallocated instance 0x1004128a0
    编译器说不允许发送消息给已经释放的对象,所以说当野指针 person 还想使用[person retainCount] 这个方法的时候,程序就崩溃了。

    既然野指针不行,那我们让 person = nil , 再调用方法,会不会崩溃?
    答:不会。person = nil,那么person指针就称为空指针,空指针可以调用对象方法,但空指针调用方法时什么都不做,毕竟原来指向的那个对象完了当然就没办法干活了,而且也不会报错。(我们可以自己在 JJPerson 类里写一个测试方法,然后在main.m 上调用一下,结果就是前面我说的”什么都没发生“,在这里就不上代码了)

    小结:
    内存管理不当:
    1、不再使用的对象未被回收,就会造成内存泄漏,程序会闪退
    2、正在使用的对象被回收,会造成野指针,访问野指针会造成程序崩溃
    3、空指针可以调用对象方法,但空指针调用方法时什么都不做,而且也不会报错

    三、dealloc

    当对象的 retainCount = 0 时,便会自动调用NSObject的方法 dealloc 回收堆内存,我们可以在 JJPerson类中重写这个方法,见如下代码:

    #import "JJPerson.h"
    @implementation JJPerson
    
    -(void)dealloc {
        
        NSLog(@"对象被释放了");
      
        [super dealloc];
    }
    @end
    

    是不是很奇怪 [super dealloc] 要放在最后,这是因为NSObject 的 dealloc 才是真正进行内存回收的代码,如果你一开始就调用了 [super dealloc] ,那就没有后面的什么事了,方法肯定不会往下走, NSLog(@"对象被释放了")就别想打印出来了。
    相似的道理,dealloc 这个方法应该避免主动调用,它是个回收对象的方法,你想想你一个对象调用它,是不是很矛盾,这样做也是防止带来许多的问题。

    那么当我在 person 的 retainCount = 1 的时候,再次调用 [person release] ,出来的结果是:
    2018-05-24 17:36:01.154622+0800 引用计数[1844:181200] 对象被释放了
    说明系统已经自动调用了dealloc 方法回收内存了。

    小结:
    1、对象的引用计数为0 ,自动通过 dealloc 方法回收内存
    2、[super dealloc] 才是真正回收内存的方法,必须在dealloc 方法的最后调用

    相关文章

      网友评论

        本文标题:ios开发基础学习笔记(八)--MRC的引用计数

        本文链接:https://www.haomeiwen.com/subject/rhedjftx.html