Objective-C高级编程之引用计数,看我就够了

自动引用计数.png

1.1 什么是自动引用计数

  • 概念:在 LLVM 编译器中设置 ARC(Automaitc Reference Counting) 为有效状态,就无需再次键入 retainrelease 代码。

1.2 内存管理 / 引用计数

1.2.1 概要

  • 引用计数就像办公室的灯的照明

    对照明设备所做的动作 对OC对象所做的动作
    开灯 生成对象
    需要照明 持有对象
    不需要照明 释放对象
    关灯 废弃对象
  • 其中,A生成对象时,引用计数为 1, 当多一个人需要照明,如B需要照明,则引用计数 +1, 以此类推。当A不需要对象,A释放对象,引用计数 -1.当最后一个持有对象的人都不要这个对象了,则引用计数变为 0,丢弃对象。

1.2.2 内存管理的思考方式

  • 客观正确的思考方式:

    • 自己生成的对象,自己所持有
    • 非自己生成的对象,自己也能持有
    • 不再需要自己持有的对象时释放该对象
    • 非自己持有的对象无法释放
    对象操作 OC方法
    生成并持有对象 alloc/new/copy/mutableCopy等
    持有对象 retain
    释放对象 release
    废弃对象 dealloc
  • 自己生成的对象,自己所持有:持有对象

    - (id) allocObject {
      // 自己生成并持有对象
      id obj = [[NSObject alloc] init];
      
      return obj;
    }
    
  • 需要注意的是: NSMutableArray 类的 array 方法取得的对象不是自己所持有的。其内部实现原理为:

    - (id)object {
        // 自己生成并持有对象
      id obj = [[NSObject alloc] init];
      
      // 将对象注册到 autoreleasepool 中, pool结束时会自动调用 release,这样的方法自己就不会持有对象。
      [obj autorelease];
      
      // 返回这个自己不持有的对象。
      return obj;
    }
    
  • 非自己生成的对象,自己也能持有:虽然一开始是不持有的,但是可以使用 retain 使其变成被自己所持有的,然后也可以使用 release 方法释放对象。

      // 取得非自己生成的对象
      id obj = [NSMutableArray array];
    
      // 取得的对象存在了,但是并非自己所持有的,引用计数还为 0, 但是该对象被放到了autoreleasepool 中,可以自动释放
      [obj retain];
      // 此时,自己就持有了这个对象,引用计数为 1
    
      [obj release];
      // 此时释放了这个对象,引用计数变为 0 ,对象就不可以再被访问了,但是对象也没有被立即废弃
    
  • 无法释放非自己持有的对象:例如

    // 取得非自己持有的对象
    id obj = [NSMutableArray array];
    
    [obj release];
    // 会导致程序崩溃
    
释放.png

1.2.3 alloc/retain/release/dealloc 实现

  • 分析 GNU 源码来理解 NSObject 类中的方法。

    • 首先是 alloc id obj = [[NSObject alloc] init];

      + (id)alloc {
      // alloc 在内部调用 allocWithZone
        return [self allocWithZone:NSDefaultMallocZone()];
      }
      
      + (id)allocWithZone:(NSZone *)zone {
      // allocWithZone 在内部调用 NSAllocateObject 
        return NSAllocateObject(self, 0, z);
      }
      
      struct obj_layout {
        NSUInteger retained;
      };
      
      inline id
      NSAllocateObject (Class aClass, NSUInteger extreBytes, NSZone *zone) {
        int size = 计算容纳对象所需内存的大小;
        // 分配内存空间
        id new = NSZoneMalloc(zone, size);
        // 将该内存空间中的值初始化为 0
        memset(new, 0, size);
        // 返回作为对象而使用的指针
        new = (id)&((struct obj_layout *) new)[1];
      }
      
      /**
      其中, NSZoneMalloc, NSDefaultMallocZone() 等名称中包含的 Zone 是为了防止内存碎片化而引入的结构。对内存分配的区域本身进行多重化管理,根据对象使用的目的,大小,分配内存,从而提高内存管理的效率。
      
      但是现在的运行时系统知识简单的忽略了区域的概念,运行时系统中的内存管理本身已经机具效率,再使用区域来管理内存反而会引起内存使用效率低下的问题。
      */
      
      
    • 去掉NSZone后简化的代码

      struct obj_layout {
        NSUInteger retained;
      };
      
      + (id)alloc {
        int size = sizeof(struct obj_layout) + 对象大小;
        // 这句的意思是,为 struct obj_layout 这个结构体分配一个 size 大小的内存空间,并且函数calloc()会将所分配的内存空间中的每一位都初始化为零,也就是这块内存中所有的值都为 0 
        struct obj_layout *p = (struct obj_layout *)calloc(1, size);
        // 返回该对象指针
        return (id)(p + 1);
      }
      
    • [obj retain]; 的实现

      - (id)retain {
        NSIncrementExtraRefCount(self);
      }
      
      inline void
      NSIncrementExtraRefCount(id anObject) {
        // 首先 (struct obj_layout *) anObject 找到的是这个对象的尾部, 所以需要 [-1] 减去该对象的大小,来寻址到该对象的头部,然后再判断该结构体中 retained 这个变量的值是否已经大于了系统最大值,如果没有,就 retained++, 使得引用计数 +1.
        if (((struct obj_layout *) anObject)[-1].retained == UINT_MAX - 1) {
          [NSException raise: NSInternalInconsistencyException format:@"NSIncrementExtraRefCount() asked to increment too far"];
          
          ((struct obj_layout *) anObject) [-1].retained++;
        }
      } 
      
    • [obj release] 的实现

      - (void)release {
        if (NSDecrementExtraRefCountWasZero(self)) {
          [self delloc];
        }
      }
      
      BOOL
      NSDecrementExtraRefCountWasZero(id anObject) {
        if (((struct obj_layout *) anObject)[-1].retained == 0) {
          return YES;
        } else {
          ((struct obj_layout *) anObject)[-1].retained--;
          return NO;
        }
      }
      
    • [obj dealloc]; 的实现

      - (void)dealloc {
        NSDeallocateObject(self);
      }
      
      inLine void
      NSDeallocateObject (id anObject) {
      // 指针 o 指向 anObject 的内存地址,然后释放这个指针指向的内存
        struct obj_layout *o = &((struct obj_layout *) anObject) [-1];
        free(o);
      }
      

1.2.4 苹果的实现

  • 首先看 alloc 的实现:

    // 依次调用这四个方法
    + alloc
    + allocWithZone:
    class_Instance
    calloc
    
  • retainCount / retain / release 的实现

    - retainCount
    __CFDoExtrernRefOperation
    CFBaseicHashGetCountOfKey
      
      
    - retain
    __CFDoExternRefOperation
    CFBasicHashAddValue;
    
    - release
    __CFDoExternRefOperation
    CFBasicHashRemoveValue
      
    // 这些函数的前缀 CF 表示他们都包含于 Core Foundation 框架的源代码中
    

    所以其内部实现可能如下:

    int __CFDoExternRefOperation(uintptr_r op, id obj) {
      CFBasicHashRef table = 取得对象的散列表(obj);
      int count;
      
      switch (op) {
        case OPERATION_retainCount:
          count = CFBasicHashGetCountOfKey(table, obj);
          return count;
        case OPERATION_retain:
          CFBasicHashAddValue(table, obj);
          return obj;
        case OPERATION_release:
          count = CFBasicHashRmoveValue(table, obj);
          // 如果count == 0, 返回 YES, 则会调用 dealloc
          return 0 == count;
      }
    }
    
      // 举例说明 retainCount
    
    - (NSUInteger)retainCount {
      return (NSUInteger)__CFDExternRefOperation(OPERATION_retainCount, self);
        }
    
    
  • 由此可看出,苹果在计数内部大概是以散列表的方式来管理引用计数的。复习散列表
  • 比较
    • 通过内存块头部管理引用计数的好处:

      • 少量代码即可完成

      • 能够统一管理引用计数需要的内存块和对象所用的内存块

    • 通过引用计数表管理引用计数的好处

      • 对象所用的内存块的分配不需要考虑它的头部(跟内存块头部管理引用计数相比,就是少了一个用来计数的头部)
      • 引用计数表各记录中存有内存块的地址,可以从各个记录追溯到各个对象的内存块。这使得计时出现故障导致了对象所占用的内存块损坏了,在 内存块头部管理引用计数 时,我们这样就没有办法访问这块内存了,但是在 引用计数表管理引用计数 时,我们就可以通过这个计数表来寻址内存块的位置。
      • 另外,在利用工具检测内存泄漏时,引用计数表也可以用来检测各个对象是否有持有者

