iOS管理對象內存的數據結構以及操做算法--SideTables、RefcountMap、weak_table_t

1、引用計數的概念

這一部分是寫給非iOS工程師的,便於你們瞭解引用計數、循環引用、弱引用的概念。若是已經瞭解相關概念能夠直接跳過第一部分。html

你們都知道想要佔用一塊內存很容易,我們 new 一個對象就完事兒了。可是何時回收?不回收天然是不成的,內存再大也不能徹底不回收利用。回收早了的話,真正用到的時候會出現 野指針 問題。回收晚了又浪費寶貴的內存資源。我們得拿出一套管理內存的方法才成。本文只討論iOS管理對象內存的 引用計數 法。數組

內存中每個對象都有一個屬於本身的引用計數器。當某個對象A被另外一個傢伙引用時,A的引用計數器就+1,若是再有一個傢伙引用到A,那麼A的引用計數就再+1。當其中某個傢伙再也不引用A了,A的引用計數會-1。直到A的引用計數減到了0,那麼就沒有人再須要它了,就是時候把它釋放掉了。數據結構

在引用計數中,每個對象負責維護對象全部引用的計數值。當一個新的引用指向對象時,引用計數器就遞增,當去掉一個引用時,引用計數就遞減。當引用計數到零時,該對象就將釋放佔有的資源。併發

採用上述機制看似就能夠知道對象在內存中應該什麼時候釋放了,可是還有一個 循環引用 的問題須要咱們解決。app

如今內存中有兩個對象,A和B。ide

A.x = B;
B.y = A;
  • 假如A是作視頻處理的,B是處理音頻的。
  • 如今A的引用計數是1(被B.y引用)。
  • 如今B的引用計數也是1(被A.x引用)。
  • 那麼當A處理完它的視頻工做之後,發現本身的引用計數是1不是0,他內心想"哦還有人須要我,我還不能被釋放。"
  • 當B處理完音頻操做之後他發現他的引用計數也是1,他內心也以爲"我還不能被釋放還有人須要我。"

這樣兩個對象互相循環引用着對方誰都不會被釋放就形成了內存泄露。爲了解決這個問題咱們來引入 弱引用 的概念。性能

弱引用指向要引用的對象,可是不會增長那個對象的引用計數。就像下面這個圖這樣。 虛線爲弱引用 (艾瑪我畫圖畫的真醜)this

 

A.x = B;
 __weak B.y = A;

這裏咱們讓B的y是一個弱引用,它還能夠指向A可是不增長A的引用計數。url

  • 因此A的引用計數是0,B的引用計數是1(被A.x引用)。
  • 當A處理完他的視頻操做之後,發現本身的引用計數是0了,ok他能夠釋放了。
  • 隨之A.x也被釋放了。( A.x是對象A內部的一個變量 )
  • A.x被釋放了之後B的引用計數就也變成0了。
  • 而後B處理完他的音頻操做之後也能夠釋放了。

循環引用的問題解決了。咱們不妨思考一下,這套方案還會不會有其它的問題?操作系統

思考中...

還有一個 野指針 的問題等待咱們解決。

  • 若是A先處理完他的視頻任務以後被釋放了。
  • 這時候B還在處理中。
  • 可是處理過程當中B須要訪問A (B.y)來獲取一些數據。
  • 因爲A已經被釋放了,因此再訪問的時候就形成了 野指針 錯誤。

所以咱們還須要一個機制,可讓A釋放以後,我再訪問全部指向A的指針( 好比B.y )的時候均可以友好的得知A已經不存在了,從而避免出錯。

咱們這裏假設用一個數組,把全部指向A的弱引用都存起來,而後當A被釋放的時候把數組內全部的若引用都設置成nil( 至關於其餘語言中的NULL )。這樣當B再訪問B.y的時候就會返回nil。經過判空的方式就能夠避免野指針錯誤了。固然提及來簡單,下面咱們來看看蘋果是如何實現的。

2、拋出問題

前面絮絮不休說了一大堆,其實真正如今才拋出本次討論的問題。

  • 一、如何實現的引用計數管理,控制加一減一和釋放?
  • 二、爲什麼維護的weak指針防止野指針錯誤?

3、數據結構分析( SideTables、RefcountMap、weak_table_t )

我們先來討論最頂層的 SideTables

爲了管理全部對象的引用計數和weak指針,蘋果建立了一個全局的SideTables,雖然名字後面有個"s"不過他實際上是一個全局的 Hash 表,裏面的內容裝的都是 SideTable 結構體而已。它使用對象的 內存地址當它的key 。管理引用計數和weak指針就靠它了。

由於對象引用計數相關操做應該是 原子性 的。否則若是多個線程同時去寫一個對象的引用計數,那就會形成數據錯亂,失去了內存管理的意義。同時又由於內存中對象的數量是 很是很是龐大 的須要很是頻繁的操做SideTables,因此 能對整個Hash表加鎖。蘋果採用了 分離鎖 技術。

