前言
ARC作為一個老生常談的話題,基本被網(wǎng)上的各種博客說盡了。但是前段時間朋友通過某些手段對YYModel進行了優(yōu)化,提高了大概1/3左右的效率,在觀賞過他改進的源碼之后我又重新看了一遍ARC相關(guān)的實現(xiàn)源碼,主要體現(xiàn)ARC機制的幾個方法分別是retain、release以及dealloc,主要與strong和weak兩者相關(guān)
ARC的內(nèi)存管理
來看看一段ARC環(huán)境下的代碼
- (void)viewDidLoad {
NSArray * titles = @[@"title1", @"title2"];
}
在編譯期間,代碼就會變成這樣:
- (void)viewDidLoad {
NSArray * titles = @[@"title1", @"title2"];
[titles retain];
/// .......
[titles release];
}
簡單來說就是ARC在代碼編譯階段,會自動在代碼的上下文中成對插入retain以及release,保證引用計數(shù)能夠正確管理內(nèi)存。如果對象不是強引用類型,那么ARC的處理也會進行相應(yīng)的改變

下面會分別說明在這幾個與引用計數(shù)相關(guān)的方法調(diào)用中發(fā)生了什么
retain
強引用有retain、strong以及__strong三種修飾,默認(rèn)情況下,所有的類對象會自動被標(biāo)識為__strong強引用對象,強引用對象會在上下文插入retain以及release調(diào)用,從runtime源碼處可以下載到對應(yīng)調(diào)用的源代碼。在retain調(diào)用的過程中,總共涉及到了四次調(diào)用:
-
id _objc_rootRetain(id obj)
對傳入對象進行非空斷言,然后調(diào)用對象的rootRetain()方法 -
id objc_object::rootRetain()
斷言非GC環(huán)境,如果對象是TaggedPointer指針,不做處理。TaggedPointer是蘋果推出的一套優(yōu)化方案,具體可以參考深入了解Tagged Pointer一文 -
id objc_object::sidetable_retain()
增加引用計數(shù),具體往下看 -
id objc_object::sidetable_retain_slow(SideTable& table)
增加引用計數(shù),具體往下看
在上面的幾步中最重要的步驟就是最后兩部的增加引用計數(shù),在NSObject.mm中可以看到函數(shù)的實現(xiàn)。這里筆者剔除了部分不相關(guān)的代碼:
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1)
#define SIDE_TABLE_RC_ONE (1UL<<2)
#define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1))
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
id objc_object::sidetable_retain()
{
// 獲取對象的table對象
SideTable& table = SideTables()[this];
if (table.trylock()) {
// 獲取 引用計數(shù)的引用
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
// 如果引用計數(shù)未越界,則引用計數(shù)增加
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
return sidetable_retain_slow(table);
}
SideTable這個類包含著一個自旋鎖slock來防止操作時可能出現(xiàn)的多線程讀取問題、一個弱引用表weak_table以及引用計數(shù)表refcnts。另外還提供一個方法傳入對象地址來尋找對應(yīng)的SideTable對象RefcountMap對象通過散列表的結(jié)構(gòu)存儲了對象持有者的地址以及引用計數(shù),這樣一來,即便對象對應(yīng)的內(nèi)存出現(xiàn)錯誤,例如Zombie異常,也能定位到對象的地址信息每次
retain后以后引用計數(shù)的值實際上增加了(1 << 2) == 4而不是我們所知的1,這是由于引用計數(shù)的后兩位分別被弱引用以及析構(gòu)狀態(tài)兩個標(biāo)識位占領(lǐng),而第一位用來表示計數(shù)是否越界。
由于引用計數(shù)可能存在越界情況(SIDE_TABLE_RC_PINNED位的值為1),因此散列表refcnts中應(yīng)該存儲了多個引用計數(shù),sidetable_retainCount()函數(shù)也證明了這一點:
#define SIDE_TABLE_RC_SHIFT 2
uintptr_t objc_object::sidetable_retainCount()
{
SideTable& table = SideTables()[this];
size_t refcnt_result = 1;
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}
引用計數(shù)總是返回1 + 計數(shù)表總計這個數(shù)值,這也是為什么經(jīng)常性的當(dāng)對象被釋放后,我們獲取retainCount的值總不能為0。至于函數(shù)sidetable_retain_slow的實現(xiàn)和sidetable_retain幾乎一樣,就不再介紹了
release
release調(diào)用有著跟retain類似的四次調(diào)用,前兩次調(diào)用的作用一樣,因此這里只放上引用計數(shù)減少的函數(shù)代碼:
uintptr_t objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
SideTable& table = SideTables()[this];
bool do_dealloc = false;
if (table.trylock()) {
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;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
return sidetable_release_slow(table, performDealloc);
}
在release中決定對象是否會被dealloc有兩個主要的判斷
- 如果引用計數(shù)為計數(shù)表中的最后一個,標(biāo)記對象為
正在析構(gòu)狀態(tài),然后執(zhí)行完成后發(fā)送SEL_dealloc消息釋放對象 - 即便計數(shù)表的值為零,
sidetable_retainCount函數(shù)照樣會返回1的值。這時計數(shù)小于宏定義SIDE_TABLE_DEALLOCATING == 1,就不進行減少計數(shù)的操作,直接標(biāo)記對象正在析構(gòu)
看到release的代碼就會發(fā)現(xiàn)在上面代碼中宏定義SIDE_TABLE_DEALLOCATING體現(xiàn)出了蘋果這個心機婊的用心之深。通常而言,即便引用計數(shù)只有8位的占用,在剔除了首位越界標(biāo)記以及后兩位后,其最大取值為2^5-1 == 31位。通常來說,如果不是項目中block不加限制的引用,是很難達(dá)到這么多的引用量的。因此占用了SIDE_TABLE_DEALLOCATING位不僅減少了額外占用的標(biāo)記變量內(nèi)存,還能以作為引用計數(shù)是否歸零的判斷
weak
最開始的時候沒打算講weak這個修飾,不過因為dealloc方法本身涉及到了弱引用對象置空的操作,以及retain過程中的對象也跟weak有關(guān)系的情況下,簡單的說說weak的操作
bool objc_object::sidetable_isWeaklyReferenced()
{
bool result = false;
SideTable& table = SideTables()[this];
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
result = it->second & SIDE_TABLE_WEAKLY_REFERENCED;
}
table.unlock();
return result;
}
weak和strong共用一套引用計數(shù)設(shè)計,因此兩者的賦值操作都要設(shè)置計數(shù)表,只是weak修飾的對象的引用計數(shù)對象會被設(shè)置SIDE_TABLE_WEAKLY_REFERENCED位,并且不參與sidetable_retainCount函數(shù)中的計數(shù)計算而已
void objc_object::sidetable_setWeaklyReferenced_nolock()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
SideTable& table = SideTables()[this];
table.refcnts[this] |= SIDE_TABLE_WEAKLY_REFERENCED;
}
另一個弱引用設(shè)置方法,相比上一個方法去掉了自旋鎖加鎖操作
dealloc
dealloc是重量級的方法之一,不過由于函數(shù)內(nèi)部調(diào)用層次過多,這里不多闡述。實現(xiàn)代碼在objc-object.h的798行,可以自行到官網(wǎng)下載源碼后研讀
__unsafe_unretained
其實寫了這么多,終于把本文的主角給講出來了。在iOS5的時候,蘋果正式推出了ARC機制,伴隨的是上面的weak、strong等新修飾符,當(dāng)然還有一個不常用的__unsafe_unretained
- weak
修飾的對象在指向的內(nèi)存被釋放后會被自動置為nil - strong
持有指向的對象,會讓引用計數(shù)+1 - __unsafe_unretained
不引用指向的對象。但在對象內(nèi)存被釋放掉后,依舊指向內(nèi)存地址,等同于assign,但是只能修飾對象
在機器上保證應(yīng)用能保持在55幀以上的速率會讓應(yīng)用看起來如絲綢般順滑,但是稍有不慎,稍微降到50~55之間都有很大的可能展現(xiàn)出卡頓的現(xiàn)象。這里不談及圖像渲染、數(shù)據(jù)大量處理等耳聞能詳?shù)男阅軔汗?,說說Model所造成的損耗。
如前面所說的,在ARC環(huán)境下,對象的默認(rèn)修飾為strong,這意味著這么一段代碼:
@protocol RegExpCheck
@property (nonatomic, copy) NSString * regExp;
- (BOOL)validRegExp;
@end
- (BOOL)valid: (NSArray<id<RegExpCheck>> *)params {
for (id<RegExpCheck> item in params) {
if (![item validRegExp]) { return NO; }
}
return YES;
}
把這段代碼改為編譯期間插入retain和release方法后的代碼如下:
- (BOOL)valid: (NSArray<id<RegExpCheck>> *)params {
for (id<RegExpCheck> item in params) {
[item retain];
if (![item validRegExp]) {
[item release];
return NO;
}
[item release];
}
return YES;
}
遍歷操作在項目中出現(xiàn)的概率絕對排的上前列,那么上面這個方法在調(diào)用期間會調(diào)用params.count次retain和release函數(shù)。通常來說,每一個對象的遍歷次數(shù)越多,這些函數(shù)調(diào)用的損耗就越大。如果換做__unsafe_unretained修飾對象,那么這部分的調(diào)用損耗就被節(jié)省下來,這也是筆者朋友改進的手段
尾話
首先要承認(rèn),相比起其他性能惡鬼改進的優(yōu)化,使用__unsafe_unretained帶來的收益幾乎微乎其微,因此筆者并不是很推薦用這種高成本低回報的方式優(yōu)化項目,起碼在性能惡鬼大頭解決之前不推薦,但是去學(xué)習(xí)內(nèi)存管理底層的知識可以幫助我們站在更高的地方看待開發(fā)。
ps:在朋友的堅持下,可恥的取消了代碼鏈接
上一篇:消息機制
下一篇:分類為什么不生成setter和getter
轉(zhuǎn)載請注明本文作者和地址