1.2.5 autorelease

  • autorelease 会像 C语言 的自动变量一样来对待对象实例。当其超出作用域时,就会对对象进行release 的调用。

  • autorelease 的具体使用方法:

    • 生成并持有 NSAutoreleasePool 对象

    • 调用已经分配对象的 autorelease 实例方法

    • 废弃 NSAutoreleasePool 对象(对对象自动调用 release)

      // 代码如下
          NSAutoreleasePool pool = [[NSAutoreleasePool alloc] init];
          id obj = [[NSObject alloc] init];
          [obj autorelease];
          [pool drain];  => 等价于 [obj release];
      
  • 我们在编程中,并不需要显式的调用 pool 对象,因为在 RunLoop 中,这一切都为我们处理好了。在一个 RunLoop 循环中,会进行 NSAutoreleasePool 对象的生成,应用程序的主线程进行处理,废弃 NSAutoreleasePool 对象。

  • 尽管是这样,我们有的时候也需要显式的调用 NSAutoreleasePool 对象,因为有时会产生大量的 autorelease 对象,只要不废弃 NSAutoreleasePool 对象,那么这些生成的对象就不能被释放,会导致内存疯长的现象。最典型的例子就是在读取大量图像的同时改变它的尺寸。

    • 图像文件读到 NSData 对象,并且从中生成 UIImage 对象,改变这个对象的尺寸后,就会生成新的 UIIamge 对象。这种情况下就会产生大量的 autorelease 对象。这时就有必要在合适的地方生成,持有或废弃 NSAutoreleasePool 对象。
  • 另外,在 Cocoa 框架中也有很多类方法用于返回 autorelease 对象。比如

    id array = [NSMutableArray arrayWithCapasity:1];
    
    // 等价于
    id array = [[[NSMuatbleArray alloc] initWithCapasity:1] autorelease];
    

1.2.6 autorelease 的实现

  • 首先来看 GNU 的源代码

  • 首先看一下 autorelease 方法的实现

    [obj autorelease];
    
    // 表面上的实现方法
    - (id)autorelease {
      [NSAutoreleasePool addObject:self];
    }
    
    /**
    实际上, autorelease 内部是用 Runtime 的 IMP Caching 方法实现的。在进行方法调用时,为了解决类名/方法名几区的方法运行是的函数指针,要在框架初始化时对他们进行缓存
    */
    id autorelease_class = [NSAutoreleasePool class];
    SEL autorelease_sel = @selector(addObject:);
    IMP autorelease_imp = [autorelease_class methodForSelector:autorelease_sel];
    
    // 实际的方法调用时使用缓存的结果值
    - (id)autorelease {
      (*autorelease_imp)(autorelease_class, autorelease_sel, self);
    }
    
  • 再看 NSAutoreleasePool 的 addObject 类方法实现

    + (void)addObject:(id)obj {
      NSAutoreleasePool *pool = 取得正在使用的 NSAutoreleasePool 对象;
      if (pool) {
        [pool addObject:anObj];
      } else {
        NSLog("不存在正在使用的 NSAutoreleasePool 对象");
      }
    }
    
    • 注意:当多个 NSAutoreleasePool 对象嵌套使用时,理所当然会调用最里层的 NSAutoreleasePool 对象
  • addObject 实例方法实现

    // 当调用 NSObject类的 autorelease 实例方法时,这个对象就会被加到 NSAutoreleasePool 对象数组中
    - (void)addObject:(id)obj {
      [array addObject:obj];
    }
    
  • drain 实例方法废弃正在使用的 NSAutoreleasePool 对象的过程

    // 执行顺序: drain() -> dealloc() -> emptyPool() -> [obj release] -> [emptyPool release]
    - (void)drain {
      [self dealloc];
    }
    
    - (void)dealloc {
      [self emptyPool];
      [array release];
    }
    
    - (void)emptyPool {
      for (id obj in array) {
        [obj release];
      }
    }
    

1.2.7 苹果的实现

  • C++的实现

    class AutoreleasePoolPage {
      static inline void *push() {
        // 生成或持有 NSAutoreleasePool 对象
      }
      static inline id autorelease(id obj) {
        // 对应 NSAutoreleasePool 类的 addObject 类方法
        AutoreleasePoolPage *autoreleasePoolPage = 取得正在使用的 AutoreleasePoolPage 实例;
        autoreleasePoolPage -> add(obj);
      }
      static inline void *pop(void *token) {
        // 废弃 NSAutoreleasePool 对象
        releaseAll();
      }
      id *add(id obj) {
        // 添加对象到 AutoreleasePoolPage 的内部数组中
      }
      void releaseAll() {
        // 调用内部数组对象的 release 类方法
      }
    };
    
      // 具体调用
    void *objc_autoreleasePoolPush(void) {
    
        return AutoreleasePoolPage::push();
    
      }
    
      void *objc_autoreleasePoolPop(void *ctxt) {
        return AutoreleasePoolPage::push(ctxt);
      }
    
      id *objc_autorelease(void) {
    
        return AutoreleasePoolPage::autorelease(obj);
    
      }
    
    
  • 观察 NSAutoreleasePool 类方法和 autorelease 方法的运行过程

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    // == objc_autoreleasePoolPush()
    
    id obj = [[NSObject alloc] init];
    
    [obj autorelease];
    // == objc_autorelease(obj)
    
    [pool drain];
    // == objc_autoreleasePoolPop(pool);
    
  • 另外:[[NSAutoreleasePool showPools]]; 可以用来确认已经被 autorelease 的对象的状况。

  • 问题: 如果 autorelease NSAutoreleasePool 对象会如何?

    • 答: 会崩溃。因为通常在使用 Foundation 框架时,无论调用哪个对象的 autorelease 方法,本质都是调用 NSObject 类的 autorelease 方法。 但是 autorelease 方法已经被 NSAutoreleasePool 类所重载。所以运行时会出现错误。

1.3 ARC 规则

1.3.1概要

  • 实际上 引用计数式内存管理 的本质部分在 ARC 中并没有改变,就像 自动引用计数 这个名称一样,ARC 所做的,只是自动的帮助我们处理了 引用计数 相关部分。

1.3.2内存管理的思考方式

  • 引用计数式内存的思考方式就是思考 ARC 所引起的变化
    • 自己生成的对象,自己所持有
    • 非自己生成的对象,自己也能持有
    • 不再需要自己持有的对象时释放该对象
    • 非自己持有的对象无法释放
  • 本质上和内存管理的思考方式一样,只是实现方式上有些许不同

