什么是引用計數(shù)合冀?
引用計數(shù)是最普遍的垃圾回收策略之一窥翩。每一個對象都會有一個額外的計數(shù)值來表示當前被引用的次數(shù)业岁。有新的引用,這個值就會+1寇蚊;結(jié)束引用叨襟,這個值會自動-1,直到計數(shù)值為0時幔荒,對象所指的內(nèi)存塊就會廢棄掉被系統(tǒng)回收糊闽,從而達到釋放內(nèi)存的目的。
在ARC還沒引入之前爹梁,引用計數(shù)大都是Coder該思考的事情右犹,所以我們可以先關(guān)掉項目的ARC來深入了解蘋果的內(nèi)存管理機制。(花括號 "{ }" 表示一個生命周期)
{
// 初始化一個對象并被obj持有 reference count = 1
NSObject *obj = [[NSObject alloc] init];
// newObj 持有后姚垃,reference count = 2
NSObject *newObj = [obj retain];
// 連續(xù)釋放對象
[newObj release];
[obj release];
/// 兩步release釋放操作的是同一個對象
/// reference count = 0, 對象被廢棄回收
}
如何操作引用計數(shù)念链?
上一步操作中,其實已經(jīng)涉及到如何操作引用計數(shù)了积糯。顯然掂墓,在Foundation框架中,NSObject 類負擔了內(nèi)存管理的職責看成,大部分對象都是繼承自 NSObject君编。
+[NSObject alloc] // 生成對象
-[NSObject retain] // 持有對象,增加一個新的引用
-[NSObject release] // 釋放對象川慌,減少一個引用
-[NSObject dealloc] // 廢棄對象
從上面的方法中可以看出吃嘿,生成對象和持有對象是兩個完全獨立的過程祠乃,很多一開始接觸代碼就用ARC的童鞋會把這兩個過程搞混淆,并且以為是一個整體兑燥。蘋果主要有兩大類方式來管理生成和持有對象亮瓷。第一種是所有童鞋都知道的以 alloc / new / copy / mutableCopy 等開頭的初始化方法名,這種是自己生成即持有降瞳。除了第一種以外的初始化方法嘱支,比如 +[NSArray array], 就是第二種,這種是別人生成挣饥,自己持有斗塘,有點類似于寄生,也是被大部分童鞋所忽略的亮靴。通過舉栗子來分析兩者的差別。
第一種生成即持有的栗子:
{
// 以 alloc 等字眼為開頭的初始化方法生成即持有
NSArray *obj = [[NSArray alloc] init];
// 釋放 obj
[obj release];
}
第二種生成不持有的栗子:
{
// newObj 不持有新生成的對象
NSArray *newObj = [NSArray array];
// 通過調(diào)用 retain 來持有非自己生成的對象
[newObj retain];
// 釋放對象于置。如果之前未調(diào)用retain茧吊,會導致程序崩潰
[newObj release];
}
第二種生成方式需要顯示地調(diào)用 -retain 才能真正持有對象,實際上 NSArray 的靜態(tài)初始化方法八毯,是調(diào)用了[[NSArray alloc] init]搓侄,但是為什么我們還要手動再調(diào)一次 -retain 呢?话速,蘋果在這里用了自動釋放池 @autoreleasepool 讶踪,簡單地說就是注冊到釋放池的對象會隨著釋放池生命周期結(jié)束而自動釋放(如果博主勤快的話,自動釋放池會在后續(xù)的章節(jié)里詳細闡述)泊交。
+ (instancetype)array {
// 這是一種通認的實現(xiàn)方式
NSArray *obj = [[[NSArray alloc] init] autorelease];
return obj;
/// 新生成的對象實際上是由自動釋放池釋放
/// 這邊引入釋放池可以保證自己生成對象由自己釋放
}
引用計數(shù)原理
蘋果管理引用計數(shù)的方法是通過哈希表來實現(xiàn)的乳讥,具體實現(xiàn)我們來看下蘋果的源碼 ,下面提取的源碼已進行過簡化整合廓俭,并刪除一些判斷條件以及表鎖云石。前面已經(jīng)提到,引用計數(shù)的操作是NSObject負責的研乒,我們可以在runtime/NSObject.mm類文件中找到相關(guān)源碼汹忠。
我們先看下 -[NSObject retain] 是如果實現(xiàn)增加引用計數(shù)的:
- (id)retain {
// 獲取對象哈希表
SideTable& table = SideTables()[this];
// 獲取對象當前的引用計數(shù)
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
// 如果當前引用計數(shù)未越界,增加引用計數(shù)
refcntStorage += SIDE_TABLE_RC_ONE;
return (id)this;
}
// 這一步是蘋果做的優(yōu)化
// 如果前面操作引用計數(shù)失敗雹熬,會進入sidetable_retain_slow函數(shù)
// sidetable_retain_slow 做的是前面類似的工作
return sidetable_retain_slow(table);
}
相對于 -[NSObject retain]宽菜,-[NSObject release] 會多一步判斷引用計數(shù)和釋放內(nèi)存塊的過程:
- (oneway void)release {
// 獲取對象哈希表
SideTable& table = SideTables()[this];
bool do_dealloc = false;
if (table.trylock()) {
// 獲取對象引用計數(shù)
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
// 引用計數(shù)為0后設(shè)置delloc標記
do_dealloc = true;
// 重置引用計數(shù)
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)) {
// 引用計數(shù)遞減
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
// release 傳進來的 performDealloc = true
if (do_dealloc && performDealloc) {
// 廢棄對象
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
return sidetable_release_slow(table, performDealloc);
}
另外竿报,分析下蘋果的源碼铅乡,就會發(fā)現(xiàn)蘋果對引用計數(shù)做的一些優(yōu)化:
- 使用哈希表管理引用計數(shù),可以通過鍵值對追溯到對象的內(nèi)存塊烈菌,在內(nèi)存泄露的時候很容易定位到問題的源頭;
- 哈希表是分段的隆判,也就是說犬庇,查找指定對象地址時,會先去查找對象的一個范圍區(qū)間侨嘀,再確定具體地址臭挽。類似于圖書館里的書都是分類整理的;
- 哈希表加入鎖,線程安全咬腕;
- 操作引用計數(shù)失敗欢峰,會有備案操作。