前言
ARC
作為一個老生常談的話題,基本被網上的各種部落格說盡了。但是前段時間朋友通過某些手段對YYModel
進行了優化,提高了大概1/3左右的效率,在觀賞過他改進的原始碼之後我又重新看了一遍ARC
相關的實現原始碼,主要體現ARC
機制的幾個方法分別是retain
、release
以及dealloc
,主要與strong
和weak
兩者相關
ARC的記憶體管理
來看看一段ARC
環境下的程式碼
1 2 3 |
- (void)viewDidLoad { NSArray * titles = @[@"title1", @"title2"]; } |
在編譯期間,程式碼就會變成這樣:
1 2 3 4 5 6 |
- (void)viewDidLoad { NSArray * titles = @[@"title1", @"title2"]; [titles retain]; /// ....... [titles release]; } |
簡單來說就是ARC
在程式碼編譯階段,會自動在程式碼的上下文中成對插入retain
以及release
,保證引用計數能夠正確管理記憶體。如果物件不是強引用型別,那麼ARC
的處理也會進行相應的改變
下面會分別說明在這幾個與引用計數相關的方法呼叫中發生了什麼
retain
強引用有retain
、strong
以及__strong
三種修飾,預設情況下,所有的類物件會自動被標識為__strong
強引用物件,強引用物件會在上下文插入retain
以及release
呼叫,從runtime原始碼處可以下載到對應呼叫的原始碼。在retain
呼叫的過程中,總共涉及到了四次呼叫:
id _objc_rootRetain(id obj)
對傳入物件進行非空斷言,然後呼叫物件的rootRetain()
方法id objc_object::rootRetain()
斷言非GC
環境,如果物件是TaggedPointer
指標,不做處理。TaggedPointer
是蘋果推出的一套優化方案,具體可以參考深入瞭解Tagged Pointer一文id objc_object::sidetable_retain()
增加引用計數,具體往下看id objc_object::sidetable_retain_slow(SideTable& table)
增加引用計數,具體往下看
在上面的幾步中最重要的步驟就是最後兩部的增加引用計數,在NSObject.mm
中可以看到函式的實現。這裡筆者剔除了部分不相關的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL,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()) { // 獲取 引用計數的引用 size_t& refcntStorage = table.refcnts[this]; if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) { // 如果引用計數未越界,則引用計數增加 refcntStorage += SIDE_TABLE_RC_ONE; } table.unlock(); return (id)this; } return sidetable_retain_slow(table); } |
SideTable
這個類包含著一個自旋鎖slock
來防止操作時可能出現的多執行緒讀取問題、一個弱引用表weak_table
以及引用計數表refcnts
。另外還提供一個方法傳入物件地址來尋找對應的SideTable
物件RefcountMap
物件通過雜湊表的結構儲存了物件持有者的地址以及引用計數,這樣一來,即便物件對應的記憶體出現錯誤,例如Zombie
異常,也能定位到物件的地址資訊- 每次
retain
後以後引用計數的值實際上增加了(1 而不是我們所知的
1
,這是由於引用計數的後兩位分別被弱引用
以及析構狀態
兩個標識位佔領,而第一位用來表示計數是否越界。
由於引用計數可能存在越界情況(SIDE_TABLE_RC_PINNED
位的值為1),因此雜湊表refcnts
中應該儲存了多個引用計數,sidetable_retainCount()
函式也證明了這一點:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#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; } |
引用計數總是返回1 + 計數表總計
這個數值,這也是為什麼經常性的當物件被釋放後,我們獲取retainCount
的值總不能為0
。至於函式sidetable_retain_slow
的實現和sidetable_retain
幾乎一樣,就不再介紹了
release
release
呼叫有著跟retain
類似的四次呼叫,前兩次呼叫的作用一樣,因此這裡只放上引用計數減少的函式程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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 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
有兩個主要的判斷
- 如果引用計數為計數表中的最後一個,標記物件為
正在析構
狀態,然後執行完成後傳送SEL_dealloc
訊息釋放物件 - 即便計數表的值為零,
sidetable_retainCount
函式照樣會返回1
的值。這時計數小於巨集定義SIDE_TABLE_DEALLOCATING == 1
,就不進行減少計數的操作,直接標記物件正在析構
看到release
的程式碼就會發現在上面程式碼中巨集定義SIDE_TABLE_DEALLOCATING
體現出了蘋果這個心機婊
的用心之深。通常而言,即便引用計數只有8
位的佔用,在剔除了首位越界
標記以及後兩位後,其最大取值為2^5-1 == 31
位。通常來說,如果不是專案中block
不加限制的引用,是很難達到這麼多的引用量的。因此佔用了SIDE_TABLE_DEALLOCATING
位不僅減少了額外佔用的標記變數記憶體,還能以作為引用計數是否歸零的判斷
weak
最開始的時候沒打算講weak
這個修飾,不過因為dealloc
方法本身涉及到了弱引用物件置空的操作,以及retain
過程中的物件也跟weak
有關係的情況下,簡單的說說weak
的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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
共用一套引用計數設計,因此兩者的賦值操作都要設定計數表,只是weak
修飾的物件的引用計數物件會被設定SIDE_TABLE_WEAKLY_REFERENCED
位,並且不參與sidetable_retainCount
函式中的計數計算而已
1 2 3 4 5 6 7 8 9 10 |
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; } |
另一個弱引用設定方法,相比上一個方法去掉了自旋鎖加鎖操作
dealloc
dealloc
是重量級的方法之一,不過由於函式內部呼叫層次過多,這裡不多闡述。實現程式碼在objc-object.h
的798
行,可以自行到官網下載原始碼後研讀
__unsafe_unretained
其實寫了這麼多,終於把本文的主角給講出來了。在iOS5的時候,蘋果正式推出了ARC
機制,伴隨的是上面的weak
、strong
等新修飾符,當然還有一個不常用的__unsafe_unretained
- weak
修飾的物件在指向的記憶體被釋放後會被自動置為nil - strong
持有指向的物件,會讓引用計數+1 - __unsafe_unretained
不引用指向的物件。但在物件記憶體被釋放掉後,依舊指向記憶體地址,等同於assign
,但是隻能修飾物件
在機器上保證應用能保持在55
幀以上的速率會讓應用看起來如絲綢般順滑,但是稍有不慎,稍微降到50~55
之間都有很大的可能展現出卡頓的現象。這裡不談及影像渲染、資料大量處理等耳聞能詳的效能惡鬼,說說Model
所造成的損耗。
如前面所說的,在ARC
環境下,物件的預設修飾為strong
,這意味著這麼一段程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@protocol RegExpCheck @property (nonatomic, copy) NSString * regExp; - (BOOL)validRegExp; @end - (BOOL)valid: (NSArray> *)params { for (id item in params) { if (![item validRegExp]) { return NO; } } return YES; } |
把這段程式碼改為編譯期間插入retain
和release
方法後的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 |
- (BOOL)valid: (NSArray> *)params { for (id item in params) { [item retain]; if (![item validRegExp]) { [item release]; return NO; } [item release]; } return YES; } |
遍歷操作在專案中出現的概率絕對排的上前列,那麼上面這個方法在呼叫期間會呼叫params.count
次retain
和release
函式。通常來說,每一個物件的遍歷次數越多,這些函式呼叫的損耗就越大。如果換做__unsafe_unretained
修飾物件,那麼這部分的呼叫損耗就被節省下來,這也是筆者朋友改進的手段
尾話
首先要承認,相比起其他效能惡鬼改進的優化,使用__unsafe_unretained
帶來的收益幾乎微乎其微,因此筆者並不是很推薦用這種高成本低迴報的方式優化專案,起碼在效能惡鬼大頭解決之前不推薦,但是去學習記憶體管理底層的知識可以幫助我們站在更高的地方看待開發。最後送上朋友的輪子
上一篇:訊息機制
轉載請註明本文作者和地址