ARC `基礎` & `原理` 總結

本文章屬於原創,轉載請註明出處

參考資料
sunnyxxx
深入理解Objective C的ARC機制
Objective-C引用計數原理
基礎參考 iOS高級編程

基礎

內存管理的思考方式
  • 自己生成的對象自己持有
  • 非自己生成的對象自己也能持有
  • 不需要自己持有的對象釋放
  • 非自己持有的對象無法釋放
內存管理語義
  • 以下方法表示自己生成的對象自己持有
  1. alloc/new/copy/mutableCopy/retain
  • 非自己生成的對象可以使用retain持有
// 生成非自己持有對象
id obj = [NSMutableArray array];
// 自己持有對象
[obj retain];
// 不需要時釋放
[obj release];
// autorealease 使對象超出指定範圍並可以正確釋放
- (id)object {
	id obj = [[NSObject alloc] init];
	[objc autorelease];
	return obj;
}	
__strong & __weak & __unsafe_unretained

引用循環容易發生內存泄露,內存泄漏是指應當廢棄的對象在超過其生命週期後繼續存在

  1. __strong
  • 可以使用__strong持有非自己生成並持有的對象
  • __strong完全遵循ARC有效時的內存管理語義,使用__strong,__weak,__autoreleasing修飾符不賦值時對象的值爲nil
  1. __weak
  • 兩個__strong的對象互相引用對方會造成引用循環併發生內存泄露
  • 使用__weak給其中一個對象賦值可以避免引用循環,廢棄對象的同時__weak賦值的對象的值變成nil
  • 不能使用__weak直接初始化對象,初始化的一瞬間,會變爲nil
id __weak obj = [[NSObject alloc]init];
// obj = nil
  1. unsafe_unretained
  • unsafed_unretained是不安全的,ARC有效時內存管理是編譯器的工作,被此修飾符修飾的變量的內存管理不屬於編譯器
  • 被修飾對象即不持有強引用也不持有若引用,使用unsafed_unretained時要保證賦值給__strong的對象的確存在,不然程序會崩潰
id __unsafe_unretained obj = [[NSObject alloc]init];
// 即使有 unsafed 標示,編譯器還是會警告,這裏的值瞬間變爲 nil
  1. __autoreleasing
  • 在ARC有效的情況下,使用@autoreleasePool代替NSAutoreleasePool類,__autoreleasing修飾符,代替autorelease方法
  • 很少顯示的聲明__autoreleasing,非自己持有的對象自動聲明爲__autoreleasing,在作爲返回值是自動聲明爲__autoreleasing
  • 被聲明爲__weak的變量自動被聲明爲__autoreleasing,將其註冊到autoreleasePool裏,在@autoreleasePool的塊結束前,都能確保對象存在
id __weak obj = obj1;
id __autoreleasing tmp = obj;
  • 最後一個顯示聲明__autoreleasing的例子,id指針或對象指針在沒有被顯示聲明的時候聲明爲__autoreleasing,主要一個用處是*error傳遞NSError對象的時候
id *obj = [[NSObject alloc]init];
id __autoreleasing *obj = [[NSObject alloc]init];
ARC規則

ARC有效時

  1. 不能使用retain/release/retainCount/autorelease/new
  2. 不能使用NSAllocateObject/NSDeallocateObject
  3. 必須遵循內存管理方法命名規則
  4. 不顯示使用dealloc
  5. 使用@autoreleasePool代替NSAutoreleasePool
  6. 不使用區域NSZone
  7. 對象型變量不能作爲C語言結構體變量
  8. 顯示轉換id和void*在ARC下會報錯
ARC有效時的屬性

屬性的內存管理語義對應的所有權修飾符

  • assign == __unsafe_unreatined
  • copy == _strong 但賦值被複制的對象
  • retain == __strong
  • strong == __strong
  • unsafe_unretained == __unsafe_unretained
  • weak == __weak

原理

在Objective-C中,有三種類型是適用ARC的

  • block
  • Objective-C對象,如id,Class,NSError *等
  • 以attribute作爲標記的

CF開頭的都是適用用ARC的

引用計數的存儲方式

主要有三種:

  1. TaggedPointer:將其指針值作爲引用計數返回
  2. isa:有的對象會將isa指針的一部分用於存儲引用計數
  3. Sidetable:使用散列表來管理引用計數
TaggedPoint

判斷標誌位是否爲一,確定對象有沒有使用TaggedPoint,id的本質就是objc_object結構體的指針類型,可以使用id的方法isTaggedPoint()方法來判讀其是否使用TaggedPoint來存儲引用計數