分離鎖和分拆鎖的區別

下降鎖競爭的另外一種方法是下降線程請求鎖的頻率。分拆鎖 (lock splitting) 和分離鎖 (lock striping) 是達到此目的兩種方式。相互獨立的狀態變量,應該使用獨立的鎖進行保護。有時開發人員會錯誤地使用一個鎖保護全部的狀態變量。這些技術減少了鎖的粒度,實現了更好的可伸縮性。可是,這些鎖須要仔細地分配,以下降發生死鎖的危險。

若是一個鎖守護多個相互獨立的狀態變量,你可能可以經過分拆鎖,使每個鎖守護不一樣的變量,從而改進可伸縮性。經過這樣的改變,使每個鎖被請求的頻率都變小了。分拆鎖對於中等競爭強度的鎖,可以有效地把它們大部分轉化爲非競爭的鎖,使性能和可伸縮性都獲得提升。

分拆鎖有時候能夠被擴展,分紅若干加鎖塊的集合,而且它們歸屬於相互獨立的對象,這樣的狀況就是分離鎖。

由於是使用對象的內存地址當key因此Hash的分部也很平均。假設Hash表有n個元素,則能夠將Hash的衝突減小到n分之一,支持n路的併發寫操做。

SideTable

當咱們經過SideTables[key]來獲得SideTable的時候,SideTable的結構以下:

1,一把自旋鎖。 spinlock_t slock;

自旋鎖 比較適用於鎖使用者保持鎖時間比較短的狀況。正是因爲自旋鎖使用者通常保持鎖時間很是短,所以選擇自旋而不是睡眠是很是必要的,自旋鎖的效率遠高於互斥鎖。信號量和讀寫信號量適合於保持時間較長的狀況,它們會致使調用者睡眠,所以只能在進程上下文使用,而自旋鎖適合於保持時間很是短的狀況,它能夠在任何上下文使用。

它的做用是在操做引用技術的時候對SideTable加鎖,避免數據錯誤。

蘋果在對鎖的選擇上能夠說是精益求精。蘋果知道對於引用計數的操做實際上是很是快的。因此選擇了雖然不是那麼高級可是確實效率高的自旋鎖,我在這裏只能說"雙擊666,老鐵們! 沒毛病!"

2,引用計數器 RefcountMap refcnts;

對象具體的引用計數數量是記錄在這裏的。

這裏注意RefcountMap實際上是個C++的 Map 。爲何Hash之後還須要個Map?其實蘋果採用的是分塊化的方法。

舉個例子

假設如今內存中有16個對象。

0x0000、0x000一、0x00十、0x00十一、0x0100......

我們建立一個SideTables[8]來存放這16個對象,那麼查找的時候發生Hash衝突的機率就是八分之一。

假設SideTables[0x0000]和SideTables[0x1111]衝突,映射到相同的結果。

SideTables[0x0000] == SideTables[0x1111]  ==> 都指向同一個SideTable

蘋果把兩個對象的內存管理都放到裏同一個SideTable中。你在這個SideTable中須要再次調用 table.refcnts.find(0x0000 )或者 table.refcnts.find(0x1111) 來找到他們真正的引用計數。

這裏是一個分流。內存中對象的數量實在是太龐大了咱們經過第一個Hash表只是過濾了第一次,而後咱們還須要再經過這個Map才能精確的定位到咱們要找的對象的引用計數器。

引用計數器的存儲結構以下

引用計數器的數據類型是:

typedef __darwin_size_t        size_t;

再進一步看它的定義實際上是 unsigned long ,在32位和64位操做系統中,它分別佔用32和64個bit。

蘋果常用 bit mask 技術。這裏也不例外。拿32位系統爲例的話,能夠理解成有32個盒子排成一排橫着放在你面前。盒子裏能夠裝0或者1兩個數字。咱們規定最後邊的盒子是低位,左邊的盒子是高位。

  • (1UL<<0)的意思是將一個"1"放到最右側的盒子裏,而後將這個"1"向左移動0位(就是原地不動):0x0000 0000 0000 0000 0000 0000 0000 0001
  • (1UL<<1)的意思是將一個"1"放到最右側的盒子裏,而後將這個"1"向左移動1位:0x0000 0000 0000 0000 0000 0000 0000 0010

