runtime-閒聊記憶體管理

林欣達發表於2016-12-24

前言

ARC作為一個老生常談的話題,基本被網上的各種部落格說盡了。但是前段時間朋友通過某些手段對YYModel進行了優化,提高了大概1/3左右的效率,在觀賞過他改進的原始碼之後我又重新看了一遍ARC相關的實現原始碼,主要體現ARC機制的幾個方法分別是retainrelease以及dealloc,主要與strongweak兩者相關

ARC的記憶體管理

來看看一段ARC環境下的程式碼

在編譯期間,程式碼就會變成這樣:

簡單來說就是ARC在程式碼編譯階段,會自動在程式碼的上下文中成對插入retain以及release,保證引用計數能夠正確管理記憶體。如果物件不是強引用型別,那麼ARC的處理也會進行相應的改變

 11783864-c4c83afb5eca5471

下面會分別說明在這幾個與引用計數相關的方法呼叫中發生了什麼

retain

強引用有retainstrong以及__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中可以看到函式的實現。這裡筆者剔除了部分不相關的程式碼:

  • SideTable這個類包含著一個自旋鎖slock來防止操作時可能出現的多執行緒讀取問題、一個弱引用表weak_table以及引用計數表refcnts。另外還提供一個方法傳入物件地址來尋找對應的SideTable物件
  • RefcountMap物件通過雜湊表的結構儲存了物件持有者的地址以及引用計數,這樣一來,即便物件對應的記憶體出現錯誤,例如Zombie異常,也能定位到物件的地址資訊
  • 每次retain後以後引用計數的值實際上增加了(1 而不是我們所知的1,這是由於引用計數的後兩位分別被弱引用以及析構狀態兩個標識位佔領,而第一位用來表示計數是否越界。

由於引用計數可能存在越界情況(SIDE_TABLE_RC_PINNED位的值為1),因此雜湊表refcnts中應該儲存了多個引用計數,sidetable_retainCount()函式也證明了這一點:

引用計數總是返回1 + 計數表總計這個數值,這也是為什麼經常性的當物件被釋放後,我們獲取retainCount的值總不能為0。至於函式sidetable_retain_slow的實現和sidetable_retain幾乎一樣,就不再介紹了

release

release呼叫有著跟retain類似的四次呼叫,前兩次呼叫的作用一樣,因此這裡只放上引用計數減少的函式程式碼:

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的操作

weakstrong共用一套引用計數設計,因此兩者的賦值操作都要設定計數表,只是weak修飾的物件的引用計數物件會被設定SIDE_TABLE_WEAKLY_REFERENCED位,並且不參與sidetable_retainCount函式中的計數計算而已

另一個弱引用設定方法,相比上一個方法去掉了自旋鎖加鎖操作

dealloc

dealloc是重量級的方法之一,不過由於函式內部呼叫層次過多,這裡不多闡述。實現程式碼在objc-object.h798行,可以自行到官網下載原始碼後研讀

__unsafe_unretained

其實寫了這麼多,終於把本文的主角給講出來了。在iOS5的時候,蘋果正式推出了ARC機制,伴隨的是上面的weakstrong等新修飾符,當然還有一個不常用的__unsafe_unretained

  • weak
    修飾的物件在指向的記憶體被釋放後會被自動置為nil
  • strong
    持有指向的物件,會讓引用計數+1
  • __unsafe_unretained
    不引用指向的物件。但在物件記憶體被釋放掉後,依舊指向記憶體地址,等同於assign,但是隻能修飾物件

在機器上保證應用能保持在55幀以上的速率會讓應用看起來如絲綢般順滑,但是稍有不慎,稍微降到50~55之間都有很大的可能展現出卡頓的現象。這裡不談及影像渲染、資料大量處理等耳聞能詳的效能惡鬼,說說Model所造成的損耗。

如前面所說的,在ARC環境下,物件的預設修飾為strong,這意味著這麼一段程式碼:

把這段程式碼改為編譯期間插入retainrelease方法後的程式碼如下:

遍歷操作在專案中出現的概率絕對排的上前列,那麼上面這個方法在呼叫期間會呼叫params.countretainrelease函式。通常來說,每一個物件的遍歷次數越多,這些函式呼叫的損耗就越大。如果換做__unsafe_unretained修飾物件,那麼這部分的呼叫損耗就被節省下來,這也是筆者朋友改進的手段

尾話

首先要承認,相比起其他效能惡鬼改進的優化,使用__unsafe_unretained帶來的收益幾乎微乎其微,因此筆者並不是很推薦用這種高成本低迴報的方式優化專案,起碼在效能惡鬼大頭解決之前不推薦,但是去學習記憶體管理底層的知識可以幫助我們站在更高的地方看待開發。最後送上朋友的輪子

上一篇:訊息機制

轉載請註明本文作者和地址

相關文章