017-深入學習 ARC 及內存管理

自動引用計數(Automatic Reference Counting)是指在內存管理中對引用計數進行自動計數的技術,適用條件為:

  • Xcode 4.2 以上
  • LLVM 編譯器 3.0 以上
  • 設置 ARC 有效

iOS 內存管理的實現

內存管理的思考方式

  • 自己生成的對象,自己持有
  • 非自己生成的對象,自己也可以持有
  • 不再需要自己持有的對象時釋放
  • 非自己持有的對象無法釋放

這四條規則在 MRC 和 ARC 中都適用,但略有差別,要注意這里的自己,可以理解為對象的使用環境,也可以理解為編程人員自身。

對象操作與 OC 方法的對應

對象操作 OC 方法
生成并持有對象 alloc/new/copy/mutableCopy系列方法
持有對象 retain
釋放對象 release
廢棄對象 dealloc

方法實現

由于包含 NSObject 的 Foundation 框架沒有公開,NSObject 包含的 alloc/new/copy 等方法也就無法看到源碼,但是可以參考 Cocoa 框架的互換框架 GNUstep。

alloc

id obj = [NSObject alloc];

實現如下

+ (id) alloc
{
  return [self allocWithZone: NSDefaultMallocZone()];
}

+ (id) allocWithZone: (NSZone*)z
{
  return NSAllocateObject (self, 0, z);
}

struct obj_layout {
  char  padding[__BIGGEST_ALIGNMENT__ - ((UNP % __BIGGEST_ALIGNMENT__)
    ? (UNP % __BIGGEST_ALIGNMENT__) : __BIGGEST_ALIGNMENT__)];
  gsrefcount_t  retained;
};

NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone)
{
  id    new;
  int   size;

  NSCAssert((!class_isMetaClass(aClass)), @"Bad class for new object");
  size = class_getInstanceSize(aClass) + extraBytes + sizeof(struct obj_layout);
  if (zone == 0)
    {
      zone = NSDefaultMallocZone();
    }
  new = NSZoneMalloc(zone, size);
  if (new != nil)
    {
      memset (new, 0, size);
      new = (id)&((obj)new)[1];
      object_setClass(new, aClass);
      AADD(aClass, new);
    }
  if (0 == cxx_construct)
    {
      cxx_construct = sel_registerName(".cxx_construct");
      cxx_destruct = sel_registerName(".cxx_destruct");
    }
  callCXXConstructors(aClass, new);

  return new;
}

簡單來說,NSAllocteObject 通過 NSZoneMalloc 函數來分配存放對象的內存空間,置 0 后返回。

簡化版代碼如下

struct obj_layout {
   NSUInteger retained;
}

+ (id)alloc
{
    int size = sizeOf(struct obj_layout) + 對象大小;
    struct obj_layout *p = (struct obj_layout *)calloc(1, size);
    return (id)(p + 1);
}

obj_layout 結構體用于存放 retained 引用計數,最后返回的指針不包含它。

獲得一個對象的引用計數可以用 retainCount 方法。

- (NSUInteger) retainCount
{
  return NSExtraRefCount(self) + 1;
}

inline NSUInteger
NSExtraRefCount(id anObject)
{
  return ((obj)anObject)[-1].retained;
}

在 C 語言中對于一個指針 p 調用 p[-1] 相當于 p - 1,獲取指針所指地址前一位的地址。

這里定義每一個對象都有一個隱式的為 1 的引用計數,同時有一個外部引用計數,所以實際上 alloc 之后的外部引用計數為 0,所以調用 retainCount 方法的返回值為 1。

retain

[obj retain];

實現如下

- (id) retain
{
  NSIncrementExtraRefCount(self);
  return self;
}