下面來分析引用計數器( 圖中右側 )的結構,從低位到高位。

  • (1UL<<0) WEAKLY_REFERENCED

    表示是否有弱引用指向這個對象,若是有的話(值爲1)在對象釋放的時候須要把全部指向它的弱引用都變成nil( 至關於其餘語言的NULL ),避免野指針錯誤。

  • (1UL<<1) DEALLOCATING

    表示對象是否正在被釋放。1正在釋放,0沒有。

  • REAL COUNT

    圖中REAL COUNT的部分纔是對象真正的引用計數存儲區。因此我們說的引用計數加一或者減一,其實是對整個unsigned long加四或者減四,由於真正的計數是從2^2位開始的。

  • (1UL<<(WORD_BITS-1)) SIDE_TABLE_RC_PINNED

    其中WORD_BITS在32位和64位系統的時候分別等於32和64。其實這一位沒啥具體意義,就是隨着對象的引用計數不斷變大。若是這一位都變成1了,就表示引用計數已經最大了不能再增長了。

3,維護weak指針的結構體 weak_table_t weak_table;

上面的RefcountMap refcnts;是一個一層結構,能夠經過key直接找到對應的value。而這裏是一個兩層結構。

第一層結構體中包含兩個元素。

第一個元素 weak_entry_t *weak_entries; 是一個數組,上面的RefcountMap是要經過find(key)來找到精確的元素的。weak_entries則是經過循環遍從來找到對應的entry。

(上面管理引用計數蘋果使用的是Map,這裏管理weak指針蘋果使用的是數組,有興趣的朋友能夠思考一下爲何蘋果會分別採用這兩種不一樣的結構)

第二個元素num_entries是用來維護保證數組始終有一個合適的size。好比數組中元素的數量超過3/4的時候將數組的大小乘以2。

第二層weak_entry_t的結構包含3個部分

  • 1,referent:
    被指對象的地址。前面循環遍歷查找的時候就是判斷目標地址是否和他相等。
  • 2,referrers
    可變數組,裏面保存着全部指向這個對象的弱引用的地址。當這個對象被釋放的時候,referrers裏的全部指針都會被設置成nil。
  • 3,inline_referrers
    只有4個元素的數組,默認狀況下用它來存儲弱引用的指針。當大於4個的時候使用referrers來存儲指針。

OK你們來看着圖看着僞代碼走一遍流程

1,alloc

這時候其實並不操做SideTable,具體能夠參考:

深刻淺出ARC(上)

Objc使用了相似散列表的結構來記錄引用計數。而且在初始化的時候設爲了一。

2,retain: NSObject.mm line:1402-1417

//一、經過對象內存地址,在SideTables找到對應的SideTable
SideTable& table = SideTables()[this];

//二、經過對象內存地址,在refcnts中取出引用計數
size_t& refcntStorage = table.refcnts[this];

//三、判斷PINNED位,不爲1則+4
if (! (refcntStorage & PINNED)) {
    refcntStorage += (1UL<<2);
}

3,release NSObject.mm line:1524-1551

table.lock();
引用計數 = table.refcnts.find(this);
if (引用計數 == table.refcnts.end()) {
    //標記對象爲正在釋放
    table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (引用計數 < SIDE_TABLE_DEALLOCATING) {
    //這裏頗有意思,當出現小余(1UL<<1) 的狀況的時候
    //就是前面引用計數位都是0,後面弱引用標記位WEAKLY_REFERENCED可能有弱引用1
    //或者沒弱引用0

    //爲了避免去影響WEAKLY_REFERENCED的狀態
    引用計數 |= SIDE_TABLE_DEALLOCATING;
} else if ( SIDE_TABLE_RC_PINNED位爲0) {
    引用計數 -= SIDE_TABLE_RC_ONE;
}
table.unlock();
若是作完上述操做後若是須要釋放對象,則調用dealloc

4,dealloc NSObject.mm line:1555-1571

dealloc操做也作了大量了邏輯判斷和其它處理,我們這裏拋開那些邏輯只討論下面部分 sidetable_clearDeallocating()

SideTable& table = SideTables()[this];
table.lock();
引用計數 = table.refcnts.find(this);
if (引用計數 != table.refcnts.end()) {
    if (引用計數中SIDE_TABLE_WEAKLY_REFERENCED標誌位爲1) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    //從refcnts中刪除引用計數
    table.refcnts.erase(it);
}
table.unlock();

weak_clear_no_lock() 是關鍵,它纔是在對象被銷燬的時候處理全部弱引用指針的方法。

weak_clear_no_lock objc-weak.mm line:461-504

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    //一、拿到被銷燬對象的指針
    objc_object *referent = (objc_object *)referent_id;

    //二、經過 指針 在weak_table中查找出對應的entry
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    //三、將全部的引用設置成nil
    weak_referrer_t *referrers;
    size_t count;

    if (entry->out_of_line()) {
        //3.一、若是弱引用超過4個則將referrers數組內的弱引用都置成nil。
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        //3.二、不超過4個則將inline_referrers數組內的弱引用都置成nil
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }

    //循環設置全部的引用爲nil
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }

    //四、從weak_table中移除entry
    weak_entry_remove(weak_table, entry);
}

講到這裏咱們就已經把SideTables的操做流程過一遍了,但願你們看的開心。

相關文章
相關標籤/搜索