长路漫漫,唯剑作伴--Automatic Reference Counting

时间:2021-11-24 16:41:47

一、引用计数

  1. 在OC中,对象什么时候会被释放?

    • 答案是当对象没有被任何变量引用(也可以说是没有指针指向该对象)的时候,就会被释放。
  2. 怎么知道对象已经没有被引用了呢?

    • OC采用引用计数(reference counting)的技术来进行管理:

      • 每个对象都有一个关联的整数,称为引用计数器

      • 当代码需要使用该对象时,则将对象的引用计数加1

      • 当代码结束使用该对象时,则将对象的引用计数减1

      • 当引用计数的值变为0时,表示对象没有被任何代码使用,此时对象将被释放。

    • 内存管理的思考方式:

      • 自己创建的对象自己管理。

      • 不是自己创建的对象,自己也能持有。

      • 不再需要自己持有的对象时,释放。

      • 不是自己持有的对象,不能释放。

      • 总结一句话就是:谁创建,谁释放,谁持有,谁管理

    • 与之对应的消息发送方法如下:

      • 增加引用计数:alloc、new、copy、mutableCopy(此为生成),retain(此为持有对象)

      • 减少引用计数:release(此为释放对象)

      • 释放dealloc(此为废弃对象)

    • 下面通过一个简单的例子说明:

      • 新建Dog类,重写其创建和销毁的方法:

        @implementation Dog
         
            - (instancetype)init {
                if (self = [super init]) {
                    NSLog(@"小狗被派出去啦!初始引用计数为 %ld",self.retainCount);
                }
                return self;
            }
         
            - (void)dealloc {
                NSLog(@"小狗回到宠物中心");
                [super dealloc];
            }
        @end
      • 在main方法中创建dog对象,給dog发送消息:

        //模拟:宠物中心派出小狗
        Dog * dog = [[Dog alloc]init];
        //模拟:xiaoming需要和小狗玩耍,需要将其引用计数加1
        [dog retain];
        NSLog(@"小狗的引用计数为 %ld",dog.retainCount);
        //模拟:xiaoming不和小狗玩耍了,需要将其引用计数减1
        [dog release];
        NSLog(@"小狗的引用计数为 %ld",dog.retainCount);
        //没人需要和小狗玩耍了,将其引用计数减1
        [dog release];
        //将指针置nil,否则变为野指针
        dog = nil;
      • 输出结果为:

        [34691:7638855] 初始引用计数为 1
        [34691:7638855] 小狗的引用计数为 2
        [34691:7638855] 小狗的引用计数为 1
        [34691:7638855] 销毁Dog
      • 可以看到,引用计数帮助宠物中心很好的标记了小狗的使用状态,在完成任务的时候及时收回到宠物中心。

    • 思考几个问题:

      • NSString引用计数问题:

        NSString * str = @"hello guys";
        NSLog(@"%ld", str.retainCount);
        // 会发现引用计数为-1,这可以理解为NSString实际上是一个字符串常量,是没有引用计数的(或者它的引用计数是一个很大的值(使用%lu可以打印查看),对它做引用计数操作没实质上的影响)。
      • 赋值不会拥有某个对象:

        NSString * name = dog.name;
        // 这里仅仅是指针赋值操作,并不会增加name的引用计数,需要持有对象必须要发送retain消息。
      • dealloc:

        • 由于释放对象是会调用dealloc方法,因此重写dealloc方法来查看对象释放的情况,如果没有调用则会造成内存泄露。在上面的例子中我们通过重写dealloc让小狗被释放的时候打印日志来告诉我们已经完成释放。

      • 在上面例子中,如果我们增加这样一个操作:

        //没人需要和小狗玩耍了,将其引用计数减1
        [dog release];
        NSLog(@"%ld",dog.retainCount);
        // 会发现获取到的引用计数为1,为什么不是0呢?
        // 这是因为对引用计数为1的对象release时,系统知道该对象将被回收,就不会再对该对象的引用计数进行减1操作,这样可以增加对象回收的效率。

 二、自动释放池

  1. 思考

    • 现在已经明确了,当不再使用一个对象时应该将其释放,但是在某些情况下,我们很难理清一个对象什么时候不再使用(比如xiaoming和小狗玩耍结束的时间不确定),这可怎么办?

    • ObjC提供autorelease方法来解决这个问题,当給一个对象发送autorelease消息时,方法会在未来某个时间給这个对象发送release消息将其释放,在这个时间段内,对象还是可以使用的。

  2. 那autorelease的原理是什么呢?

    • 原理就是对象接收到autorelease消息时,它会被添加到了当前的自动释放池中,当自动释放池被销毁时,会給池里所有的对象发送release消息。

  3. 创建

    • 方法一:使用NSAutoreleasePool来创建

      NSAutoreleasePool * pool = [[NSAutoreleasePool alloc]init];
      //这里写代码
      [pool release];
    • 方法二:使用@autoreleasepool创建

      @autoreleasepool {
          //这里写代码
      }
    • 自动释放池创建后,就会成为活动的池子,释放池子后,池子将释放其所包含的所有对象。

    • 以上两种方法推荐第一种,因为将内存交给ObjC管理更高效。

    • 自动释放池什么时候创建?

      • app使用过程中,会定期自动生成和销毁自动释放池,一般是在程序事件处理之前创建,当然我们也可以自行创建自动释放池,来达到我们一些特定的目的。

    • 自动释放池什么时候销毁?

      • 自动释放池的销毁时间是确定的,一般是在程序事件处理之后释放,或者由我们自己手动释放。

  4. 下面举例说明自动释放池的工作流程:

    • 代码

      //创建一个自动释放池
      NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
      //模拟:宠物中心派出小狗
      Dog * dog = [[Dog alloc]init];
      //模拟:xiaoming需要和小狗玩耍,需要将其引用计数加1
      [dog retain];
      NSLog(@"小狗的引用计数为 %ld",dog.retainCount);
      //模拟:xiaohong需要和小狗玩耍,需要将其引用计数加1
      [dog retain];
      NSLog(@"小狗的引用计数为 %ld",dog.retainCount);
      //模拟:xiaoming确定不想和小狗玩耍了,需要将其引用计数减1
      [dog release];
      NSLog(@"小狗的引用计数为 %ld",dog.retainCount);
      //模拟:xiaohong不确定何时不想和小狗玩耍了,将其设置为自动释放
      [dog autorelease];
      NSLog(@"小狗的引用计数为 %ld",dog.retainCount);
      //没人需要和小狗玩耍了,将其引用计数减1
      [dog release];
      NSLog(@"释放池子");
      [pool release];
      //创建一个自动释放池
      @autoreleasepool {
          //模拟:宠物中心派出小狗
          Dog * dog = [[Dog alloc]init];
          //模拟:xiaoming需要和小狗玩耍,需要将其引用计数加1
          [dog retain];
          NSLog(@"小狗的引用计数为 %ld",dog.retainCount);
          //模拟:xiaohong需要和小狗玩耍,需要将其引用计数加1
          [dog retain];
          NSLog(@"小狗的引用计数为 %ld",dog.retainCount);
          //模拟:xiaoming确定不想和小狗玩耍了,需要将其引用计数减1
          [dog release];
          NSLog(@"小狗的引用计数为 %ld",dog.retainCount);
          //模拟:xiaohong不确定何时不想和小狗玩耍了,将其设置为自动释放
          [dog autorelease];
          NSLog(@"小狗的引用计数为 %ld",dog.retainCount);
          //没人需要和小狗玩耍了,将其引用计数减1
          [dog release];
          NSLog(@"释放池子");
      }
    • 结果

      [34819:7801589] 初始引用计数为 1
      [34819:7801589] 小狗的引用计数为 2
      [34819:7801589] 小狗的引用计数为 3
      [34819:7801589] 小狗的引用计数为 2
      [34819:7801589] 小狗的引用计数为 2
      [34819:7801589] 释放池子
      [34819:7801589] 销毁Dog
      // 可以看到,当池子释放后,dog对象才被释放,因此在池子释放之前,xiaohong都可以尽情地和小狗玩耍。
  5. 使用自动释放池需要注意:

    • 自动释放池实质上只是在释放的时候給池中所有对象对象发送release消息,不保证对象一定会销毁,如果自动释放池向对象发送release消息后对象的引用计数仍大于1,对象就无法销毁。

    • 自动释放池中的对象会集中同一时间释放,如果操作需要生成的对象较多占用内存空间大,可以使用多个释放池来进行优化。比如在一个循环中需要创建大量的临时变量,可以创建内部的池子来降低内存占用峰值。

    • autorelease不会改变对象的引用计数。

  6. 自动释放池的常见问题

    • 在管理对象释放的问题上,自动帮助我们释放池节省了大量的时间,但是有时候它却未必会达到我们期望的效果,比如在一个循环事件中,如果循环次数较大或者事件处理占用内存较大,就会导致内存占用不断增长,可能会导致不希望看到的后果。

    • 示例代码:

      for (int i = 0; i < 100000; i ++) {
          NSString * log  = [NSString stringWithFormat:@"%d", i];
          NSLog(@"%@", log);
      }
    • 前面讲过,自动释放池的释放时间是确定的,这个例子中自动释放池会在循环事件结束时释放,那问题来了:在这个十万次的循环中,每次都会生成一个字符串并打印,这些字符串对象都放在池子中并直到循环结束才会释放,因此在循环期间内存不增长。

    • 这类问题的解决方案是在循环中创建新的自动释放池,多少个循环释放一次由我们自行决定。

      for (int i = 0; i < 100000; i ++) {
          @autoreleasepool {
              NSString * log  = [NSString stringWithFormat:@"%d", i];
              NSLog(@"%@", log);
          }
      }
  7. 被autorelease处理过的对象的释放时机

    • autorelease并不是根据作用域来决定释放时机的,而是Runloop。当一个runloop结束时系统才会一次性清理掉被autorelease处理过的对象,其实本质上说是在本次runloop迭代结束时清理掉被本次迭代期间被放到autorelease pool中的对象的。至于何时runloop结束并没有固定的duration!

 