1.3.3所有权修饰符

  • ARC 有效时,id 类型和对象类型与 C语言 中的其他类型不同,其类型必须附加 所有权修饰符 。共有以下四种

    • __strong 修饰符
    • __weak 修饰符
    • __unsafe_unretained 修饰符
    • __autoreleasing 修饰符
  • __strong 修饰符

    • __strong 修饰符是 id 类型和对象类型默认的所有权修饰符

      // 在 ARC 有效的环境下
      id obj = [[NSObject alloc] init] <==> id __strong obj = [[NSObject alloc] init];
      
      // 在 ARC 无效的环境下
      {
        id obj = [[NSObject alloc] init] 
        [obj release];
      }
      
    • 当被__strong 修饰符修饰的,自己生成的,对象在超过其作用域时:

      {
      // 自己生成并持有对象
        id __strong obj = [[NSObject alloc] init];
        
        /**
        因为变量 obj 为强引用,所以自己持有对象
        */
      }
      // 因为变量超出作用域,强引用失效,所以释放对象,因为对象此时没有其他的所有者了,对象被废弃。
      // 正好遵循内存管理的原则
      
    • 当对象的所有者和对象的生命周期是明确的,取得非自己生成并持有的对象时:

      {
      // 首先取得非自己生成的对象,但是由于__strong修饰符修饰着这个对象,所以自己持有这个对象
        id __strong obj = [NSMutableArray array];
      }
      /**
      当超出对象的作用域时,强引用失效,所以释放对象。
      但是由于 [NSMutableArray array] 所生成的对象并非自己所持有的,而是自动的加到 autoreleasePool 中,所以会在一个 RunLoop 周期结束后,自动废弃对象。
      */
      
    • 有 __strong修饰符的变量之间可以相互赋值

      // 首先 obj0 强引用指向 对象A , obj1 强引用指向 对象B,表示 obj1 持有 B, obj2 不持有任何对象
      id __strong obj0 = [[NSObject alloc] init]; // 对象A
      id __strong obj1 = [[NSObject alloc] init]; // 对象B
      id __strong obj2 = nil;
      
      // 此时 obj0 与 obj1 强引用同一个对象 B, 没有人持有 对象A 了,所以 对象A 被废弃。
      obj0 = obj1;
      
      // 此时 obj2 指向 obj0 所持有的对象, 所以 对象B 现在被三个引用所持有。
      obj2 = obj0;
      
      // 现在 obj1 对 对象B 的强引用失效,所以现在持有 对象B 的强引用变量为 obj0,obj2
      obj1 = nil;
      
      // 同理,现在只有 obj2 持有对象B
      obj0 = nil;
      
      // 没有引用指向 对象B 了,废弃 对象B
      obj2 = nil;
      
    • __strong修饰符 也可以修饰 OC类成员变量,也可以在方法的参数上,使用附有 _strong 修饰符的变量

      @interface Test : NSObject
      {
          id __strong obj_;
      }
      - (void)setObject:(id __strong)obj;
      @end
      @implementation Test
      
      - (instancetype)init
      {
          self = [super init];
          return self;
      }
      
      - (void)setObject:(id)obj {
          obj_ = obj;
      }
      
      @end
        
      // 调用函数
      {
              
              // 首先test 持有 Test 对象的强引用
              id __strong test = [[Test alloc] init];
              
              // Test对象 的 obj_ 成员,持有 NSObject 对象的强引用
              [test setObject:[[NSObject alloc] init]];
      }
          
          /**
           此时test强引用超出了其作用域,它失效了。
           所以此时没有强引用指向 Test对象 了, Test对象会被废弃
           
           废弃 Test 对象的同时, Test对象 的 obj_ 成员也被废弃。
           所以它释放了指向 NSObject 的强引用
           
           因为 NSObject 没有其他所有者了,所以 NSObject 对象也被废弃。
           */
      
    • _strong修饰符 与 _weak, _autoreleasing 修饰符一样,初始化时,即使不明确指出,他们也都会自动将该引用指向nil。通过 _strong修饰符,完美的满足了 引用计数的思考方式

    • id类型和对象类型的所有权修饰符默认都为 __strong 所以不需要再显式的指明修饰对象的修饰符为 _strong

  • __weak 修饰符

    • _weak修饰符 的出现就是为了解决 _strong修饰符在内存管理中所带来的循环引用问题。如上例:

      @interface Test : NSObject
      {
          id __strong obj_;
      }
      - (void)setObject:(id __strong)obj;
      @end
      @implementation Test
      
      - (instancetype)init
      {
          self = [super init];
          return self;
      }
      
      - (void)setObject:(id)obj {
          obj_ = obj;
      }
      
      @end
        
      // 调用函数,打印变量结果如下:
      {
              
              // 首先test1 持有 Test 对象的强引用, test2 持有 Test 对象的强引用
              id __strong test1 = [[Test alloc] init];  // 对象A
              id __strong test2 = [[Test alloc] init]; // 对象B
      
              /**
                  TestA 对象中的 obj_ 成员变量持有着 test2指向的 对象B, 同时,test2指向的对象B中的              obj_又强引用着 对象A, 所以造成了循环引用。
              */
              [test1 setObject:test2];
              [test2 setObject:test1];
      }
      /**
          当跳出作用域后,test1释放它对 对象A 的强引用
                        test2释放它对 对象B 的强引用
          但是此时 对象A中的 obj_A 对 对象B 的强引用本应该被释放,但是由于在 对象B 中强引用了对象A,所以 obj_A 不会被释放,会一直强引用 对象B, 而同理,对象B 中的 obj_B 也不会被释放,所以它将一直强引用着 对象A, 所以此时外部没有谁引用着 对象A 和 对象B, 但是他们自己在互相引用着,这样就造成了内存泄漏!(所谓内存泄漏,指的就是应该被废弃的对象,却在超出其生存周期变量作用域时还继续存在着)
      */
      
      /**
      打印变量结果如下:
          其中 test1 对象中强引用 test2 对象, test2对象 又强引用 test1 对象,造成无尽的循环。
      */
      
互相强引用.png
循环.png
  • 而下面这种情况:

    {
      id test = [[Test alloc] init];
      [test setObject:test];
    }
    /**
    当强引用test 的作用域结束后,它释放了对 Test 对象的引用。
    但是 Test对象 内部还保留着 对 Test对象 的强引用,所以 Test对象 被引用着,所以不会被回收
    */
    // 也会发生内存泄漏!
    
自己引用自己.png
  • 所以此时,就非常需要一个 __weak修饰符 来避免循环引用

    // 弱引用与强引用正好相反,不能够持有对象实例。
    // 这样写会发出警告:Assigning retained object to weak variable; object will be released after assignment
    // 表示因为没有人持有着 NSObject 对象,所以该对象一旦被创建就会立即被销毁
    id __weak obj = [[NSObject alloc] init];
    
    // 正确的使用弱引用的方式
    {
      // 自己生成并持有 NSObject 对象
            id obj = [[NSObject alloc] init];
      
      // 因为 NSObject 对象已经被 obj 强引用着, 所以此时 obj1 对它使用弱引用也没有关系,
      // 不会使它的引用计数 +1
            id __weak obj1 = obj;
    }
    /**
        当超出变量的作用域时, obj 对 NSObject对象 的强引用消失,
        此时没有人持有 NSObject对象 了。
        NSObject对象 被废弃
    */
    
  • 对上述循环引用的例子进行修改如下:

    @interface Test : NSObject
    {
        id __weak obj_;
    }
    - (void)setObject:(id __strong)obj;
    @end
    @implementation Test
    
    - (instancetype)init
    {
        self = [super init];
        return self;
    }
    
    - (void)setObject:(id)obj {
        obj_ = obj;
    }
    
    @end
      
    // 调用函数,打印变量结果如下:
    {
            
            // 首先test1 持有 Test 对象的强引用, test2 持有 Test 对象的强引用
            id __strong test1 = [[Test alloc] init];  // 对象A
            id __strong test2 = [[Test alloc] init]; // 对象B
    
            /**
                TestA 对象中的 obj_ 成员变量弱引用着 test2指向的 对象B, 同时,test2指向的 对象B 中的               obj_又弱引用着 对象A。
            */
            [test1 setObject:test2];
            [test2 setObject:test1];
    }
    /**
        当跳出作用域后,test1释放它对 对象A 的强引用
                      test2释放它对 对象B 的强引用
        此时,由于 对象中的 obj_变量只拥有对对象的弱引用,所以 没有谁持有着 对象A,和对象B,他们被释放,没有造成循环引用!
    */
    
  • __weak修饰符 的另一优点:当持有某个对象的弱引用时,如果该对象被废弃,则弱引用将自动失效,并且会被置为 nil的状态(空弱引用)

    id __weak obj1 = nil;
            {
              // 自己生成并持有对象
                id __strong obj0 = [[NSObject alloc] init];
                
              // obj1 现在也指向 NSObject对象
                obj1 = obj0;
                
              // 此时打印 obj1 有值
                NSLog(@"A = %@", obj1);
            }
            
    /**
        当变量 obj0 超出作用域,它不再持有 NSObject对象,
        由于 obj1 是弱引用,所以它也不持有 NSObject对象
        由于没人持有 NSObject对象, NSObject对象被废弃
        
        被废弃的同时, obj1 变量的弱引用失效, obj1 被重新赋值为 nil
    */ 
    NSLog(@"B = %@", obj1);
    
    /**
        结果打印如下:
        2017-12-14 15:16:39.859875+0800 littleTest[10071:1377629] A = <NSObject: 0x10054da70>
        2017-12-14 15:16:39.860432+0800 littleTest[10071:1377629] B = (null)
    */
    
