Objective-C 記憶體管理之alloc/retain/release/dealloc實現原理

小白HAKU發表於2019-03-02

GNUstep開源框架的實現

以GNUstep開源框架為例(與Foundation框架相類似)

/**
 * Allocates a new instance of the receiver from the default
 * zone, by invoking +allocWithZone: with
 * <code>NSDefaultMallocZone()</code> as the zone argument.<br />
 * Returns the created instance.
 */
+ (id) alloc
{
  return [self allocWithZone: NSDefaultMallocZone()];
}

/**
 * This is the basic method to create a new instance.  It
 * allocates a new instance of the receiver from the specified
 * memory zone.
 *
 */
+ (id) allocWithZone: (NSZone*)z
{
  return NSAllocateObject (self, 0, z);
}

複製程式碼

通過allocWithZone:類方法呼叫NSAllocateObject函式分配了物件。

/*
 *	Now do the REAL version - using the other version to determine
 *	what padding (if any) is required to get the alignment of the
 *	structure correct.
 */
struct obj_layout {
  char	padding[__BIGGEST_ALIGNMENT__ - ((UNP % __BIGGEST_ALIGNMENT__)
    ? (UNP % __BIGGEST_ALIGNMENT__) : __BIGGEST_ALIGNMENT__)];
  gsrefcount_t	retained;
};
typedef	struct obj_layout *obj;


/*
 *	Now do conditional compilation of memory allocation functions
 *	depending on what information (if any) we are storing before
 *	the start of each object.
 */

inline id
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);
    }

  /* Don`t bother doing this in a thread-safe way, because the cost of locking
   * will be a lot more than the cost of doing the same call in two threads.
   * The returned selector will persist and the runtime will ensure that both
   * calls return the same selector, so we don`t need to bother doing it
   * ourselves.
   */
  if (0 == cxx_construct)
    {
      cxx_construct = sel_registerName(".cxx_construct");
      cxx_destruct = sel_registerName(".cxx_destruct");
    }
  callCXXConstructors(aClass, new);

  return new;
}

複製程式碼

NSAllocateObject函式通過呼叫NSZoneMalloc函式來分配存放物件所需要的記憶體空間,之後該內從空間置0,最後返回座位物件而使用的指標。


注:NSZone是為防止記憶體碎片化而引入的結構。對記憶體分配的區域本身進行多重化管理,根據使用物件的目的、物件的大小分配記憶體、從而提高了記憶體管理的效率。但是蘋果官方ARC文件中指出,現在的執行時系統只是簡單地忽略了區域的概念。執行時系統中的記憶體管理本身已極具效率,使用區域來管理記憶體反而會引起記憶體使用效率低下以及原始碼複雜化等問題。

alloc類方法用struct obj_layout中的retained來儲存引用計數,並將其寫入物件的記憶體頭部,該記憶體塊全部置0後返回。


注:置0個人理解為通過呼叫malloc方法,分配一個size為指定大小的記憶體塊後進行初始化,相當於calloc(1,size)動作。

物件的引用計數可以通過retainCount方法獲得

/**
* Returns the reference count for the receiver.  Each instance has an
* implicit reference count of 1, and has an `extra reference count`
* returned by the NSExtraRefCount() function, so the value returned by
* this method is always greater than zero.<br />
* By convention, objects which should (or can) never be deallocated
* return the maximum unsigned integer value.
*/
- (NSUInteger) retainCount
{
 return NSExtraRefCount(self) + 1;
}

/**
* Return the extra reference count of anObject (a value in the range
* from 0 to the maximum unsigned integer value minus one).<br />
* The retain count for an object is this value plus one.
*/
inline NSUInteger
NSExtraRefCount(id anObject)
{
 return ((obj)anObject)[-1].retained;
}
複製程式碼

因為分配時全部置0,所以retained為0。由NSExtraRefCount(self) + 1得出,retainCount為1.可以推測出,retain方法使retained變數+1,而release方法使retained變數-1。

實現程式碼如下:

/**
* Increments the reference count and returns the receiver.<br />
* The default implementation does this by calling NSIncrementExtraRefCount()
*/
- (id) retain
{
 NSIncrementExtraRefCount(self);
 return self;
}

/**
* Increments the extra reference count for anObject.<br />
* The GNUstep version raises an exception if the reference count
* would be incremented to too large a value.<br />
* This is used by the [NSObject-retain] method.
*/
inline void
NSIncrementExtraRefCount(id anObject)
{
#if	defined(GSATOMICREAD)
 /* I have 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)
   {
     [NSException raise: NSInternalInconsistencyException
       format: @"NSIncrementExtraRefCount() asked to increment too far"];
   }
#else	/* GSATOMICREAD */
 NSLock *theLock = GSAllocationLockForObject(anObject);

 [theLock lock];
 if (((obj)anObject)[-1].retained > 0xfffffe)
   {
     [theLock unlock];
     [NSException raise: NSInternalInconsistencyException
       format: @"NSIncrementExtraRefCount() asked to increment too far"];
   }
 ((obj)anObject)[-1].retained++;
 [theLock unlock];
#endif	/* GSATOMICREAD */
}