#if SUPPORT_MSB_TAGGED_POINTERS
#   define TAG_MASK (1ULL<<63)
#else
#   define TAG_MASK 1

inline bool 
objc_object::isTaggedPointer() 
{
#if SUPPORT_TAGGED_POINTERS
    return ((uintptr_t)this & TAG_MASK);
#else
    return false;
#endif
}
isa

在64位環境並且是Objective-C 2.0的情況下,有些對象會使用isa的一部分作爲引用計數存儲

因爲64位環境存儲一個指針地址很浪費,所以作爲優化,使用isa指針的一部分來存儲,

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
    
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x0000000000000001ULL
#   define ISA_MAGIC_VALUE 0x0000000000000001ULL
    struct {
        uintptr_t indexed           : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 14;
#       define RC_ONE   (1ULL<<50)
#       define RC_HALF  (1ULL<<13)
    };

# else
    // Available bits in isa field are architecture-specific.
#   error unknown architecture
# endif

// SUPPORT_NONPOINTER_ISA
#endif

};

SUPPORT_NONPOINTER_ISA用來標記是否使用isa指針存儲引用計數

#if !__LP64__  ||  TARGET_OS_WIN32  ||  TARGET_IPHONE_SIMULATOR  ||  __x86_64__
#   define SUPPORT_NONPOINTER_ISA 0
#else
#   define SUPPORT_NONPOINTER_ISA 1
#endif

isa指針中一些變量的定義如下

  • indexed:0 表示普通的 isa 指針,1 表示使用優化,存儲引用計數
  • has_assoc:表示該對象是否包含 associated object,如果沒有,則析構時會更快
  • shiftcls:類的指針
  • weakly_referenced:表示該對象是否有過 weak 對象,如果沒有,則析構時更快
  • has_sidetable_rc:表示該對象的引用計數值是否過大無法存儲在 isa 指針
  • extra_rc:存儲引用計數值減一後的結果

isa指針不一定會存儲引用計數,has_sideable_rc中,如果爲1,就是用Sidetable來存儲

散列表 sidetable

sidetable裏存儲的引用計數,總是爲真正的引用計數減1。使用sidetable來存儲引用計數,當調試時,即使對象內存地址損壞,也能使用散列表來找到損壞的對象的地址

sidetable用於管理引用計數表和weak表,使用 spinlock_t slock來保證可能發生的競態條件

spinlock_t slock;//保證原子操作的自選鎖
RefcountMap refcnts;//保存引用計數的散列表
weak_table_t weak_table;//保存 weak 引用的全局散列表

weak表的作用是在對象全部dealloc的時候,將weak的對象都設置爲nil,避免懸掛指針
weak表是一個全局的表,其中對象爲鍵,weak_entry_t 爲值,weak_entry_t 中保存了所有指向該對象的weak指針

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};
獲取引用計數
  • taggedPoint可以直接獲取引用計數的值,isa指針使用其中一個變量保存
  • sidetbale:使用sidetable_retainCount()方法,其實現是獲取當前sidetable對象的實例,其refcnts屬性是存儲引用計數的散列表,使用迭代器匹配到當前實例的鍵值對,返回其 值 + 1
retain方法
- (id)retain {
    return ((id)self)->rootRetain();
}
inline id objc_object::rootRetain()
{
    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}

可以看出retain先判斷是否支持isTaggedPoinnter,然後調用sidetable_retain()方法操作sidetable的refcnts屬性,將引用計數加1,
再看看sidetable的實現

id objc_object::sidetable_retain()
{
    //獲取table
    SideTable& table = SideTables()[this];
    //加鎖
    table.lock();
    //獲取引用計數
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
         //增加引用計數
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    //解鎖
    table.unlock();
    return (id)this;
}
release

sidetable_release()返回是否執行dealloc方法

bool 
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.indexed);
#endif
    SideTable *table = SideTable::tableForPointer(this);

    bool do_dealloc = false;

    if (spinlock_trylock(&table->slock)) {
        RefcountMap::iterator it = table->refcnts.find(this);
        if (it == table->refcnts.end()) {
            do_dealloc = true;
            table->refcnts[this] = SIDE_TABLE_DEALLOCATING;
        } else if (it->second < SIDE_TABLE_DEALLOCATING) {
            // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
            do_dealloc = true;
            it->second |= SIDE_TABLE_DEALLOCATING;
        } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
            it->second -= SIDE_TABLE_RC_ONE;
        }
        spinlock_unlock(&table->slock);
        if (do_dealloc  &&  performDealloc) {
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
        }
        return do_dealloc;
    }

    return sidetable_release_slow(table, performDealloc);
}