互相弱引用.png
  • __unsafe_unretained 修饰符

    • _unsafe_unretained 修饰符 是不安全的修饰符,在 iOS4 以前用来代替 _weak修饰符

      id __unsafe__unretained obj1 = nil;
              {
                  id __strong obj0 = [[NSObject alloc] init];
                  
                  obj1 = obj0;
                  
                  NSLog(@"A = %@", obj1);
              }
              
      NSLog(@"B = %@", obj1);
      
      /**
       该源码无法正确执行,因为 __unsafe_unretained修饰符 使变量既不强引用对象,也不弱引用对象。
       当 obj0 超出作用域时, NSObject 无引用,所以被释放
       在此同时, obj1 有时会错误访问对象,形成下面这种打印
      2017-12-14 15:38:21.462724+0800 littleTest[10140:1399554] A = <NSObject: 0x10044eea0>
      2017-12-14 15:38:21.463007+0800 littleTest[10140:1399554] B = <NSObject: 0x10044eea0>
      
      有时会发生错误,直接使程序崩溃。
      造成这两种情况的本质为: obj1 访问的对象已经被废弃了,造成了 垂悬指针!
      */
      
    • 所以,需要注意的是,当使用 _unsafe_unretained 修饰符 访问对象时,必须要确保该对象确实是真实存在的。

  • __autoreleasing 修饰符

    • 在 ARC 有效时,我们不可以使用如下代码,因为这些代码是在非 ARC 环境下使用的:

      NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
      id obj = [[NSObject alloc] init];
      [obj autorelease];
      
      [pool drain];
      
    • 作为替换,在 ARC 有效时, 我们会使用

      @autoreleasepool {        
         id __autoreleasing obj = [[NSObject alloc] init];
      }
      
    • 他们的对应关系如图:

等价于.png
  • 我们可以非显式的使用 __autoreleasing 修饰符 。

  • 情况一:当取得非自己生成并持有的对象时,虽然可以使用 alloc/new/copy/mutableCopy 以外的方法来取得对象,但是该对象已经被注册到 autoreleasePool 中。这和在 ARC无效 时,调用 autorelease 方法取得的结果相同。这是因为编译器会自动检查方法名是否以alloc/new/copy/mutableCopy 开始,如果不是,则自动将对象注册到 autoreleasePool 中。

        @autoreleasepool {
            // 首先取得非自己生成的对象,因为 obj 的强引用,所以它持有这个对象
    // 因为这个对象的方法名不是以 alloc/new/copy/mutableCopy 开头的,所以他被自动注册到               autoreleasePool中了。
          {
             id __strong obj = [NSMutableArray array];
          }
          /**
            当变量超出其作用域时,他失去对这个对象的强引用。所以它会释放自己所持有的对象
            但是此时 autoreleasePool 中还持有着对这个对象的引用,所以它不会立即被废弃
          */ 
        }
    
    /**
        当 autoreleasePool 的作用域也结束后,没有人持有这个对象了,所以它被废弃了。
    */
    
    • 验证上述说法,首先:创建对象的方式为 [NSMutableArray array]

      // 在  autoreleasepool 的作用域外定义一个 obj1 持有弱引用  
      id __weak obj1 = nil;
          
      @autoreleasepool {
              
              {
                  id __strong obj = [NSMutableArray array];
                  obj1 = obj;
                  NSLog(@"%@", obj1);
              }
              NSLog(@"%@", obj1);
      }
      NSLog(@"%@", obj1);
      
      /**
      打印结果:
      2017-12-14 16:32:07.118513+0800 littleTest[10242:1444479] (
      )
      2017-12-14 16:32:07.118819+0800 littleTest[10242:1444479] (
      )
      2017-12-14 16:32:07.118850+0800 littleTest[10242:1444479] (null)
      
      结果表明:当obj0超出其作用域时,它失去了对对象的引用。但是由于该对象被自动注册到 autoreleasepool 中,使得第二个 NSLog 打印时 obj1 依旧弱引用着这个对象,当第三个 NSLog 打印时,由于 autoreleasepool 已经被清空,所以这个对象也被销毁了, obj1 又被重置为 nil
      */
      
  • 此时,创建对象的方式为:[[NSMutableArray alloc] init]

    id __weak obj1 = nil;
          
          @autoreleasepool {
              
              {
                  id __strong obj = [[NSMutableArray alloc] init];
                  obj1 = obj;
                  NSLog(@"%@", obj1);
              }
              NSLog(@"%@", obj1);
          }
          NSLog(@"%@", obj1);
    
      /**
          打印结果如下:
          2017-12-14 16:36:09.584864+0800 littleTest[10257:1449554] (
          )
          2017-12-14 16:36:09.585131+0800 littleTest[10257:1449554] (null)
          2017-12-14 16:36:09.585149+0800 littleTest[10257:1449554] (null)
          
          这是因为,使用 alloc/new/copy/mutableCopy 方法创建对象时,不会将该对象自动的放入 autoreleasePool 中,这就使得当 obj0 超出其作用域后,就没有人强引用着 NSMutableArray 对象了,该对象也就被废弃了。
      */
    
  • 以下为 取得非自己生成并持有的对象 时所调用方法:

    + (id)array {
      return [[NSMutableArray alloc] init];
    }
    
    /**
      这段代码也没有使用 __autorelease修饰符,所以这个方法内部的对象不会被注册到 autoreleasePool 中。
    */
    
    // 上述方法也可以写成如下形式:
    + (id)array {
      id obj = [[NSMutableArray alloc] init];
      return obj;
    }
    /**
      因为 return 使得 obj 超出作用域,所以它所指向的对象 NSMutableArray 会被自动释放,但是因为 return 将这个对象作为函数的返回值返回给主调函数,所以这个对象不会被废弃。并且由于这个对象的生成方法是将其作为返回值,不是由alloc/new/copy/mutableCopy 方法创建的,所以 NSMutableArray 对象会被自动添加到 autoreleasePool 中
    */
    
  • 情况二: 在访问有 __weak修饰符 的变量时,实际上必定会访问注册到 autoreleasePool 中的对象

    id __weak obj1 = obj0;
    NSLog(@"class=%@", [obj1 class]);
    
    // 等价于
    id __weak obj1 = obj0;
    id _autoreleasing temp = obj1;
    NSLog(@"class=%@", [temp class]);
    
    /**
    出现这种情况的原因是因为:__weak修饰符 只持有对象的弱引用,这样没法保证它访问对象的过程中,对象不被废弃。所以我们将他要访问的对象放到 autoreleasePool 中,这样就会使得 @autoreleasePool块 结束之前都能保证该对象的存在。
    */
    
  • 情况三:由于 id obj <==> id __strong obj 所以我们希望能推出 id *obj <==> id __strong *obj 但是实际上并非如此,实际情况是 id *obj <==> id __autoreleasing obj 同理:NSObject **obj <==> NSObject * __autoreleasing *obj ,像这样的,id 的指针或对象的指针在没有显示的指定时,会被附加上 __autoreleasing修饰符

    // 例如 NSString 中的这个方法
    stringWithContentsOfFile:(nonnull NSString *) encoding:(NSStringEncoding) error:(NSError * _Nullable __autoreleasing * _Nullable)
    
    // 使用这个方式的源代码如下:
    NSError *error = nil;
    BOOL result = [obj performOperationWithError:&error];
    
    // 函数声明如下
    - (BOOL)performOperationWithError:(NSError **)error;
    // 等价于
    - (BOOL)performOperationWithError:(NSError * __autoreleasing *)error;
    
    /**
        之所以使用 __autoreleasing 作为修饰符,是因为我们这个方法声明的参数的是 error 这个指针变量的指针,也就是 需要传递 error 的地址。而在这个方法内部的执行如下,它改变了 error 这个指针所指向的内容。使其指向了一个由 alloc 生成的对象。而我们需要明白的内存管理的思考方式为:除了由 alloc/new/copy/mutableCopy 生成的对象外,其他方式生成的对象都必需要注册到 autoreleasePool 中。并取得非自己所持有的对象。所以将变量声明为 (NSError * __autoreleasing *)error 就可以实现这一目的。
    */
    
    - (BOOL)performOperationWithError:(NSError * __autoreleasing *)error {
        *error = [[NSError alloc] initWithDomain:MyAppDomain code:errorCode userInfo:nil];
        return NO;
    }
    
    /**
        可能对于不熟悉 C语言 的小伙伴来说,不是很明白为什么这里非要将函数参数声明为 “指针的指针”,这是因为当我们仅仅把参数声明为指针时,方法就变为如下,当我们给函数传递指针时,默认会生成跟指针类型相同的实例变量。当我们在这个方法中操作指针时,我们以为操作的是指针,实际上只是这复制的实例变量。也就是说,在这个例子中 error 就是这个复制的实例变量。当这个方法结束时,error 会被释放,其所指向的内容也会一并被释放。所以此时外部的 error 依旧指向 nil。没有任何改变。
        而当我们使用 (NSError * __autoreleasing *)error 作为参数时,虽然复制的实例变量情况还是存在,但是这次复制的是“指针的指针”,也就是说,它指向跟参数指针相同指针地址, 在函数内部使用 *error 获取到了指针地址,使其指向了 NSError对象 。这样,虽然函数当出了其作用域时,那个复制的实例变量被销毁了,但是它改变了函数外部 error 指针所指向的对象,使其从 nil 变成了 NSError对象。
    */ 
    
    - (BOOL)performOperationWithError:(NSError *)error {
        error = [[NSError alloc] initWithDomain:MyAppDomain code:errorCode userInfo:nil];
        return NO;
    }
    
  • 对于函数传递指针及指针的指针 还不明白的请看这里

  • 👇的代码会产生编译错误:

    NSError *error = nil;
    NSError **perror = &error;
    //Pointer to non-const type 'NSError *' with no explicit ownership
    
  • 因为

    // 需要改变perror的所有权修饰符
    NSError *error = nil;
    NSError *__strong *perror = &error;
    
    // 对于其他类型的所有权修饰符也一样
    NSError __weak *error = nil;
    NSError *__weak *perror = &error;
    
    /**
      可是我们刚刚在调用
      - (BOOL)performOperationWithError:(NSError * __autoreleasing *)error
      方法时,并没有将 NSError *error 转化为 __autoreleasing修饰符修饰的,这是为什么?
    */ 
    // 实际上,编译器自动帮我们转化了修饰符
    NSError *error = nil;
    NSError *_autoreleasing *temp = nil;
    BOOL result = [obj performOperationWithError:&temp];
    error = temp;
    
  • 像 NSAutoreleasePool 一样, autoreleasepool 也可以嵌套使用。例如在 iOS 程序中,整个程序都被包含在 @autoreleasepool块 中。

  • NSRunLoop等实现无论 ARC 是否有效,都能够随时释放注册到 autoreleasepool 中的对象。