inline void
NSIncrementExtraRefCount(id anObject)
{
  BOOL  tooFar = NO;

#if defined(GSATOMICREAD)
  /* I've seen comments saying that some platforms only support up to
   * 24 bits in atomic locking, so raise an exception if we try to
   * go beyond 0xfffffe.
   */
  if (GSAtomicIncrement((gsatomic_t)&(((obj)anObject)[-1].retained))
    > 0xfffffe)
    {
      tooFar = YES;
    }
#else   /* GSATOMICREAD */
  NSLock *theLock = GSAllocationLockForObject(anObject);

  [theLock lock];
  if (((obj)anObject)[-1].retained > 0xfffffe)
    {
      tooFar = YES;
    }
  else
    {
      ((obj)anObject)[-1].retained++;
    }
  [theLock unlock];
#endif  /* GSATOMICREAD */
  if (YES == tooFar)
    {
      static NSHashTable        *overrun = nil;
      [gnustep_global_lock lock];
      if (nil == overrun)
        {
          overrun = NSCreateHashTable(NSNonRetainedObjectHashCallBacks, 0);
        }
      if (0 == NSHashGet(overrun, anObject))
        {
          NSHashInsert(overrun, anObject);
        }
      else
        {
          tooFar = NO;
        }
      [gnustep_global_lock lock];
      if (YES == tooFar)
        {
          NSString      *base;

          base = [NSString stringWithFormat: @"<%s: %p>",
            class_getName([anObject class]), anObject];
          [NSException raise: NSInternalInconsistencyException
            format: @"NSIncrementExtraRefCount() asked to increment too far"
            @" for %@ - %@", base, anObject];
        }
    }
}

主要是檢查是否數值過大而溢出,未溢出則會把 retained 的變量自增一。

release

[obj release];

實現如下

- (oneway void) release
{
  if (NSDecrementExtraRefCountWasZero(self))
    {
#  ifdef OBJC_CAP_ARC
      objc_delete_weak_refs(self);
#  endif
      [self dealloc];
    }
}

inline BOOL

NSDecrementExtraRefCountWasZero(id anObject)
{
    if (((struct obj_layout *)anObject)[-1].retained == 0) {
        return YES;
    } else {
        ((struct obj_layout *)anObject)[-1].retained == 0) {
            return NO;
        }
    }
}

這里可以理解下,當 retained 值為 0 的時候,調用 retainCount 方法返回 1,此時調用 release 方法就會執行 dealloc 方法,因為沒有變量再持有這個對象。

dealloc

- (void) dealloc
{
  NSDeallocateObject (self);
}

inline void
NSDeallocateObject(id anObject)
{
  Class aClass = object_getClass(anObject);

  if ((anObject != nil) && !class_isMetaClass(aClass))
    {
      obj   o = &((obj)anObject)[-1];
      NSZone    *z = NSZoneFromPointer(o);

      /* Call the default finalizer to handle C++ destructors.
       */
      (*finalize_imp)(anObject, finalize_sel);

      AREM(aClass, (id)anObject);
      if (NSZombieEnabled == YES)
    {
      GSMakeZombie(anObject, aClass);
      if (NSDeallocateZombies == YES)
        {
          NSZoneFree(z, o);
        }
    }
      else
    {
      object_setClass((id)anObject, (Class)(void*)0xdeadface);
      NSZoneFree(z, o);
    }
    }
  return;
}

NSDeallocateObject 會釋放掉最開始 alloc 開辟的內存空間。

蘋果的實現略有不同,沒有用結構體而是用散列表(引用計數表)來管理,后面對于 __weak 變量也用到了散列表。散列表中鍵值為內存塊地址的散列值,這樣可以方便地從各個記錄追溯到各對象的內存塊,也有助于檢測各對象的持有者是否存在。

autorelease 與實現

autorelease 在 MRC 和 ARC 中使用都比較廣泛,在 MRC 中使用方法如下:

  1. 生成并持有 NSAutoreleasePool 對象
  2. 調用已分配對象的 autorelease 實例方法
  3. 廢棄 NSAutoreleasePool 對象

而在 ARC 中用 @autoreleasingpool 和 __autoreleasing 修飾符來實現。