三、ARC

  1. 简介

    • ARC,自动引用计数,是指iOS的内存管理使用引用计数的技术。

    • 在OC中采用Automatic Reference Counting的机制,让编译器进行内存管理。在新一代的Apple LLVM编译器中设置ARC为有效状态,就不用再次键入retain、release代码,这在降低程序崩溃、内存泄漏等风险的同时,很大程度上减少了开发程序的工作量。编译器完全清楚目标对象,并能立刻释放那些不再被使用的对象(有待斟酌)。如此一来,应用程序将具有可预测性,且运行流畅,速度也将大幅提升。(摘自苹果官方文档说明)

  2. ARC修饰符

    • __strong:强引用,持有所指向对象的所有权,无修饰符情况下的默认值。如需强制释放,可置nil。

      • 比如我们常用的定时器:NSTimer * timer = [NSTimer timerWith...];

      • 相当于NSTimer * __strong timer = [NSTimer timerWith...];

      • 当不需要使用时,强制销毁定时器:[timer invalidate];timer = nil;

    • __weak:弱引用,不持有所指向对象的所有权,引用指向的对象内存被回收之后,引用本身会置nil,避免野指针。

      • __weak __typeof(self) weakSelf = self;

    • __autoreleasing:自动释放对象的引用,一般用于传递参数

    • __unsafe_unretained:为兼容iOS5以下版本的产物,可以理解成MRC下的weak,现在基本用不到,这里不作描述

    • 使用__autoreleasing可能会遇到哪些问题?

    • 使用修饰符的正确姿势

      NSString * __weak str = @"hehe"; // 正确!
      
      __weak NSString *str = @"hehe";  // 错误!
      
      // 我相信很多人都和我一样,从开始用ARC就一直用上面那种错误的写法。
      // 那这里就有疑问了,既然文档说是错误的,为啥编译器不报错呢?好吧,是苹果考虑到很多人会用错,所以在编译器这边贴心地帮我们忽略并处理掉了这个错误:)
      // 虽然不报错,但是我们还是应该按照正确的方式去使用这些修饰符
      // 如果你以前也常常用错误的写法,那看到这里记得以后不要这么写了,哪天编译器怒了,再不支持错误的写法,就要郁闷了。
      // (参见苹果官方文档)
  3. sasf