複製程式碼
/**
* Decrements the retain count for the receiver if greater than zero,
* otherwise calls the dealloc method instead.<br />
* The default implementation calls the NSDecrementExtraRefCountWasZero()
* function to test the extra reference count for the receiver (and
* decrement it if non-zero) - if the extra reference count is zero then
* the retain count is one, and the dealloc method is called.<br />
* In GNUstep, the [NSObject+enableDoubleReleaseCheck:] method may be used
* to turn on checking for ratain/release errors in this method.
*/
- (oneway void) release
{
 if (NSDecrementExtraRefCountWasZero(self))
   {
#  ifdef OBJC_CAP_ARC
     objc_delete_weak_refs(self);
#  endif
     [self dealloc];
   }
}




/**
* Examines the extra reference count for the object and, if non-zero
* decrements it, otherwise leaves it unchanged.<br />
* Returns a flag to say whether the count was zero
* (and hence whether the extra reference count was decremented).<br />
* This function is used by the [NSObject-release] method.
*/
inline BOOL
NSDecrementExtraRefCountWasZero(id anObject)
{
 if (double_release_check_enabled)
   {
     NSUInteger release_count;
     NSUInteger retain_count = [anObject retainCount];
     release_count = [autorelease_class autoreleaseCountForObject: anObject];
     if (release_count >= retain_count)
       [NSException raise: NSGenericException
   	    format: @"Release would release object too many times."];
   }
 {
#if	defined(GSATOMICREAD)
   gsrefcount_t	result;

   result = GSAtomicDecrement((gsatomic_t)&(((obj)anObject)[-1].retained));
   if (result < 0)
     {
       if (result != -1)
         {
           [NSException raise: NSInternalInconsistencyException
             format: @"NSDecrementExtraRefCount() decremented too far"];
         }
       /* The counter has become negative so it must have been zero.
        * We reset it and return YES ... in a correctly operating
        * process we know we can safely reset back to zero without
        * worrying about atomicity, since there can be no other
        * thread accessing the object (or its reference count would
        * have been greater than zero)
        */
       (((obj)anObject)[-1].retained) = 0;
       return YES;
     }
#else	/* GSATOMICREAD */
   NSLock *theLock = GSAllocationLockForObject(anObject);

   [theLock lock];
   if (((obj)anObject)[-1].retained == 0)
     {
       [theLock unlock];
       return YES;
     }
   else
     {
       ((obj)anObject)[-1].retained--;
       [theLock unlock];
       return NO;
     }
#endif	/* GSATOMICREAD */
 }
 return NO;
}
複製程式碼
- (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;
}

複製程式碼

總結上述程式碼的邏輯為:

  • 在OC的物件中存有引用計數這一整數值。
  • 呼叫alloc或是retain方法後,引用計數值加1。
  • 呼叫release後,引用計數值-1。
  • 引用計數值為0後,呼叫dealloc方法廢棄物件。

Foundation框架的實現

在NSOject類的alloc類方法上設定斷點,追蹤程式的執行。

以下列出了執行所呼叫的方法和函式

  • +alloc
  • +allocWithZone:
  • class_createInstance
  • calloc

alloc類方法首先呼叫allocWithZone:類方法,然後呼叫class_createInstance函式,最後通過呼叫calloc來分配記憶體塊。

retainCount方法的執行:

  • -ratainCount
  • __CFDoExternRefOperation
  • CFBasicHashGetCountOfKey

retain方法的執行:

  • -ratain
  • __CFDoExternRefOperation
  • CFBasicHashAddValue

release方法的執行:

  • -release
  • __CFDoExternRefOperation
  • CFBasicHashRemoveValue(CFBasicHashRemoveValue返回為0時,-release呼叫dealloc)

apple通過儲存引用計數表(雜湊表)來實現。

GNUstep將引用計數儲存在記憶體快頭部的優點:

  • 少量程式碼即可完成。
  • 能夠統一管理引用計數記憶體塊與物件用記憶體塊。

蘋果引用計數表儲存的優點:

  • 物件用記憶體塊的分配無需考慮記憶體塊頭部。
  • 引用計數表各記錄中存由記憶體塊地址,可從各個記錄追溯到個物件的記憶體塊。(即使出現故障導致物件佔用的記憶體塊損壞,但是隻要引用計數表沒有被破壞,依然可以確認各記憶體塊的位置)。

相關文章