同時在 Cocoa 框架中,相當于程序主循環的 NSRunLoop 或者在其他程序可運行的地方,對 NSAutoreleasePool 對象進行生成、持有和廢棄處理,所以不一定非要手動創建 NSAutoreleasePool 對象。但是如果是大量產生 autorelease 對象的場景,只要不廢棄 NSAutoreleasePool 對象,生成的對象就不會被釋放,可能產生內存不夠的現象。

在 GNUstep 中

[obj autorelease]

實現如下

- (id) autorelease
{
  if (double_release_check_enabled)
    {
      NSUInteger release_count;
      NSUInteger retain_count = [self retainCount];
      release_count = [autorelease_class autoreleaseCountForObject:self];
      if (release_count > retain_count)
        [NSException
      raise: NSGenericException
      format: @"Autorelease would release object too many times.\n"
      @"%"PRIuPTR" release(s) versus %"PRIuPTR" retain(s)",
      release_count, retain_count];
    }

  (*autorelease_imp)(autorelease_class, autorelease_sel, self);
  return self;
}

關鍵的語句是最后一句(*autorelease_imp)(autorelease_class, autorelease_sel, self);,這里用到了 "IMP Caching" 技術來緩存經常被調用的方法的結果值,實際的方法調用就是使用緩存的結果值

    autorelease_class = [NSAutoreleasePool class];
    autorelease_sel = @selector(addObject:);
    autorelease_imp = [autorelease_class methodForSelector: autorelease_sel];

所以實際上它等同于下面的語句

[NSAutoreleasePool addObject:self];

而 NSAutoreleasePool 實際上是維護一個列表,在列表內存儲各個 autorelease 對象。

蘋果封裝了一些方法,用動態數組來存儲 authorelease 對象,下面是簡化代碼

class AutoreleasePoolPage
{
    static inline void *push() 
    {
        生成或持有 NSAutoreleasePool 對象
    }
    
    static inline void pop(void *token) 
    {
        廢棄 NSAutoreleasePool 對象
        releaseAll();
    }
    
    static inline id autorelease(id obj)
    {
        相當于 addObject
    }
    
    void releaseAll()
    {
        依次調用數組中對象的 release 實例方法
    }
}

有兩個非公開類方法可以確認并打印出 autoreleasePool 的狀態

  • [NSAutoreleasePool showPools] 這個方法在 ARC 下不可用

  • _objc_autoreleasePoolPrint()

    這個方法使用前要先聲明,extern void _objc_autoreleasePoolPrint();

要注意一點,對于 Foundation 框架中的對象調用 autorelease 方法時,實際是調用 NSObject 的 autorelease 方法,但是 NSAutoreleasePool 類的 autorelease 方法被重栽了所以調用會發生異常。

ARC 規則

iOS 內存管理的思考方式本質在 ARC 中并未改變,但是 ARC 將實現方式做了改動,能便捷可靠地管理對象的生命周期。

ARC 權限可以設置為單個文件的屬性,ARC 和 非 ARC 文件可以共存于一個項目中。

所有權修飾符

ARC 有效時,id 類型和 OC 對象類型與 C 語言其他類型不同,必須加上所有權修飾符

  • __strong 默認修飾符
  • __weak
  • __unsafe_unretained
  • _autoreleasing

__strong 修飾符可以用于方法參數上,這樣傳入的參數就會被使用環境強持有。

- (void)setObject: (id __strong) target

__strong__weak__autoreleasing 可以保證將附有這些修飾符的自動變亮初始化為 nil。

__weak 修飾符在持有某個對象弱引用時,當對象被廢棄,此弱引用會自動失效且置為 nil,不會造成野指針問題。

__unsafe_unretained 修飾符主要用于兼容 iOS4 以下版本,它修飾的變量不屬于編譯器的內存管理對象,同時在對象被廢棄時不會被自動置為 nil,因此可能造成野指針問題。

由于 ARC 下不能手動使用 autorelease 方法和 NSAutoreleasePool 對象,因此用 @autoreleasingpool 和 __autoreleasing 修飾符來代替。

