前言
ARC
作為一個(gè)老生常談的話題宰缤,基本被網(wǎng)上的各種博客說盡了琐鲁。但是前段時(shí)間朋友通過某些手段對YYModel
進(jìn)行了優(yōu)化菠齿,提高了大概1/3左右的效率,在觀賞過他改進(jìn)的源碼之后我又重新看了一遍ARC
相關(guān)的實(shí)現(xiàn)源碼熙宇,主要體現(xiàn)ARC
機(jī)制的幾個(gè)方法分別是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
,保證引用計(jì)數(shù)能夠正確管理內(nèi)存馆蠕。如果對象不是強(qiáng)引用類型圆裕,那么ARC
的處理也會進(jìn)行相應(yīng)的改變
下面會分別說明在這幾個(gè)與引用計(jì)數(shù)相關(guān)的方法調(diào)用中發(fā)生了什么
retain
強(qiáng)引用有retain
、strong
以及__strong
三種修飾荆几,默認(rèn)情況下吓妆,所有的類對象會自動被標(biāo)識為__strong
強(qiáng)引用對象,強(qiáng)引用對象會在上下文插入retain
以及release
調(diào)用吨铸,從runtime源碼處可以下載到對應(yīng)調(diào)用的源代碼行拢。在retain
調(diào)用的過程中,總共涉及到了四次調(diào)用:
-
id _objc_rootRetain(id obj)
對傳入對象進(jìn)行非空斷言诞吱,然后調(diào)用對象的rootRetain()
方法 -
id objc_object::rootRetain()
斷言非GC
環(huán)境舟奠,如果對象是TaggedPointer
指針竭缝,不做處理。TaggedPointer
是蘋果推出的一套優(yōu)化方案沼瘫,具體可以參考深入了解Tagged Pointer一文 -
id objc_object::sidetable_retain()
增加引用計(jì)數(shù)抬纸,具體往下看 -
id objc_object::sidetable_retain_slow(SideTable& table)
增加引用計(jì)數(shù),具體往下看
在上面的幾步中最重要的步驟就是最后兩部的增加引用計(jì)數(shù)耿戚,在NSObject.mm
中可以看到函數(shù)的實(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()) {
// 獲取 引用計(jì)數(shù)的引用
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
// 如果引用計(jì)數(shù)未越界,則引用計(jì)數(shù)增加
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
return sidetable_retain_slow(table);
}
SideTable
這個(gè)類包含著一個(gè)自旋鎖slock
來防止操作時(shí)可能出現(xiàn)的多線程讀取問題膜蛔、一個(gè)弱引用表weak_table
以及引用計(jì)數(shù)表refcnts
坛猪。另外還提供一個(gè)方法傳入對象地址來尋找對應(yīng)的SideTable
對象RefcountMap
對象通過散列表的結(jié)構(gòu)存儲了對象持有者的地址以及引用計(jì)數(shù),這樣一來皂股,即便對象對應(yīng)的內(nèi)存出現(xiàn)錯誤墅茉,例如Zombie
異常,也能定位到對象的地址信息每次
retain
后以后引用計(jì)數(shù)的值實(shí)際上增加了(1 << 2) == 4
而不是我們所知的1
呜呐,這是由于引用計(jì)數(shù)的后兩位分別被弱引用
以及析構(gòu)狀態(tài)
兩個(gè)標(biāo)識位占領(lǐng)就斤,而第一位用來表示計(jì)數(shù)是否越界。
由于引用計(jì)數(shù)可能存在越界情況(SIDE_TABLE_RC_PINNED
位的值為1)蘑辑,因此散列表refcnts
中應(yīng)該存儲了多個(gè)引用計(jì)數(shù)洋机,sidetable_retainCount()
函數(shù)也證明了這一點(diǎn):
#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;
}
引用計(jì)數(shù)總是返回1 + 計(jì)數(shù)表總計(jì)
這個(gè)數(shù)值,這也是為什么經(jīng)常性的當(dāng)對象被釋放后以躯,我們獲取retainCount
的值總不能為0
槐秧。至于函數(shù)sidetable_retain_slow
的實(shí)現(xiàn)和sidetable_retain
幾乎一樣啄踊,就不再介紹了
release
release
調(diào)用有著跟retain
類似的四次調(diào)用忧设,前兩次調(diào)用的作用一樣,因此這里只放上引用計(jì)數(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
有兩個(gè)主要的判斷
- 如果引用計(jì)數(shù)為計(jì)數(shù)表中的最后一個(gè)颠通,標(biāo)記對象為
正在析構(gòu)
狀態(tài)址晕,然后執(zhí)行完成后發(fā)送SEL_dealloc
消息釋放對象 - 即便計(jì)數(shù)表的值為零,
sidetable_retainCount
函數(shù)照樣會返回1
的值顿锰。這時(shí)計(jì)數(shù)小于宏定義SIDE_TABLE_DEALLOCATING == 1
谨垃,就不進(jìn)行減少計(jì)數(shù)的操作,直接標(biāo)記對象正在析構(gòu)
看到release
的代碼就會發(fā)現(xiàn)在上面代碼中宏定義SIDE_TABLE_DEALLOCATING
體現(xiàn)出了蘋果這個(gè)心機(jī)婊
的用心之深硼控。通常而言刘陶,即便引用計(jì)數(shù)只有8
位的占用,在剔除了首位越界
標(biāo)記以及后兩位后牢撼,其最大取值為2^5-1 == 31
位匙隔。通常來說,如果不是項(xiàng)目中block
不加限制的引用熏版,是很難達(dá)到這么多的引用量的纷责。因此占用了SIDE_TABLE_DEALLOCATING
位不僅減少了額外占用的標(biāo)記變量內(nèi)存捍掺,還能以作為引用計(jì)數(shù)是否歸零的判斷
weak
最開始的時(shí)候沒打算講weak
這個(gè)修飾,不過因?yàn)?code>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
共用一套引用計(jì)數(shù)設(shè)計(jì),因此兩者的賦值操作都要設(shè)置計(jì)數(shù)表喂柒,只是weak
修飾的對象的引用計(jì)數(shù)對象會被設(shè)置SIDE_TABLE_WEAKLY_REFERENCED
位不瓶,并且不參與sidetable_retainCount
函數(shù)中的計(jì)數(shù)計(jì)算而已
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;
}
另一個(gè)弱引用設(shè)置方法,相比上一個(gè)方法去掉了自旋鎖加鎖操作
dealloc
dealloc
是重量級的方法之一胳喷,不過由于函數(shù)內(nèi)部調(diào)用層次過多湃番,這里不多闡述。實(shí)現(xiàn)代碼在objc-object.h
的798
行吭露,可以自行到官網(wǎng)下載源碼后研讀
__unsafe_unretained
其實(shí)寫了這么多吠撮,終于把本文的主角給講出來了。在iOS5的時(shí)候讲竿,蘋果正式推出了ARC
機(jī)制泥兰,伴隨的是上面的weak
、strong
等新修飾符题禀,當(dāng)然還有一個(gè)不常用的__unsafe_unretained
- weak
修飾的對象在指向的內(nèi)存被釋放后會被自動置為nil - strong
持有指向的對象鞋诗,會讓引用計(jì)數(shù)+1 - __unsafe_unretained
不引用指向的對象。但在對象內(nèi)存被釋放掉后迈嘹,依舊指向內(nèi)存地址削彬,等同于assign
,但是只能修飾對象
在機(jī)器上保證應(yīng)用能保持在55
幀以上的速率會讓應(yīng)用看起來如絲綢般順滑秀仲,但是稍有不慎融痛,稍微降到50~55
之間都有很大的可能展現(xiàn)出卡頓的現(xiàn)象。這里不談及圖像渲染神僵、數(shù)據(jù)大量處理等耳聞能詳?shù)男阅軔汗硌闼ⅲf說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àng)目中出現(xiàn)的概率絕對排的上前列炮障,那么上面這個(gè)方法在調(diào)用期間會調(diào)用params.count
次retain
和release
函數(shù)目派。通常來說,每一個(gè)對象的遍歷次數(shù)越多胁赢,這些函數(shù)調(diào)用的損耗就越大企蹭。如果換做__unsafe_unretained
修飾對象,那么這部分的調(diào)用損耗就被節(jié)省下來,這也是筆者朋友改進(jìn)的手段
尾話
首先要承認(rèn)练对,相比起其他性能惡鬼改進(jìn)的優(yōu)化遍蟋,使用__unsafe_unretained
帶來的收益幾乎微乎其微,因此筆者并不是很推薦用這種高成本低回報(bào)的方式優(yōu)化項(xiàng)目螟凭,起碼在性能惡鬼大頭解決之前不推薦虚青,但是去學(xué)習(xí)內(nèi)存管理底層的知識可以幫助我們站在更高的地方看待開發(fā)。
ps:在朋友的堅(jiān)持下螺男,可恥的取消了代碼鏈接
上一篇:消息機(jī)制
下一篇:分類為什么不生成setter和getter
轉(zhuǎn)載請注明本文作者和地址