美文网首页
006-内存管理

006-内存管理

作者: Yasic | 来源:发表于2017-11-26 19:17 被阅读9次

    内存管理

    对于面向对象语言,离不开对象的声明和操作,而本质上来说对象就是在内存里开辟的一段空间,用来放置数据和方法。对于这段内存空间的声明周期需要进行管理,objectivec 中大致有三种机制来管理内存空间。

    • MRC 手动内存管理
    • ARC 自动内存管理
    • 垃圾回收机制

    其中垃圾回收机制在 iOS 中是不支持的,而 iOS 5 所引入的 ARC 是目前最推荐的内存管理机制,MRC 则是早期 objectivec 所采取的方式。

    一、对象的生命周期

    因为要管理对象的内存,因此有必要知道 objectivec 中的一个对象从创建到销毁具体经历了哪些过程。

    1. 创建

    完整流程

    在对象的创建过程中,系统在栈区开辟空间存储当前对象的指针变量,在堆区中开辟空间存储对象数据,并将栈区的指针指向堆区中的内存空间。objectivec 采用两段式构造模式,这一点在《对象与使用》中已经提到过

    Animal * animal = [[Animal alloc]init];
    

    而具体来说这里其实调用了 alloc 和 init 两个方法。

    alloc 方法

    该方法是一个类方法,将会在堆分区中为需要创建的对象开辟合适的内存空间,同时返回一个未被 “初始化” 的对象,具体来说做了以下事情。

    • 将新对象的引用计数加1
    • 将新对象的 isa 成员变量指向它的类对象
    • 将该新对象的所有其它成员变量的值设置成零。(根据成员变量类型,零有可能是指 nil 或 Nil 或 0.0)

    所以 alloc 返回的对象的成员变量还没有初始化,因此需要调用 init 方法。

    init 方法

    init 方法对 alloc 返回的对象进行真正的初始化工作。

    在某些情况下,init 会造成 alloc 的原本空间不够用,从而二次分配空间,因此 alloc 和 init 需要放在一行调用。另外,基于类簇的概念,init 方法会根据接收到的参数决定返回什么样的对象更合适,所以不应分开使用这两个方法。

    new 方法

    new 可以简单理解为 alloc 与 init 方法的联合调用。

    2. 销毁

    如前面所述,当对象的引用计数为 0 时将会被系统销毁,具体来说会调用该对象的析构函数—— dealloc 方法。在 dealloc 方法中会释放对象所分配的资源及其成员变量关联的内存。一旦重写 dealloc 方法就必须在代码块最后调用 [super dealloc] 方法,调用 [super dealloc] 方法是为了让 super 释放相应的资源。同时不要自己调用 dealloc 方法。

    二、引用计数

    如何判断一个对象是否需要被回收有很多策略,objectivec 采用的是引用计数机制,为每一个对象绑定一个NSUInteger 类型的整数来表示该对象的引用次数,即当前有多少引用者正在引用此对象。如果把引用计数理解为一种拥有权,那么引用计数机制是在维护一个对象目前正在被多少指针变量拥有。

    这样,如果一个对象的引用次数不为0,则表示当前存在指针变量正在拥有这一对象,所以不能回收。一旦对象引用次数为0,则表示该对象可以被销毁回收了。

    引用计数机制的作用在于它解决了从其他方法获得的对象的内存管理权的转移问题。当对象 A 将一个对象 N 发送给对象 B 以后,B 如果需要长时间使用则通过增加对象 M 的引用计数获得拥有权,这样对象 A 可以安全地放弃对象 M 的引用不用担心因此而使 M 被回收从而 B 在使用时发生异常。

    相关操作

    对于对象引用计数有改变的操作主要有

    对象 方法 引用计数变化
    生成并持有对象 alloc/init/new/copy/mutableCopy等 +1
    持有对象 retain +1
    释放对象 release -1
    废弃对象 dealloc 在调用release时间接调用

    管理原则

    • 自己生成的对象自己持有

        Cat * cat = [[Cat alloc] init];
      
    • 非自己生成的对象也可以持有

          id s = [NSArray array];
          [s retain];
      
    • 不再需要持有的对象自己释放

          id s = [NSArray array];
          [s retain];
          [s release];
      
    • 非自己持有的对象无法释放

        Cat * cat = [[Cat alloc] init];
          cat.name = @"Tom";
          [cat getName];
          [cat getRealName];
          [cat release];
          [cat release];  //不再持有,所以不能继续释放
          [cat getName];
      

    自动释放 autorelease

    release 会立即释放当前对象的引用,而 autorelease 是一种预约式的释放,会在一次 runloop 结束后释放引用。之所以要使用自动释放是因为很多时候我们不想关心或者不能确定当前对象的具体释放时间,使用自动释放可以延迟对象的释放时机。

    Cat * cat = [[[Cat alloc] init] autorelease];
    

    自动释放实际上是将该对象放入当前 Autorelease Pool 中,在当前 Autorelease Pool 释放时,会对添加进该 Autorelease Pool 中的对象逐一调用 release 方法。

    自动释放在返回对象时有用,

    +(Animal *)getAnimal
    {
        Animal * animal = [[Animal alloc] init];
        return animal;
    }
    

    由于 alloc 和 init 都会将引用次数加一,因此返回的 animal 的引用次数比预期多1,但是调用者不知道这一细节,所以需要方法内进行释放。但调用 release 释放是立刻执行的,因此将导致无法返回有效的对象,这时就需要 autorelease 将其放入自动释放池中,能保证调用者获得一个可以使用的对象。

    这里要注意

    • 会将调用 autorelease 方法的对象放入自动释放池中
    • 当 pool 销毁时,池中记录的所有对象都会被发送 release 消息;
    • 调用 autorelease 方法不影响引用计数器的计数值,返回对象本身

    自动释放池有两种实现方式

    • 采用 NSAutoreleasePool,只支持 MRC 模式

          NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
          [pool release];
      
    • 采用 @autoreleasepool,ARC 和 MRC 都支持

          @autoreleasepool {
              Cat * cat = [[Cat alloc] init];
          }
      

    三、ARC 自动引用计数机制

    垃圾回收机制是在 objectivec 中引入的特性,类似于 Java 中的自动垃圾回收,但是在 iOS 中从未支持过,因此不考虑。MRC 手动内存管理就是指通过上面提到的 retain、release 等方式进行手动的内存管理,但是会产生“野指针”和“内存泄漏”等问题。

    • 野指针

      野指针是指所定义的指针未初始化或者指针指向了一处已被释放的内存空间。

        Cat * cat = [[[Cat alloc] init] autorelease];
          [cat release];
          [cat release];
      

      在这里,第一次 release 时 cat 指针指向的内存空间已经被释放了,所以再次 release 会报错

      Cat object 0x1004032b0 overreleased while already deallocating; break on objc_overrelease_during_dealloc_error to debug
      

      这里就是指针对一处已被释放的内存空间进行操作的问题,它可能会引发崩溃。

      之所以说“很可能”没有说一定,是因为对象在堆区所占的内存在“解除分配”(deallocated)之后只是放回到“可用内存池”,如果此时执行NSLog时尚未覆盖对象内存,那么该对象依旧有效,这时程序不会奔溃。由此可见,因过早释放对象而导致的bug很难调试。

      所以在 release 之后应当及时将指针清空

          Cat * cat = [[[Cat alloc] init] autorelease];
          [cat release];
          cat = nil;
          [cat release];
      

      这时对 cat 进行 relase 时,由于 objectivec 规定在 nil 对象上进行操作的结果是不作为,所以不会报错或者崩溃。

    • 内存泄漏

      {
          Cat * cat = [[[Cat alloc] init] autorelease];
      }
      

      由于 cat 指针在代码块结束后就从栈中被释放了,所以在堆区开辟的内存空间就成了泄漏的内存空间。

    ARC作为WWDC2011和iOS5之后LLVM 3.0编译器的一项新特性,其本质是由编译器在编译阶段在程序合适位置加入引用计数操作代码,所以为了保证这一机制能够正常运行,在 ARC 模式下 xcode 不允许在代码中加入类似 retain、release 等操作 ,也禁止复写内存管理方法。

    具体来说,ARC 会操作的地方有

    • 插入 retain 方法:
      • 将对象引用赋值给其他变量或常量
      • 将对象引用赋值给其他属性或实例变量
      • 将对象传递给函数参数,或者返回值
      • 将对象加入集合中
    • 插入 release 方法:
      • 将局部变量或全局变量赋值为 nil 或其他值
      • 将属性赋值为 nil 或其他值
      • 实例属性所在的对象被释放
      • 参数或局部变量离开函数
      • 将对象从集合中删除

    内存管理与属性特性

    修饰属性的部分属性特性在内存管理里有很大关联。

    • strong:ARC 中引入,成员变量为强指针类型,类似 MRC 中的 retain,将传入对象引用计数加一,针对 OC 对象
    • weak:成员变量为弱指针类型,类似 MRC 中的 assign,不释放旧值,不改变引用计数,当对象被释放时,指针指向nil,针对需要避免保留循环的情况使用
    • assign:成员变脸为值类型,也是默认类型
    • retain:将传入对象引用计数加一,获得该对象拥有权
    • copy:将旧的值 release 后复制新的值,开辟一个新的内存空间放置赋值的对象,新对象引用计数为1,原对象引用计数不变
    • unsafe_unretained:非持有关系,但是与 weak 相比,不会自动 nil 化,容易造成野指针问题

    指针类型

    与属性特性类似,ARC 的指针也有内存管理的特性

    • __strong:默认标识符,只要存在一个,对象就不会被回收

      Animal * __strong animal = [[Animal alloc] init];
      
    • __weak:不保持对象的存活,当对象被回收,指针被置为 nil

    • __unsafe_unretained:与上一条类似,但是当对象回收时不会被置为 nil

    • __autoreleasing:一般结合自动释放池使用,可以在自动释放池的 runloop 结束时回收对象空间,但容易造成野指针问题

          Animal * __autoreleasing animal;
          @autoreleasepool {
              animal = [[Animal alloc] init];
          }
          NSLog(@"%@", animal);
      

    相关文章

      网友评论

          本文标题:006-内存管理

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