在執行release方法前,存儲引用計數會先預留一個1,這也是爲什麼總返回真正的引用計數減1,因爲release方法總會將引用計數減1,在減1之前先看存儲引用值是否爲0,當爲0時,直接進行dealloc操作,否則就減1,這樣避免了產生負數

Autorelease

autorelease對象什麼時候釋放
  • 在手動加入autoreleasePool的情況下,ARC有效時,在@autoreleasePool的大括號結束前釋放對象,MRC下,調用[pool drain]時釋放對象
  • 在沒有手動加入autoreleasePool的情況下,Autorelease對象是在當前Runloop迭代結束時釋放的,能釋放的正確的原因是,系統在每個runloop迭代中,都加入自動釋放池的push和pop
小實驗
__weak id reference = nil;
- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *str = [NSString stringWithFormat:@"xxx"];
    // str是一個autorelease對象,設置一個weak的引用來觀察它
    reference = str;
}
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"%@", reference); 
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"%@", reference); 
}

由於viewController在loadview之後便加到了windom視圖的層級上,所以viewDidLoad和viewWillAppear是在同一個runloop調用的,因此在viewWillAppear中,這個對象依然有值

Autorelease原理
  • autoreleasePoolPage
    在ARC下,使用@autoreleasePool {} 來使用一個AutoreleasePool,隨後編譯器將其改寫爲
void *context = objc_autoreleasePoolPush();
// 括號裏的代碼
objc_autoreleasePoolPop(context);

這兩個函數都是對autoreleasePoolPage的封裝,因此關鍵在這個類

// AutoreleasePoolPage
{
	magic_t const magic;
	id *next;
	phread_t const thread;
	AutoreleasePoolPage *const parent;
	AutoreleasePoolPage *child;
	uint32_t const depth;
	uint32_t hiwait;
}
  • autoreleasePool並沒有單獨的數據結構,而其是有若干個AutoreleasePoolPage以雙向鏈表的形式組合而成(分別對應上面的child和parent指針
  • autoreleasePool是按線程一一對應的
  • autoreleasePoolPage會爲對象申請4096的內存空間(一頁的大小),除了上面實例變量所佔的空間,剩下的空間全部用來存儲autorelease對象的地址
  • id *next指針作爲標記新加進來(add)的autorelease對象的下一個
  • 一個AutoreleasePoolPage的空間被佔滿,會創建一個新的AutoreleasePoolPage對象,連接鏈表,後來的autorelease對象在新的autoreleasePoolPage中加入
// end 棧頂
// <-- next指針指向
// id objN 最新的autorelease對象的位置
// ...
// id obj2 
// id obj1(begin)棧底
// 類實例所佔內存

如上,當這一頁再加入一個autorelease對象就要滿了的時候(next指針馬上指向棧頂),這時執行上面的操作,新建下一頁Page對象,與這一頁鏈表連接完成後,新page的next指針被初始化在棧底,然後繼續向新的棧頂添加對象

所以向一個對象發送autorelease消息就相當於把對象加到autoreleasePoolPage棧頂的next指針指向的位置

釋放時刻
// AutoreleasePoolPage
// (end) 棧頂
// <-- next指針指向
// id objN 最新的autorelease對象的地址
// ...
// obj3 
// 0 哨兵位置
// id obj2
// id obj1 (begin)棧底
// 類實例所佔內存

每當進行AutoreleasePoolPush調用時,runtime就向當前的AutoreleasePoolPage中add一個哨兵對象,值爲0(nil),Page就變成上面的樣子

objc_autoreleasePoolPush的返回值正是這個哨兵對象的地址,被objc_autoreleasePoolPop(哨兵對象)作爲入參,於是

  • 根據傳入的哨兵對象找到哨兵對象所處的page
  • 在當前page中,在晚於哨兵對象插入的所有autorelease對象都發送一次release消息,並移動next指針到正確位置
  • 從最新加入的對象一直向前清理,可以跨越若干個page,直到哨兵所處的page
    剛纔的objc_autoreleasePoolPop執行後,變成如下
// (end)棧頂
// ... 
// <-- next指針指向
// id obj2
// id obj1 (棧底)
// 

嵌套的autoreleasrPool

  • pop的時候總是釋放到上次的push爲止,多層pool是多個哨兵對象,釋放時每次一層,互補影響
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章