1.3.4规则

  • 在 ARC 有效时编译代码,必须遵守以下规则:

    • 不能使用 retain/release/retainCount/autorelease
    • 不能使用 NSAllocateObject/NSDeallocateObject
    • 必须遵守内存管理方法命名规则
    • 不能显示的调用 dealloc
    • 使用 @autoreleasepool块 代替 NSAutoreleasePool
    • 不能使用区域 NSZone
    • 对象型变量不能作为 C语言 结构体的成员
    • 显示的转换 id 和 void *
  • 不能使用 retain/release/retainCount/autorelease

    • 摘自苹果的官方说明 "设置ARC有效时,无需再次键入release或retain代码" 否则就会编译错误。
  • 不能使用 NSAllocateObject/NSDeallocateObject

    • 我们已经知道了当我们在调用 NSObject 类的 alloc 方法时,会生成并持有 OC 对象,如 GNUstep 所示,实际上 alloc 就是直接调用 NSAllocateObject 函数来生成持有对象的,但是在 ARC 环境下,如果我们也显示的调用 NSAllocateObject 会产生编译错误。
  • 必须遵守内存管理方法命名规则

    • 在 ARC 无效时,用于对象生成/持有需要遵循如下规则:alloc/new/copy/mutableCopy。以上述名称开始的方法在返回对象时,必须得返回给调用方应当持有的对象。这点在 ARC 有效时也是一样。

    • 在 ARC 有效时,追加的一条命名规则:init

      • 以 init 开始的方法规则要比 alloc/new/copy/mutableCopy 更加严格。该方法必须得是实例方法。不允许是类方法。并且返回对象应该为 id 类型或者该方法声明类的对象类型,或者是该类的超类或子类。该返回对象并不注册到 autoreleasepool 中。基本上只是对 alloc 方法返回值的对象进行初始化操作并返回该对象。

        // 以下为使用该方法的源代码: init 方法会初始化alloc 方法返回值,并且返回该对象
        id obj = [[NSObject alloc] init];
        
        // 下列是不允许的,因为它没有返回对象
        - (void)initTheData:(id)data;
        
        // 另外,👇方法虽然也有init, 但它不包含在命名规则里,因为他是一个单词 initialize
        - (void)initialize;
        
  • 不能显示的调用 dealloc

    • 因为当对象废弃时,无论如何都会调用对象的 dealloc 方法,所以不需要我们手动调用。而当我们手贱一下的去调用时,就会产生编译错误
    • dealloc 方法在大多数情况下用于删除已经注册的代理或者观察者对象
    • 在 ARC 无效时,必须在 dealloc 方法内部显式的调用其父类的 dealloc 方法 [super dealloc];
    • 在 ARC 有效时,这一切都是自动处理的。
  • 使用 @autoreleasepool块 代替 NSAutoreleasePool

    • 在 ARC 中使用 NSAutoreleasePool 会引起编译错误
  • 不能使用区域 NSZone

    • 无论 ARC 是否有效,NSZone在现在的运行时系统已经被完全忽略了。
  • 对象型变量不能作为 C语言 结构体的成员

    • C语言 的结构体中如果存在 OC对象型变量 会引起编译错误

    • 如果非要将对象加入结构体,则可强制转化为 void * 或者附加 _unsafe_unretained修饰符 ,因为被 _unsafe_unretained修饰符所修饰的对象,已经不属于编译器的内存管理对象了。

      struct Data {
        NSMutableArray __unsafe__unretained *array
      }
      
  • 显示的转换 id 和 void *

    • 当 ARC 无效时,将 id变量 强制转化为 void *变量 不会出现问题

      id obj = [[NSObject alloc] init];
      
      void *p = obj;
      
      // 将 void * 赋给 id变量 中,调用他的实例方法,运行时也不会出现问题
      id o = p;
      [o release];
      
    • 当 ARC 有效时,会引起编译错误。此时,id型 或 对象型变量 赋值给 void * 或者逆向赋值时都需要进行特定的转换,如果只是想单纯的赋值则可以使用 bridge转换

      id obj = [[NSObject alloc] init];
      
      // id转化为 void *,它的安全性比 __unsafe__unretained 还要低,一不小心就会有垂悬指针
      void *p = (__bridge void *)obj;
      
      // void * 转换为 id
      id o = (__bridge id)p;
      
    • __bridge转换 中还包括 _bridge_retained转换, _bridge_transfer转换

      id obj = [[NSObject alloc] init];
      void *p = (__bridge_retained void *)obj
      
      • 该代码在 非ARC 环境下

        id obj = [[NSObject alloc] init];
        
        void *p = obj;
        // __bridge__retained转变为了 retain,使得 p 和 obj 都持有了这个对象
        [(id)p retain];
        
    • 一个其他的例子:

      void *p = 0;
      {
        id obj = [[NSObject alloc] init];
        
        p = (__bridge_retained void *)obj;
      }
      NSLog(@"class = %@", [(__bridge_retained)p class]);
      
      • 该代码在 非ARC 环境下

        void *p = 0;
        {
          id obj = [[NSObject alloc] init];
          
          p = [obj retain];
          
          [obj release];
        }
        
        /**
          此时 p 依旧持有对 NSObject对象 的引用
        */
        NSLog(@"class = %@", [(__bridge_retained)p class]);
        
    • __bridge_transfer,被转换的变量所持有的对象在该变量被赋值给转换目标变量后释放

      id obj = (__bridge_transfer id)p;
      
      • 非ARC 环境下

        id obj = (id)p;
        [obj retain];
        [(id)p release];
        
    • Objective-C 对象 与 Foundation对象

      • Core Foundation 对象主要使用在用 C语言 编写的 Core Foundation 框架中,并使用引用计数对象。在 ARC无效 时, Core Foundation 框架中的 retain、release 分别是 CFRetain,CFRelease
      • Core Foundation 对象与 OC 对象的区别只在于是 Core Foundation 框架 还是 Foundation 框架所生成的。无论是由哪种框架生成的对象,一旦生成之后,就能在其它框架上使用。比如 Foundation 框架的 API 生成并持有的对象可以由 Core Foundation 框架的 API 进行释放。
      • Core Foundation 对象与 Objective-C 对象没有区别,所以在 ARC无效 时,只用简单的 C语言的转换也能实现互换。另外这种互换不需要占用 CPU 资源,所以也叫做 "免费桥"(Toll-Free Bridge)

