本文章屬於原創,轉載請註明出處
參考資料
sunnyxxx
深入理解Objective C的ARC機制
Objective-C引用計數原理
基礎參考 iOS高級編程
基礎
內存管理的思考方式
- 自己生成的對象自己持有
- 非自己生成的對象自己也能持有
- 不需要自己持有的對象釋放
- 非自己持有的對象無法釋放
內存管理語義
- 以下方法表示自己生成的對象自己持有
- 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
引用循環容易發生內存泄露,內存泄漏是指應當廢棄的對象在超過其生命週期後繼續存在
- __strong
- 可以使用__strong持有非自己生成並持有的對象
- __strong完全遵循ARC有效時的內存管理語義,使用__strong,__weak,__autoreleasing修飾符不賦值時對象的值爲nil
- __weak
- 兩個__strong的對象互相引用對方會造成引用循環併發生內存泄露
- 使用__weak給其中一個對象賦值可以避免引用循環,廢棄對象的同時__weak賦值的對象的值變成nil
- 不能使用__weak直接初始化對象,初始化的一瞬間,會變爲nil
id __weak obj = [[NSObject alloc]init];
// obj = nil
- unsafe_unretained
- unsafed_unretained是不安全的,ARC有效時內存管理是編譯器的工作,被此修飾符修飾的變量的內存管理不屬於編譯器
- 被修飾對象即不持有強引用也不持有若引用,使用unsafed_unretained時要保證賦值給__strong的對象的確存在,不然程序會崩潰
id __unsafe_unretained obj = [[NSObject alloc]init];
// 即使有 unsafed 標示,編譯器還是會警告,這裏的值瞬間變爲 nil
- __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有效時
- 不能使用retain/release/retainCount/autorelease/new
- 不能使用NSAllocateObject/NSDeallocateObject
- 必須遵循內存管理方法命名規則
- 不顯示使用dealloc
- 使用@autoreleasePool代替NSAutoreleasePool
- 不使用區域NSZone
- 對象型變量不能作爲C語言結構體變量
- 顯示轉換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的
引用計數的存儲方式
主要有三種:
- TaggedPointer:將其指針值作爲引用計數返回
- isa:有的對象會將isa指針的一部分用於存儲引用計數
- 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是多個哨兵對象,釋放時每次一層,互補影響