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閱讀 227,156評論 6 529
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,866評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 174,880評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,398評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,202評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,743評論 1 320
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,822評論 3 438
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 41,962評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,476評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,444評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,579評論 1 365
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,129評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,840評論 3 344
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,231評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,487評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,177評論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,568評論 2 370

推薦閱讀更多精彩內容