1.3.5属性

  • 属性声明的属性所有权修饰符 对应的关系

    属性声明的属性 所有权修饰符
    assign _unsafe_unretained
    copy __strong(赋值的是被复制的对象)
    retain __strong
    strong __strong
    unsafe_unretained _unsafe_unretained
    weak __weak

1.3.6数组

  • 静态数组的情况:

    // 将附有各种修饰符的变量作为静态数组的使用情况
    // 比如
    id __weak obj[10];
    // 除了 __unsafe__unretained修饰符之外的其他修饰符都是会将数组元素的值默认初始化为nil
    // 当数组超出其变量作用域时,内存管理也同样适用于他之中的各个对象
    
  • 动态数组:在这种情况下,根据不同的目的选择使用 NSMutableArray, NSMutableDictionary, NSMutableSet 等 Foundation 框架中的容器,这些容器会恰当的持有追加的对象并会为我们管理这些对象。

  • 看一下动态数组在 C语言 中的实现

    // 首先,声明一个动态数组需要使用指针。来表示指针的地址
    id __strong *array = nil;
    //这里是由于 id * 类型的指针默认修饰符为 id __autoreleasing * 类型, 所以有必要显示的指定为 __strong 修饰符。另外,虽然保证了附有 __strong修饰符 的 id 类型变量被初始化为 nil, 但是不保证 array变量, 也就是 id指针型变量 被初始化为 nil
    // 当类型是其他类型时,如下:
    NSObject * __strong *array = nil;
    
    // 之后,使用 calloc函数 确保想分配的,附有 __strong修饰符变量 的容量占有的内存块
    array = (id __strong *)calloc(entries, sizeof(id));
    // 其中 entries 表示内存块的数量。并且 calloc 函数将数组中的每个变量指向的对象都自动初始化为 nil
    
    // 注意这里如果使用了 malloc函数 来分配内存, 则需要手动的将每个变量所指向的对象都初始化为 0,注意这里只能使用 memset等函数 来进行初始化赋值
    
    // 然后,通过 calloc函数 分配的动态数组就能完全按照静态数组的方法使用
    array[0] = [[NSObject alloc] init];
    
    // 但是在动态数组中操作 __strong修饰符 的变量与静态数组有很大差异,需要自己手动释放数组,但是当它释放时,必须手动的先将数组的每个变量都置为nil,此时不能使用  memset等函数 将数组中的元素值设为 0 。这也会内存泄漏
    for (NSInteger i = 0; i < entries; ++i) {
      array[i] = nil;
    }
    free(array);
    

1.4 ARC 的实现