雖然 __strong 修飾符是對于 id 類型和 OC 對象類型的默認修飾符,但不適用于 id 指針和對象指針,id 指針和對象指針的默認修飾符是 autoreleasing。

    NSError *realError = nil;
    NSError **errorPointer = &realError;

由于對象指針賦值時所有權必須一致,因此會報編譯錯誤

    NSError *realError = nil;
    NSError * __strong *errorPointer = &realError;

《Pro multithreading and memory management for iOS and OSX》 在 autoreleasing 修飾符這部分寫的很混亂,也可能是翻譯的原因,英文版也有很多困惑的地方。

規則

  • 不能使用 retain/release/retainCount/autorelease

  • 不能使用 NSAllocateObject/NSDealloateObject

    NSAllocateObject 和 NSDeallocateObject 是 alloc 等方法的內部實現,所以也不能使用。

  • 遵守內存管理的方法命名規則

    alloc/new/copy/mutableCopy 等駝峰式前綴的方法必須返回給調用方所應當持有的對象,init 駝峰式前綴方法必須是實例方法,返回對象為 id 類型或該方法聲明類的對象類型,返回對象不會注冊到 autoreleasePool 中。

  • 不顯式調用 dealloc 方法

  • 使用 @autoreleasingpool 和 __autoreleasing 修飾符代替 NSAutoreleasePool

  • 不能使用 NSZone

  • OC 對象不能作為 C 語言結構體成員

    C 語言無法管理 OC 對象的生命周期,可以用 void * 或 __unsafe_unretained 修飾符

橋接轉換

如上所述,OC 對象生命周期由編譯器管理,C 語言無法管理,如果想把 OC 對象轉換為 C 變量,在 MRC 中可以簡單轉換,但是在 ARC 中必須進行橋接轉換,這一點在使用 Core Foundation 框架時很常見,因為 Core Foundation 框架包含的是 C 語言接口。

    id obj = [[NSObject alloc] init];
    void *p = (__bridge void *)obj;
    id o = (__bridge id)p;

__bridge 轉換的安全性與 __unsafe_unretained 相近,因此可能造成野指針問題。

__bridge_retained 將 OC 對象轉換為 CF 對象,CF 對象需要負責用 CFRelease 等方法釋放對象

    void *p = (__bridge_retained void *)obj;

__bridge__transfer 將 Core Foundation 對象轉換為 OC 對象,同時 CF 對象放棄持有。

    id o = (__bridge_transfer id)p;

在 CF 框架中有一組對應 API

  • CFBridgingRelease -- __bridge__transfer
  • CFBridgingRetain -- __bridge_retained

屬性

ARC 中屬性具有的特性與所有權修飾符的對應關系

屬性聲明的屬性 所有權修飾符
assign __unsafe_unretained
copy __strong
retain __strong
strong _strong
unsafe_unretained __unsafe_unretained
weak __weak

其中 copy 屬性的賦值是通過 NSCopying 接口的 copyWithZone: 方法。

ARC 實現

__strong 實現

對于 alloc/new/copy/mutableCopy 方法等,其模擬源代碼如下

    id obj = objc_msgSend(NSObject, @selector(alloc));
    objc_msgSend(obj, @selector(init));
    objc_release(obj);

而對于非 alloc 等方法,實現源碼略有不同

    id obj = objc_msgSend(NSMutableArray, @selector(array));
    objc_retainAutoreleasedReturnValue(obj);
    objc_release(obj);

這里用到一個新的方法 objc_retainAutoreleasedReturnValue,而在返回對象的方法,例如這里的 array 方法等,有一個配對的方法

+ (id) array
{
    id obj = objc_mgSend(NSMutableArray, @selector(alloc));
    objc_msgSend(obj, @selector(init));
    return objc_autoreleaseReturnValue(obj);
}

