• OC-内存管理


    解铃还须系铃人

    --1--内存管理的原理及分类
    1.1 内存管理的原理
    1.2 内存管理的分类
    --2--手动内存管理
    2.1 关闭ARC的方法
    2.2 手动管理(MRC快速入门
    --3-- 内存管理的原则
    3.1 内存管理的原则
    3.2 内存管理研究的内容
    --4-- 单对象内存管理
    4.1 单个对象的野指针问题
    4.2 避免使用僵尸对象的方法
    4.3 对象的内存泄漏
    --5-- 多个对象内存管理
    5.1 多个对象的野指针问题
    5.2 多个对象内存泄漏问题
    --6-- set方法内存管理
    6.1 set方法存在的问题
    6.2 不同类型的setter写法
    --7-- autorelease
    7.1 autorelease是什么
    7.2 为什么会有autorelease
    7.3 autorelease基本用法
    7.4 autorelease的原理
    7.5 autorelease什么时候被释放
    7.6 autorelease使用注意

    --------------------------------------

    【写在开头】

    『使用这个标题,“解铃还须系铃人”好像有点不正式。但这里,只是想突出一个内存管理的原则:“谁创建,谁释放”。iOS的内存管理和Java等语言的垃圾回收机制不同,Java的垃圾回收机制是运行时的特性,由jvm去回收释放内存。这里不谈Java,回到iOS的内存管理,目前创建项目默认就是ARC机制,而以前是MRC机制,需要程序员自己手动去回收释放内存,注意ARC和MRC都是编译器的特性,一个移动设备的内存是有限的,所以学习iOS的内存管理非常必要。

    OC内存管理的范围:

    管理任何继承自NSObject的对象,对其他的基本数据类型无效。

    本质原因是因为对象和其他数据类型在系统中的存储空间不一样,如局部变量最主要存放在栈中。而对象存储在堆中,当代码块结束时,这个代码块中涉及的所有的局部变量会被回收,指向对象的指针也被回收,此时对象已经没有指针指向,但依然存在于内存中,就会造成内存泄漏。

    下面的内存管理内容主要是MRC机制』

     

     

    --1--内存管理的原理及分类

    1.1 内存管理的原理

    1)对象的所有权及引用计数

      对象所有权概念:

      任何对象都可能拥有一个或多个所有者。只要一个对象至少还拥有一个所有者,它就会继续存在。

      Cocoa所有权策略:

      任何自己创建的对象都归自己所有,可以使用名字以“alloc"或”new“开头或名字中包含”copy“的方法创建对象,可以使用retain来获得一个对象的所有权。

      对象的引用计数器:

      每个OC对象都有自己的引用计数器,是一个整数表示对象被引用的次数,即现在有多少东西在使用这个对象。对象刚被创建时,默认计数器值为1,当计数器的值变为0时,则对象销毁。

    2)引用计数器的作用

      引用计数器(retainCount):标志被引用的次数,存储当前对象有几个使用者,是判断对象是否回收的依据。

      (例外:对象值为nil时,引用计数器为0,但不回收空间,因为没有分配空间)

     

    3)对引用计数器的操作:

      retain消息:使计数器+1,方法返回对象本身

      release消息:使计数器-1

      retainCount消息:获得当前对象的retainCount的值

     

    4)对象的销毁:

      当retainCount为0时,对象被销毁,内存被回收。对象被销毁时,系统自动向对象发送一条dealloc消息,一般会重写dealloc消息,在这里释放相关的资源,dealloc就像是对象的“临终遗言”。

      一旦重写了dealloc方法就必须调用[super dealloc](注意不能直接调用dealloc方法)。

      一旦对象被回收了,那么他所占据的存储空间就不再可用,坚持使用会导致程序崩溃(野指针错误)。

    注意:

    1)如果对象的计数器不为0,那么在整个程序运行过程中,它占用的内存就不可能被回收(除非整个程序已经退出)

    2任何一个对象,刚创建的时候,引用计数器都为1,当使用alloc、new或者coppy创建一个对象时,对象的引用计数器默认是1

    从此可以看出:

    回收一个对象与否的标记就是引用计数器是否为0.

    1.2 内存管理的分类

    OC内存管理分为3类:

      1)Manual Reference Counting (MRC,手动管理,在开发iOS4.1之前的版本的项目时,需要自己负责使用引用计数来管理内存,比如要手动retain、release、autorelease等,而在其后的版本可以使用ARC,让系统自己管理内存

      2)Automatic Reference Counting(ARC,自动引用计数,iOS4.1之后推出

      3)garbage collection(垃圾回收)IOS不支持垃圾回收

    ARC作为苹果新提供的技术,苹果推荐开发者使用ARC技术来管理内存。

    --2--手动内存管理

     2.1 关闭ARC

    创建一个项目时,默认是ARC的(自动内存管理),手动把ARC项目改成MRC项目的方法之一如下:

    a.选中项目,此时Xcode右侧会出现如图设置信息

    b. 在同时选中Build Settings和Levels时在右侧搜索框搜索auto,此时会出现ARC设置

    c.把ARC设置中目标target下的Yes设置为No,其他相应的Yes也变为No,此时就是MRC管理

    2.2 手动管理(MRC快速入门

    内存管理的关键是判断对象是否被回收?

    重写dealloc方法,

    代码规范

    1)要调用父类的dealloc方法[supper dealloc]而且要放到最后,意义是:先释放子类占用的空间再释放父类占用的空间

    2)对self(当前)所拥有的其他对象做一次release操作

     -  (void) dealloc
    
    {
    
    [_car release]; //对象关联的对象
    
    [super dealloc];
    
    }

    注意:

    一旦对象被回收了,它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针)为了防止调用出错,也可以将“野指针”指向nil(0);

    --3-- 原则 

    3.1 内存管理的原则

    1原则:

      如果对象还有使用者,就不应该回收;

      如果你想使用这个对象,那么就应该让这个对象的引用计数器+1

      当你不想使用这个对象时,应该让对象的引用计数器-1

    2谁创建,谁release

      1.如果通过alloc new, coppy来创建一个对象,那么久必须调用对应的release或者autorelease方法

      2.谁创建谁负责

    3谁retain,谁release

      只要你调用了retain无论这个对象是如何生成的,你都需调用release

    总结:

    有始有终,有加就应该有减,让某个对象计数器加1,就应该让其在最后-1

    对象如果不再使用了,就应该回收它的空间,防止造成内存泄漏

     

    3.2 内存管理研究的内容

    1)野指针(僵尸对象)

    野指针:

      1)定义的指针变量没有初始化

      2)指向的空间已经被释放了

    2)内存泄漏 

    内存泄漏:

    Person *p = [Person new];
    
    //变量p存储在栈区
    
    //[Person new]; 对象存储在堆区
    
    如果栈区的p已经被回收,而堆区的空间还没有释放,堆区的空间就造成了泄漏

    --4-- 单对象内存管理

    4.1 单个对象的野指针问题

    野指针错误:访问了一块不可再用的内存

    僵尸对象:所占的内存已经被回收的对象,僵尸对象不能再被使用。(默认情况下Xcode为了提高编码效率,没有开启僵尸对象检测。若要检测,需先打开僵尸对象检测)

    空指针:没有指向任何对象的指针: Person *p = nil;

       给空指针发送消息不会有反应

    4.2 避免使用僵尸对象的方法

    为了防止不小心调用了僵尸对象,可以将对象赋值nil对象的空值)

    Dog *dog = [[Dog alloc] init];
    
    [dog release]; //引用计数-1
    
    dog = nil; //空指针
    
    [dog retain]; //给nil发送任何消息,都不会有效果。所以僵尸对象检测不会报错

    4.3 对象的内存泄漏

    情景1

    //创建完成使用后,没有release
    
    Dog *d = [Dog new]; //1
    //如果对象没有被回收,就造成了内存泄漏

    情景2

    没有遵守内存管理的原则

    Dog *d = [Dog new]; //引用计数为1
    
    [d retain]; // 计数+1 = 2
    
    [d release]; //计数 - 1 = 1
    //计数不为0,不能被回收

     情景3

    不当的使用了nil

    Dog *d = [Dog new];
    
    d = nil; //指针指向nil
    
    [d run]; //nil run不会有效果
    
    [d relese]; //nil release
    
    //而此处不能回收,因为Dog对象的引用计数还是1

    情景4:

    在方法中对传入的对象进行了retain,而调用时没有release

    - (BOOL)compareColorWithOther:(Dog *)dog{
    
        [dog retain]; //此处对象引用计数 + 1
    
        return YES;
    
    }
    
    //而如果在后面没有相应的release,则同样会造成内存泄漏

    --5-- 多个对象内存管理

    5.1 多个对象的野指针问题

    /**
    情景:
    Person对象拥有一个Car的对象属性
    */
    Person *person = [[Person alloc] init]; //引用计数为1
    Car *car = [[Car alloc] init]; //计数为1
            
    car.speed = 180; //car对象拥有速度属性
            
    person.car = car; //将Person对象属性赋值为car
            
    [car release]; //car对象计数释放一次 - 1 -->变成了0
    
    [person goByCar]; //此时car变成了一个僵尸对象-->car已经是野指针。但是goByCar中还使用着car对象,这样开启检测后就会抛出运行时错误

    5.2 多个对象内存泄漏问题

    针对 5.1 中存在的问题。

    如果在[car  release]之后,不小心再使用了car对象,就会造成野指针访问错误。所以此处解决这个问题的方法是,在Person类中重写setter方法,在setter方法中将传过来的car对象retain一次。

    这样,调用时,[car release]后还可以使用car对象,并在Person的dealloc中将_car对象release一次。

    只是这样的解决方法还是不严谨的,正确的写法应该是下面 6.2 中的写法。

    --6-- set方法内存管理

    6.1 set方法存在的问题

    原对象无法释放造成了内存泄漏

    根据内存管理的原则:

    谁创建,谁release

    Car *car = [[Car alloc] init];
    
    Car *bmw = [[Car alloc] init]; //新对象
    
    //谁创建,谁release
    [bmw release]; 
    [car release]; 

    按照 5.2 中的写法,直接在setter方法中将对象的引用计数+1。但像这样有两个对象时,那么旧对象就不能释放了,因为旧对象在setter中之前就retain了一次,变成了2,而在Person中只是将包含的_car对象release一次,这样旧对象就又造成了内存泄漏。 

    所以setter中正确的写法应该是这样:

    - (void)setCar:(Car *)car{
    
        if (_car != car){ //如果是新的对象,则先把旧的对象release一次,再将新的对象赋值_car
    
        [_car release]; //初次是[nil release];
    
        _car = [car retain]; //再让新的对象引用计数+1
    
        }
    
    }

    Person的dealloc中,只要让成员对象release一次就行了

    - (void)dealloc{
    
        [_car release]; //relase成员属性
    
        NSLog(@"Person被回收");
    
        [super dealloc];
    
    }

    6.2 不同类型的setter写法

    1基本数据类型:直接赋值(因为基本数据类型由系统回收)

    //基本数据类型直接赋值即可
    //int  float  double  long  struct  enum
    
    -(void)setAge:(int)age
    
    {
    
        _age = age;
    
    }

    2OC对象类型

    //对象类型需手动释放其内存
    -(void)setCar:(Car *)car
    
    {
    
        //1.先判断是不是新传进来的对象
    
        if (car != _car){
    
        //2.对旧对象做一次release
    
        [_car release]; //若没有旧对象,则没有影响
    
        //3.对新对象做一次retain,并且赋值给实例变量
    
        _car = [car retain];
    
        }
    
    }

    总结:

    setter的内存管理:

    原则:如果在一个类中有其他类的对象(关联关系),在setter中,要判断是否是同一个对象。如果是不同对象则先release旧值,再retain新值。

     

     

    --7-- @autoreleasepool

    7.1 @autoreleasepool是什么

    autoreleasepool自动释放池

    (1)在iOS程序运行过程中,会创建无数个“池子”,这些“池子”以栈结构的形式存在。

    (2)当一个对象调用autorelease时,会将这个对象放到位于栈顶的释放池中 。

    自动释放池的创建方式

    1)iOS5.0以前的创建方式

    NSAutoreleasePool *pool=[[NSAutoreleasePool alloc]init];
    
    ……………….
    
    [pool release];//[pool drain];用于mac

    2)iOS5.0以后

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

    autorelease

    是一种支持引用计数的内存管理方式

    它可以暂时的保存某个对象(object),然后在内存池自己的排干(drain)的时候对其中的每个对象发送release消息。

    注意,这里只是发送release消息,如果当时的引用计数(reference-counted)依然不为0,则该对象依然不会被释放。可以用该方法来保存某个对象,但也要注意保存之后要释放该对象。

    7.2 为什么会有autorelease

    OC的内存管理机制中比较重要的一条规律是:谁申请,谁释放

    考虑这种情况,如果一个方法需要返回一个新建的对象,该对象何时释放?

    方法内部是不会写release来释放对象的,因为这样做会将对象立即释放而返回一个空对象:调用也不会主动释放该对象的,因为调用者遵循“谁申请,谁释放”的原则,那么这个时候,就发生了内存泄露。

    不使用autorelease存在的问题

    针对这种情况,Objective-C的设计了autorelease,既能确保对象能正确释放,又能返回有效的对象。

    使用autorelease的好处

    (1)不需要再关心对象释放的时间

    (2)不需要再关心什么时候调用release

     

    7.3 autorelease基本用法

    基本用法

    (1)会将对象放到一个自动释放池中

    (2)当自动释放池被销毁时,会对池中的所有对象发送一次release

    (3)返回对象本身

    (4)调用完autorelease方法后,对象的计数器不受影响(销毁时影响)

     

    在autorelease的模式下,下述写法是合理的,即可以正确返回结果,也不会造成内存泄露

    ClassA *Func1(){
    
    Class *obj=[[ClassA alloc]init autorelease];
    
    return obj;
    
    }
    int main(int argc, const char * argv[]) {
        Person *p = [[Person alloc] init];
        @autoreleasepool {
            
            //注意:加入到自动释放池中以后,引用计数不会变化
            [p autorelease]; //把对象p加入到自动释放池中
            NSLog(@"p.retainCount = %lu", p.retainCount); //1
            //[p release]; //无需手动release
        }
        return 0;
    }

    重写delloc

    @implementation Person
    
    //重写dealloc
    - (void)dealloc{
        NSLog(@"%@对象释放", self);
        [super dealloc];
    }
    @end

    输出-->

    7.4 autorelease的原理

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

     

    7.5 autorelease什么时候被释放

    对于autorelease pool本身,会在如下两个条件发生时候被释放(详细信息请参见第5条)

    (1)手动释放Autorelease pool

    (2)Runloop结束后自动释放

    对于autorelease pool内部的对象

    在引用计数的retain==0的时候释放。

    release和autorelease pool的drain都会触发retain–事件。

     

    7.6 autorelease使用注意

    1)并不是放到自动释放池代码中,都会自动加入到自动释放池

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

    2)在自动释放池的外部发送autorelease 不会被加入到自动释放池中

    autorelease是一个方法,只有在自动释放池中调用才有效。

    @autoreleasepool {
     }
     // 没有与之对应的自动释放池, 只有在自动释放池中调用autorelease才会加入释放池
     Person *p = [[[Person alloc] init] autorelease]; //没有写在自动释放池内,写在了自动释放池外
     [p run];
    
    
    // 正确写法1
      @autoreleasepool {
        Person *p = [[[Person alloc] init] autorelease];
     }
    
    // 正确写法2
     Person *p = [[Person alloc] init];
      @autoreleasepool {
        [p autorelease];
     }

     自动释放池的嵌套使用

    • 自动释放池是以栈(栈结构)的形式存在的
    • 由于栈只有一个入口, 所以调用autorelease会将对象放到栈顶的自动释放池
      •   >栈顶是离调用autorelease方法最近的自动释放池
    @autoreleasepool { // 栈底自动释放池
    
        @autoreleasepool {
    
            @autoreleasepool { // 栈顶自动释放池
    
            Person *p = [[[Person alloc] init] autorelease];
            }
         Person *p = [[[Person alloc] init] autorelease];
         }
     }            
    • 自动释放池中不适宜放占用内存比较大的对象
      • 尽量要避免将大内存对象加入释放池,因为释放池的延迟释放机制,会使其一直常驻内存
      • 不要把大量循环操作放到同一个 @autoreleasepool 之间,这样会造成内存峰值的上升
        // 内存峰值会上升
        @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];
            } //创建完一个对象就释放了
        }

    【写在结尾:】

    『学习如逆水行舟,不进则退』

    ☂业精于勤,荒于嬉;行成于思,毁于随。☂
  • 相关阅读:
    css选择器
    HTML标签用法
    pyenv python 版本控制
    Django之路
    Day15-Django
    python+selenium实现登录账户
    requests and BeautifulSoup
    清除MAC 可清除空间
    将python源文件打包成exe文件
    swift的一些东西
  • 原文地址:https://www.cnblogs.com/wang-biao/p/5663905.html
Copyright © 2020-2023  润新知