1.4.1 __strong修饰符

  • 观察赋值给附有 __strong修饰符 的变量在实际程序中到底是如何运行的,👇代码(首先是正常的会使引用计数 +1 的 alloc/new/copy/mutableCopy 方法):

    {
      id __strong obj = [[NSObject alloc] init];
    }
    
    • 该段代码转化为汇编代码后,为(具体如何转化为汇编代码,请看我的另一篇文章):

      .section        __TEXT,__text,regular,pure_instructions
              .macosx_version_min 10, 13
              .globl  _main
              .p2align        4, 0x90
      _main:                                  ## @main
              .cfi_startproc
      ## BB#0:
              pushq   %rbp
      Lcfi0:
              .cfi_def_cfa_offset 16
      Lcfi1:
              .cfi_offset %rbp, -16
              movq    %rsp, %rbp
      Lcfi2:
              .cfi_def_cfa_register %rbp
              subq    $16, %rsp
              movl    $0, -4(%rbp)
              movq    L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rax
              movq    L_OBJC_SELECTOR_REFERENCES_(%rip), %rsi
              movq    %rax, %rdi
              callq   _objc_msgSend
              leaq    -16(%rbp), %rdi
              xorl    %ecx, %ecx
              movl    %ecx, %esi
              movq    %rax, -16(%rbp)
              callq   _objc_storeStrong
              xorl    %eax, %eax
              addq    $16, %rsp
              popq    %rbp
              retq
              .cfi_endproc
      
              .section        __DATA,__objc_classrefs,regular,no_dead_strip
              .p2align        3               ## @"OBJC_CLASSLIST_REFERENCES_$_"
      L_OBJC_CLASSLIST_REFERENCES_$_:
              .quad   _OBJC_CLASS_$_NSObject
      
              .section        __TEXT,__objc_methname,cstring_literals
      L_OBJC_METH_VAR_NAME_:                  ## @OBJC_METH_VAR_NAME_
              .asciz  "alloc"
      
              .section        __DATA,__objc_selrefs,literal_pointers,no_dead_strip
              .p2align        3               ## @OBJC_SELECTOR_REFERENCES_
      L_OBJC_SELECTOR_REFERENCES_:
              .quad   L_OBJC_METH_VAR_NAME_
      
              .section        __TEXT,__objc_methname,cstring_literals
      L_OBJC_METH_VAR_NAME_.1:                ## @OBJC_METH_VAR_NAME_.1
              .asciz  "init"
      
              .section        __DATA,__objc_selrefs,literal_pointers,no_dead_strip
              .p2align        3               ## @OBJC_SELECTOR_REFERENCES_.2
      L_OBJC_SELECTOR_REFERENCES_.2:
              .quad   L_OBJC_METH_VAR_NAME_.1
      
              .section        __DATA,__objc_imageinfo,regular,no_dead_strip
      L_OBJC_IMAGE_INFO:
              .long   0
              .long   64
      .subsections_via_symbols        
      
  • 简化后的模拟代码为:

    // 首先发送消息给 NSObject 类,消息内容为 alloc 指令,然后将结果赋值给 obj
    id obj = objc_msgSend(NSObject, @selector(alloc));
    // 然后将 init 消息发送给 obj
    objc_msgSend(obj, @selector(init));
    // 最后释放 obj
    objc_release(obj);
    
    // 由此可知,在 ARC 有效时,自动插入了 release 方法
    
  • 取得 非自己生成的,但是自己持有的 对象:

    id __strong obj = [NSMutableArray array];
    
    • 转化成汇编语言:

      .section        __TEXT,__text,regular,pure_instructions
              .macosx_version_min 10, 13
              .globl  _main
              .p2align        4, 0x90
      _main:                                  ## @main
              .cfi_startproc
      ## BB#0:
              pushq   %rbp
      Lcfi0:
              .cfi_def_cfa_offset 16
      Lcfi1:
              .cfi_offset %rbp, -16
              movq    %rsp, %rbp
      Lcfi2:
              .cfi_def_cfa_register %rbp
              subq    $16, %rsp
              movl    $0, -4(%rbp)
              movq    L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rax
              movq    L_OBJC_SELECTOR_REFERENCES_(%rip), %rsi
              movq    %rax, %rdi
              callq   _objc_msgSend
              movq    %rax, %rdi
              callq   _objc_retainAutoreleasedReturnValue
              leaq    -16(%rbp), %rdi
              xorl    %ecx, %ecx
              movl    %ecx, %esi
              movq    %rax, -16(%rbp)
              callq   _objc_storeStrong
              xorl    %eax, %eax
              addq    $16, %rsp
              popq    %rbp
              retq
              .cfi_endproc
      
              .section        __DATA,__objc_classrefs,regular,no_dead_strip
              .p2align        3               ## @"OBJC_CLASSLIST_REFERENCES_$_"
      L_OBJC_CLASSLIST_REFERENCES_$_:
              .quad   _OBJC_CLASS_$_NSMutableArray
      
              .section        __TEXT,__objc_methname,cstring_literals
      L_OBJC_METH_VAR_NAME_:                  ## @OBJC_METH_VAR_NAME_
              .asciz  "array"
      
              .section        __DATA,__objc_selrefs,literal_pointers,no_dead_strip
              .p2align        3               ## @OBJC_SELECTOR_REFERENCES_
      L_OBJC_SELECTOR_REFERENCES_:
              .quad   L_OBJC_METH_VAR_NAME_
      
              .section        __DATA,__objc_imageinfo,regular,no_dead_strip
      L_OBJC_IMAGE_INFO:
              .long   0
              .long   64
              
      .subsections_via_symbols
      
  • 简化后的汇编语言:

    // 首先发送 array 消息给接收者 NSMutableArray, 然后将结果的返回值赋给 obj
    id obj = objc_msgSend(NSMutableArray, @selector(array));
    
    /**
        obj_retainAutoreleasedReturnValue 函数主要用于最优化程序运行,顾名思义 obj_retainAutoreleasedReturnValue 表示的是 “持有 Autorelease 的返回值”,表示的是,它是用于自己持有对象的函数,但他持有的对象应为返回注册在 autoreleasepool 中对象的方法,,或是函数的返回值。像这段源代码一样,也就是 obj 需要被 __strong 所修饰在调用 alloc/new/copy/mutableCopy 以外的方法时,由编译器插入该函数
    */
    objc_retainAutoreleasedReturnValue(obj);
    // 释放 obj
    objc_release(obj);
    
  • 由于,objc_retainAutoreleasedReturnValue 函数总是成对出现的,所以实际上它还有一个姐妹:objc_autoreleaseReturnValue, 它主要用在 alloc/new/copy/mutableCopy 以外的方法生成对象时的返回对象上,也就是如👇所示

    + (id)array {
      return [[NSMutableArray alloc] init];
    }
    
    // 转化成汇编后的简化代码
    + (id)array {
      id obj = objc_msgSend(NSMutableArray, @selector(alloc));
      objc_msgSend(obj, @selector(init));
      // 此时,返回注册到 autoreleasepool 中对象的方法:使用了 obj_autoreleaseReturnValue 函数来返回注册到 autoreleasepool 中的对象,但是 obj_autoreleaseReturnValue 方法与 obj_autorelease 方法不同,一般不仅限于注册对象到 autoreleasepool 中
      return objc_autoreleaseReturnValue(obj);
    }
    
    /**
        objc_autoreleaseReturnValue 方法会检查使用该函数的方法或调用方的执行命令列表,
    1.如果方法或函数的调用方在调用了方法或函数后紧接着调用了 objc_retainAutoreleasedReturnValue() 函数,那么就不会将返回的对象注册到 autoreleasepool 中,而是直接传递到方法或函数的调用方去。
    2.如果方法或函数的调用方在调用了方法或函数后紧接着没调用objc_retainAutoreleasedReturnValue() 函数,那么就会将返回对象注册到 autoreleasepool 中。
    
    而 objc_retainAutoreleasedReturnValue() 函数与 objc_retain 函数不同,他即便不注册到 autoreleasepool 中,也能正确的获取对象。
    
    通过 objc_autoreleaseReturnValue 和 objc_retainAutoreleasedReturnValue 方法的协作,可以不将对象注册到 autoreleasepool 中二直接传递,这一过程达到最优化
    */
    

1.4.2 __weak修饰符

  • 就像我们之前看到的:__weak修饰符 所提供的功能如魔法一般
    • 若附有 __weak修饰符 的变量所引用的对象被废弃,则将 nil 赋值给该变量
    • 使用附有 __weak修饰符 的变量,即是使用注册到 autoreleasepool 中的对象