這里這兩個方法其實是一種優化策略,如果 objc_autoreleaseReturnValue() 檢測使用該函數的方法或函數調用方的執行命令列表,如果方法或調用方在調用了該方法后會緊接著調用 objc_retainAutoreleasedReturnValue,則該函數會取消注冊到 autoreleasePool 的操作,并且將結果保存到 thread-local storage 中,也就是一個線程局部存儲,這一部分存儲空間只作為某個線程專有的存儲。然后被調用函數直接返回這個 object,同時外部接收到返回值后去檢查 TLS 中剛好有這個對象,就直接返回,不進行retain 操作。相當于節約了一次 release 和 retain 的操作。

但是這樣做的前提是被調用方要能得知外部調用方的環境是 ARC 還是 非 ARC,這里會用到 __builtin_return_address 方法,作用是得到函數的返回地址,參數表示層數,于是內部的被調用方加入偏移值就可以查看外部的調用方的匯編指令,從而檢測出外部是否調用了 objc_retainAutoreleasedReturnValue 這個方法。

__weak 實現

前面提過引用計數的存儲是用引用計數表來實現的,這里對于 __weak 對象也是如此。以對象地址作為主鍵,存儲多個 __weak 修飾符的變量,對它們進行統一管理。

{
    id __weak obj1 = obj;
}

這句聲明賦值語句的實現如下

id obj1;
obj1_initWeak(&obj1, obj);
obj1_destroyWeak(&obj1);

實際上也就是

id obj1;
obj1 = 0;
objc_storeWeak(obj1, obj);
objc_storeWeak(&obj1, 0);

這里 objc_storeWeak 函數會把第二餐素的賦值對象的地址作為鍵值,將第一參數變量地址注冊到 weak 表中。如第二參數為 0,則把變量地址從 weak 表中刪除。

當廢棄對象時,將進行以下操作:

  • 從 weak 表中獲取廢棄對象的地址為鍵值的記錄
  • 將包含在記錄中的所有附有 __weak 修飾符變量的地址置為 nil
  • 從 weak 表中刪除該記錄
  • 從引用計數表中刪除廢棄對象的地址為鍵值的記錄

因此如果大量使用 __weak 變量就會消耗相應的 CPU 資源。

另一方面,在 使用 附有 _weak 修飾符的變量時變量會被注冊到 autoreleasePool 中。

{
    id __weak boj1 = obj;
    NSLog(@"%@", obj1);
}

其實現如下

id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&obj1);

這里強調在使用該變量時才會注冊到 autoreleasePool 中。由于每次使用時都會注冊一遍,所以當多次使用時會注冊大量變量到 autoreleasePool 中,因此最好暫時用 __strong 變量暫存一下,因為 __strong 變量只會被注冊一次。

關于注冊到 autoreleasePool 的操作無法被驗證

    id obj = [[NSObject alloc] init];
    id __weak obj1 = obj;
    NSLog(@"pre %lu", _objc_rootRetainCount(obj));
    NSLog(@"%@", [obj1 class]);
    NSLog(@"after %lu", _objc_rootRetainCount(obj));

按照 《Pro multithreading and memory management for iOS and OSX》 的說法打印出來應該是使用前為 1,使用后變成 2,實際打印結果

pre 1
NSObject
pre 1
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1.1 什么是自動引用計數 概念:在 LLVM 編譯器中設置 ARC(Automaitc Reference Co...
    __silhouette閱讀 5,235評論 1 17
  • 自動引用計數 自動引用計數:指內存管理中對引用采取自動計數的技術。 內存管理/引用計數 持有對象引起引用計數加...
    南京小伙閱讀 1,329評論 2 3
  • 29.理解引用計數 Objective-C語言使用引用計數來管理內存,也就是說,每個對象都有個可以遞增或遞減的計數...
    Code_Ninja閱讀 1,521評論 1 3
  • 貌似每個iOS開發者都有一篇屬于自己的內存管理,記錄了自己對內存管理理解的深度以及廣度,所以我也來記錄一下我的理解...
    Bugfix閱讀 2,276評論 0 3
  • 你像天邊最美的那朵云 而我在遙遠的這里 望著 我和你 終究天各一方 你是學校里最優秀的學生 而我離開那個地方已經...
    迷人的hero閱讀 243評論 0 1