若附有 __weak修饰符 的变量所引用的对象被废弃,则将 nil 赋值给该变量 原理验证:
  • 下面我们来看看 __weak修饰符 原理实现:

    {
      id __weak obj1 = obj;
    }
    
    /**
      编译器的模拟代码
    */
    id obj1;
    
    // 首先通过 obj_initWeak 函数初始化附有 __weak 修饰符的变量
    objc_initWeak(&obj1, obj);
    
    // 然后在变量作用域结束时,通过 obj_destroyWeak 函数释放该变量
    objc_destroyWeak(&obj1);
    
     /**
    
          其中,objc_initWeak 函数的作用是:将附有 __weak修饰符 的变量初始化为 0 后,会将赋值的对象作为参数调用 objc_storeWeak 函数
    
          obj_destroyWeak 函数的作用是:将 0 作为参数调用 obj_storeWeak 函数
    
      */
    
      objc_initWeak(&obj1, obj); <==> obj1 = 0; objc_storeWeak(&obj1, obj);
    
      objc_destroyWeak(&obj1) <==> objc_storeWeak(&obj1, 0);
    
      /**
    
          objc_storeWeak 函数把 第二个参数 的赋值对象的 地址 作为 "键值",将 第一个参数 的附有 __weak修饰符 的变量的"地址"注册到 weak 表 中。如果第二个参数为 0 ,则把变量的地址从 weak 表中删除
    
          weak 表与引用计数表相同,实现方式都为"散列表"。如果使用 weak 表,将废弃对象的地址作为键值进行搜索,就能高速的获取对应的附有 weak修饰符 的变量的地址。另外,由于一个对象可以同时赋值给多个附有 weak修饰符 的变量中,所以对于一个键值,可注册多个变量的地址。
    
      */
    
    
  • 释放对象时,废弃没人持有的对象的同时,程序是如何操作的,下面我们来跟踪观察,对象将通过 objc_release 方法释放

    • obj_release
    • 引用计数为 0, 所以执行 dealloc
    • _objc_RootDealloc
    • object_dispose
    • objc_destrctInstance
    • objc_clear_deallocating
      • 其中,objc_clear_deallocating 的动作如下
        • 从 weak 表中获取废弃对象的地址作为键值的记录
        • 将包含在记录中的所有附有 __weak修饰符 变量的地址赋值为 nil
        • 从 weak 表中删除该记录
        • 从引用计数表中删除废弃对象的地址作为键值的记录
  • 根据以上步骤可知:__weak修饰符 所修饰的变量所引用的对象被废弃,该变量被置为 nil 得到实现。但是由此可知,如果大量使用附有 _weak修饰符修饰变量,将会产生性能问题。

  • 在使用 __weak修饰符 时, 如果如下方式,会引起警告

    id __weak obj = [[NSObject alloc] init];
      // 因为该对象刚被创建就会被释放
    
      // 编译器的模拟代码
      id obj;
      id tmp = objc_msgSend(NSObject, @selector(alloc));
      objc_msgSend(tmp, @selector(init));
      // 虽然自己生成并持有的对象通过 objc_initWeak 函数被赋值给附有 __weak修饰符 的变量中,但是编译器判断它没有持有者,所以该对象立即通过 objc_release 方法释放
      objc_initWeak(&obj, tmp);
      objc_release(tmp);
      objc_destroyWeak(&obj);
      // 这样一来, nil 就会被赋值给引用废弃对象的附有 __weak修饰符 的变量
    
  • 关于立即释放对象的一些思考

    // 已知以下代码会引起编译器的警告,这是因为编译器判断生成并持有的对象不能继续持有,因为没有强引用指向它
    id __weak obj = [[NSObject alloc] init];
    
    // ---------------------------------------------------------------------------------
    // 附有 __unsafe_unretained 修饰符的变量会怎样? 也会产生警告
    id __unsafe_unretained obj = [[NSObject alloc] init];
    
    // 转换成编译器的模拟代码:
    id obj = obj_msgSend(NSObject, @selector(alloc));
    objc_msgSend(obj, @selector(init));
    // obj_release 函数立刻释放了生成并持有的对象,这样该对象的垂悬指针被赋给 obj
    objc_release(obj);
    
    // ---------------------------------------------------------------------------------
    
      // 如果在生成对象的时候不把它赋给变量会怎样?
    
      // 在 非ARC 环境下,必然会发生内存泄漏
    
      // 但是在 ARC 环境下,由于不能继续持有该对象,会立即调用 obj_release 函数,由于 ARC 的处理,这样的代码不会产生内存泄漏
    
      [[NSObject alloc] init];
    
      // ARC 下生成的代码
    
      id tmp = obj_msgSend(NSObject, @selector(alloc));
    
      objc_msgSend(tmp, @selector(init));
    
      objc_release(tmp);
    
    // ---------------------------------------------------------------------------------
    
      // 是否可以调用被立即释放掉的对象的实例方法?
    
      (void)[[[NSObject alloc] init] hash];
    
      // 该代码会变成如下形式:
    
      id tmp = obj_msgSend(NSObject, @selector(alloc));
    
      objc_msgSend(tmp, @selector(init));
    
      objc_msgSend(tmp, @selector(hash));
    
      objc_release(tmp);
    
      // 所以,obj_release 方法是在该对象实例方法调用完成后才会被调用,所以可以调用被立即释放的对象的实例方法
    
    
使用附有 __weak修饰符 的变量,即是使用注册到 autoreleasepool 中的对象 ,原理验证:
  • 看👇代码

    {
      id __weak obj1 = obj;
      NSLog(@"%@", obj1);
    }
    
    // 该源码可转化为如下形式
    id obj1;
    objc_initWeak(&obj1, obj);
    // objc_loadWeakRetained 取出附有 __weak修饰符 变量所引用的对象并 retain
    id tmp = objc_loadWeakRetained(&obj1);
    // 将对象注册到 autoreleasepool 中
    objc_autorelease(tmp);
    NSLog(@"%@", tmp);
    objc_destroyWeak(&obj1);
    
  • 由此可知:因为附有 __weak修饰符 的变量所引用的对象像这样被注册到 autoreleasepool 中,所以在 @autoreleasepool 块结束之前都可以放心的使用 _weak修饰的变量。但是,不能大量的使用附有 _weak修饰符 修饰的变量,否则会引起注册到 autoreleasepool 中的对象大大增加,因此在使用附有 _weak修饰符 的变量时,最好先暂时赋给附有 _strong修饰符 的变量后再使用。若看不太懂则可以只看下面代码:

    // 下面这段代码会使变量 o 所赋值的对象被注册到 autoreleasepool 中 5 次
    {
          id __weak o = obj;
          for (int i = 0; i < 5; ++i) {
            NSLog(@"%d -- %@", i, o);
          }   
    }
    
      // 而下面这段代码只会使变量 o 所赋值的对象被注册到 autoreleasepool 中 1 次
    
      {
    
        id __weak o = obj;
        id tmp = o;
              for (int i = 0; i < 5; ++i) {
              NSLog(@"%d -- %@", i, tmp);
          }   
        
    
      }
    
    
不支持 __weak修饰符 的情况
  • 在 iOS4 和 OS X Snow Leopard 不支持 __weak修饰符 。
  • 不支持 __weak修饰符 的类:NSMachPort等, 这个类重写了 retain/release 方法,并且实现了自己的引用计数。
  • 不支持 __weak修饰符 的类在其类中声明附加了 --attribute—((objc_arc_weak_reference_unavailable)) 这一属性,同时定义了 NS_AUTOMATED_REFCOUNT_WEAK_UNAVAALIBLE
  • 还有一种情况也不能使用 __weak修饰符 ,那就是当 allocWeakReference/retainWeakReference 实例方法返回 NO 的情况。(这种情况没有被写入 NSObject 类的接口说明文档中),也就是说,这两个方法我们一般不会接触到。

1.4.3 __autoreleasing修饰符

  • 将对象赋值给附有 __autoreleasing修饰符 的变量等同于 ARC 无效时,调用对象的 autorelease 方法。

  • 首先看一下使用 alloc/new/copy/mutableCopy 时的情况

      @autoreleasepool {
        id __autoreleasing obj = [[NSObject alloc] init];
      }
    
      // 模拟代码
      id pool = objc_autoreleasePoolPush();
      id obj = objc_msgSend(NSObject, @selector(alloc));
      objc_msgSend(obj, @selector(init));
      objc_autorelease(obj);
      objc_autoreleasPoolPop(pool);
    
  • 再看一下使用 alloc/new/copy/mutableCopy 以外的方法时的情况

    @autoreleasepool {
      id __autoreleasing obj = [NSMutableArray array];
    }
    
    // 模拟代码
    id pool = objc_autoreleasePoolPush();
    id obj = objc_msgSend(NSMutableArray, @selector(array));
    objc_retainAutoreleasedReturnValue(obj);
    objc_autorelease(obj);
    objc_autoreleasPoolPop(pool);
    
    // 虽然 obj 持有对象的方法变为 objc_retainAutoreleasedReturnValue, 但是将 obj 所引用的对象注册到 autoreleasepool 中的方法并没有改变
    

1.4.4 引用计数

  • 引用计数数值本身到底是什么?

    // 这个函数为获得引用计数的函数数值
    uintptr_t _objc_rootRetainCount(id obj);
    
    {
          id __strong obj = [[NSObject alloc] init];
      NSLog(@"retain count = %d", _objc_rootRetainCount);
    }
    // 打印结果:retain count = 1
    
  • 我们实际上并不能完全信任 _objc_rootRetainCount 这个函数所取得的数值,因为有时对于已经释放的对象以及不正确的对象地址,有时也会返回 1 。 并且在多线程中使用对象的引用计数数值,因为有竞争状态的问题,所以取得的数值并不一定完全可信

1.5 总结

  • 至此,我们所探究的 自动引用计数 已经完全讲解完毕,如有疏漏或不正确,不准确的地方,还望大家批评指正,共同进步。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,018评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,046评论 2 372
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,215评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,303评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,181评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,171评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,577评论 3 384
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,260评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,546评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,630评论 2 311
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,390评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,260评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,633评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,920评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,210评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,569评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,767评论 2 335

推荐阅读更多精彩内容