iOS管理對象內(nèi)存的數(shù)據(jù)結(jié)構(gòu)以及操作算法

第一次寫文章語言表達(dá)能力太差。如果有哪里表達(dá)的不夠清晰可以直接評論回復(fù)我,我來加以修改。這篇文章力求脫離語言的特性,咱們多講結(jié)構(gòu)和算法。即使你不懂iOS開發(fā),不懂Objective-C語言也可以看這篇文章。

通過閱讀本文你可以了解iOS管理對象內(nèi)存的數(shù)據(jù)結(jié)構(gòu)是什么樣的,以及操作邏輯。對象的reatin、release、dealloc操作是該通過怎樣的算法實現(xiàn)的。

本文所闡述的內(nèi)容代碼部分在蘋果的開源項目objc4-706中。

本文流程:

一、引用計數(shù)的概念

二、拋出問題

三、數(shù)據(jù)結(jié)構(gòu)分析( SideTables、RefcountMap、weak_table_t)

一、引用計數(shù)的概念

這一部分是寫給非iOS工程師的,便于大家了解引用計數(shù)、循環(huán)引用、弱引用的概念。如果已經(jīng)了解相關(guān)概念可以直接跳過第一部分。

大家都知道想要占用一塊內(nèi)存很容易,咱們 new 一個對象就完事兒了。但是什么時候回收?不回收自然是不成的,內(nèi)存再大也不能完全不回收利用。回收早了的話,真正用到的時候會出現(xiàn)野指針問題。回收晚了又浪費寶貴的內(nèi)存資源。咱們得拿出一套管理內(nèi)存的方法才成。本文只討論iOS管理對象內(nèi)存的引用計數(shù)法。

內(nèi)存中每一個對象都有一個屬于自己的引用計數(shù)器。當(dāng)某個對象A被另一個家伙引用時,A的引用計數(shù)器就+1,如果再有一個家伙引用到A,那么A的引用計數(shù)就再+1。當(dāng)其中某個家伙不再引用A了,A的引用計數(shù)會-1。直到A的引用計數(shù)減到了0,那么就沒有人再需要它了,就是時候把它釋放掉了。

在引用計數(shù)中,每一個對象負(fù)責(zé)維護(hù)對象所有引用的計數(shù)值。當(dāng)一個新的引用指向?qū)ο髸r,引用計數(shù)器就遞增,當(dāng)去掉一個引用時,引用計數(shù)就遞減。當(dāng)引用計數(shù)到零時,該對象就將釋放占有的資源。

采用上述機(jī)制看似就可以知道對象在內(nèi)存中應(yīng)該何時釋放了,但是還有一個循環(huán)引用的問題需要我們解決。

9F4E7963-0B4B-4153-A9FD-C3E9689B545B.png

現(xiàn)在內(nèi)存中有兩個對象,A和B。

A.x = B;

B.y = A;

假如A是做視頻處理的,B是處理音頻的。

現(xiàn)在A的引用計數(shù)是1(被B.y引用)。

現(xiàn)在B的引用計數(shù)也是1(被A.x引用)。

那么當(dāng)A處理完它的視頻工作以后,發(fā)現(xiàn)自己的引用計數(shù)是1不是0,他心里想"哦還有人需要我,我還不能被釋放。"

當(dāng)B處理完音頻操作以后他發(fā)現(xiàn)他的引用計數(shù)也是1,他心里也覺得"我還不能被釋放還有人需要我。"

這樣兩個對象互相循環(huán)引用著對方誰都不會被釋放就造成了內(nèi)存泄露。為了解決這個問題我們來引入弱引用的概念。

弱引用指向要引用的對象,但是不會增加那個對象的引用計數(shù)。就像下面這個圖這樣。虛線為弱引用 (艾瑪我畫圖畫的真丑)

EFDCA2C8-4E42-48EF-AE5F-3D4607B6CF68.png

A.x = B;

__weak B.y = A;

這里我們讓B的y是一個弱引用,它還可以指向A但是不增加A的引用計數(shù)。

所以A的引用計數(shù)是0,B的引用計數(shù)是1(被A.x引用)。

當(dāng)A處理完他的視頻操作以后,發(fā)現(xiàn)自己的引用計數(shù)是0了,ok他可以釋放了。

隨之A.x也被釋放了。(A.x是對象A內(nèi)部的一個變量)

A.x被釋放了以后B的引用計數(shù)就也變成0了。

然后B處理完他的音頻操作以后也可以釋放了。

循環(huán)引用的問題解決了。我們不妨思考一下,這套方案還會不會有其它的問題?

思考中...

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

如果A先處理完他的視頻任務(wù)之后被釋放了。

這時候B還在處理中。

但是處理過程中B需要訪問A (B.y)來獲取一些數(shù)據(jù)。

由于A已經(jīng)被釋放了,所以再訪問的時候就造成了野指針錯誤。

因此我們還需要一個機(jī)制,可以讓A釋放之后,我再訪問所有指向A的指針(比如B.y)的時候都可以友好的得知A已經(jīng)不存在了,從而避免出錯。

我們這里假設(shè)用一個數(shù)組,把所有指向A的弱引用都存起來,然后當(dāng)A被釋放的時候把數(shù)組內(nèi)所有的若引用都設(shè)置成nil(相當(dāng)于其他語言中的NULL)。這樣當(dāng)B再訪問B.y的時候就會返回nil。通過判空的方式就可以避免野指針錯誤了。當(dāng)然說起來簡單,下面我們來看看蘋果是如何實現(xiàn)的。

二、拋出問題

前面絮絮叨叨說了一大堆,其實真正現(xiàn)在才拋出本次討論的問題。

1、如何實現(xiàn)的引用計數(shù)管理,控制加一減一和釋放?

2、為何維護(hù)的weak指針防止野指針錯誤?

三、數(shù)據(jù)結(jié)構(gòu)分析( SideTables、RefcountMap、weak_table_t)

9BE315AE-E25E-41D1-99FD-883EDC5884F6.png

咱們先來討論最頂層的SideTables

EA251BCE-F990-4CA6-B66E-8822D8089D61.png

為了管理所有對象的引用計數(shù)和weak指針,蘋果創(chuàng)建了一個全局的SideTables,雖然名字后面有個"s"不過他其實是一個全局的Hash表,里面的內(nèi)容裝的都是SideTable結(jié)構(gòu)體而已。它使用對象的內(nèi)存地址當(dāng)它的key。管理引用計數(shù)和weak指針就靠它了。

因為對象引用計數(shù)相關(guān)操作應(yīng)該是原子性的。不然如果多個線程同時去寫一個對象的引用計數(shù),那就會造成數(shù)據(jù)錯亂,失去了內(nèi)存管理的意義。同時又因為內(nèi)存中對象的數(shù)量是非常非常龐大的需要非常頻繁的操作SideTables,所以不能對整個Hash表加鎖。蘋果采用了分離鎖技術(shù)。

分離鎖和分拆鎖的區(qū)別

降低鎖競爭的另一種方法是降低線程請求鎖的頻率。分拆鎖 (lock splitting) 和分離鎖 (lock striping) 是達(dá)到此目的兩種方式。相互獨立的狀態(tài)變量,應(yīng)該使用獨立的鎖進(jìn)行保護(hù)。有時開發(fā)人員會錯誤地使用一個鎖保護(hù)所有的狀態(tài)變量。這些技術(shù)減小了鎖的粒度,實現(xiàn)了更好的可伸縮性。但是,這些鎖需要仔細(xì)地分配,以降低發(fā)生死鎖的危險。

如果一個鎖守護(hù)多個相互獨立的狀態(tài)變量,你可能能夠通過分拆鎖,使每一個鎖守護(hù)不同的變量,從而改進(jìn)可伸縮性。通過這樣的改變,使每一個鎖被請求的頻率都變小了。分拆鎖對于中等競爭強(qiáng)度的鎖,能夠有效地把它們大部分轉(zhuǎn)化為非競爭的鎖,使性能和可伸縮性都得到提高。

分拆鎖有時候可以被擴(kuò)展,分成若干加鎖塊的集合,并且它們歸屬于相互獨立的對象,這樣的情況就是分離鎖。

因為是使用對象的內(nèi)存地址當(dāng)key所以Hash的分部也很平均。假設(shè)Hash表有n個元素,則可以將Hash的沖突減少到n分之一,支持n路的并發(fā)寫操作。

SideTable

當(dāng)我們通過SideTables[key]來得到SideTable的時候,SideTable的結(jié)構(gòu)如下:

1,一把自旋鎖。spinlock_t??slock;

自旋鎖比較適用于鎖使用者保持鎖時間比較短的情況。正是由于自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠(yuǎn)高于互斥鎖。信號量和讀寫信號量適合于保持時間較長的情況,它們會導(dǎo)致調(diào)用者睡眠,因此只能在進(jìn)程上下文使用,而自旋鎖適合于保持時間非常短的情況,它可以在任何上下文使用。

它的作用是在操作引用技術(shù)的時候?qū)ideTable加鎖,避免數(shù)據(jù)錯誤。

蘋果在對鎖的選擇上可以說是精益求精。蘋果知道對于引用計數(shù)的操作其實是非常快的。所以選擇了雖然不是那么高級但是確實效率高的自旋鎖,我在這里只能說"雙擊666,老鐵們! 沒毛病!"

2,引用計數(shù)器 RefcountMap??refcnts;

對象具體的引用計數(shù)數(shù)量是記錄在這里的。

這里注意RefcountMap其實是個C++的Map。為什么Hash以后還需要個Map?其實蘋果采用的是分塊化的方法。

舉個例子

假設(shè)現(xiàn)在內(nèi)存中有16個對象。

0x0000、0x0001、0x0010、0x0011、0x0100......

咱們創(chuàng)建一個SideTables[8]來存放這16個對象,那么查找的時候發(fā)生Hash沖突的概率就是八分之一。

假設(shè)SideTables[0x0000]和SideTables[0x1111]沖突,映射到相同的結(jié)果。

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

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

這里是一個分流。內(nèi)存中對象的數(shù)量實在是太龐大了我們通過第一個Hash表只是過濾了第一次,然后我們還需要再通過這個Map才能精確的定位到我們要找的對象的引用計數(shù)器。

引用計數(shù)器的存儲結(jié)構(gòu)如下

77490066-7101-4F70-BF50-604D3658F7C4.png

引用計數(shù)器的數(shù)據(jù)類型是:

typedef __darwin_size_t? ? ? ? size_t;

再進(jìn)一步看它的定義其實是unsigned long,在32位和64位操作系統(tǒng)中,它分別占用32和64個bit。

蘋果經(jīng)常使用bit mask技術(shù)。這里也不例外。拿32位系統(tǒng)為例的話,可以理解成有32個盒子排成一排橫著放在你面前。盒子里可以裝0或者1兩個數(shù)字。我們規(guī)定最后邊的盒子是低位,左邊的盒子是高位。

(1UL<<0)的意思是將一個"1"放到最右側(cè)的盒子里,然后將這個"1"向左移動0位(就是原地不動):0x0000 0000 0000 0000 0000 0000 0000 0001

(1UL<<1)的意思是將一個"1"放到最右側(cè)的盒子里,然后將這個"1"向左移動1位:0x0000 0000 0000 0000 0000 0000 0000 0010

下面來分析引用計數(shù)器(圖中右側(cè))的結(jié)構(gòu),從低位到高位。

(1UL<<0)????WEAKLY_REFERENCED

表示是否有弱引用指向這個對象,如果有的話(值為1)在對象釋放的時候需要把所有指向它的弱引用都變成nil(相當(dāng)于其他語言的NULL),避免野指針錯誤。

(1UL<<1)????DEALLOCATING

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

REAL COUNT

圖中REAL COUNT的部分才是對象真正的引用計數(shù)存儲區(qū)。所以咱們說的引用計數(shù)加一或者減一,實際上是對整個unsigned long加四或者減四,因為真正的計數(shù)是從2^2位開始的。

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

其中WORD_BITS在32位和64位系統(tǒng)的時候分別等于32和64。其實這一位沒啥具體意義,就是隨著對象的引用計數(shù)不斷變大。如果這一位都變成1了,就表示引用計數(shù)已經(jīng)最大了不能再增加了。

3,維護(hù)weak指針的結(jié)構(gòu)體 weak_table_t ??weak_table;

9BE315AE-E25E-41D1-99FD-883EDC5884F6.png

上面的RefcountMap??refcnts;是一個一層結(jié)構(gòu),可以通過key直接找到對應(yīng)的value。而這里是一個兩層結(jié)構(gòu)。

第一層結(jié)構(gòu)體中包含兩個元素。

第一個元素weak_entry_t *weak_entries;是一個數(shù)組,上面的RefcountMap是要通過find(key)來找到精確的元素的。weak_entries則是通過循環(huán)遍歷來找到對應(yīng)的entry。

(上面管理引用計數(shù)蘋果使用的是Map,這里管理weak指針蘋果使用的是數(shù)組,有興趣的朋友可以思考一下為什么蘋果會分別彩種這兩種不同的結(jié)構(gòu))

第二個元素num_entries是用來維護(hù)保證數(shù)組始終有一個合適的size。比如數(shù)組中元素的數(shù)量超過3/4的時候?qū)?shù)組的大小乘以2。

第二層weak_entry_t的結(jié)構(gòu)包含3個部分

1,referent:

被指對象的地址。前面循環(huán)遍歷查找的時候就是判斷目標(biāo)地址是否和他相等。

2,referrers

可變數(shù)組,里面保存著所有指向這個對象的弱引用的地址。當(dāng)這個對象被釋放的時候,referrers里的所有指針都會被設(shè)置成nil。

3,inline_referrers

只有4個元素的數(shù)組,默認(rèn)情況下用它來存儲弱引用的指針。當(dāng)大于4個的時候使用referrers來存儲指針。

OK大家來看著圖看著偽代碼走一遍流程

1,alloc

這時候其實并不操作SideTable,具體可以參考:

深入淺出ARC(上)

Objc使用了類似散列表的結(jié)構(gòu)來記錄引用計數(shù)。并且在初始化的時候設(shè)為了一。

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

//1、通過對象內(nèi)存地址,在SideTables找到對應(yīng)的SideTable

SideTable& table = SideTables()[this];

//2、通過對象內(nèi)存地址,在refcnts中取出引用計數(shù)

size_t& refcntStorage = table.refcnts[this];

//3、判斷PINNED位,不為1則+4

if (! (refcntStorage & PINNED)) {

refcntStorage += (1UL<<2);

}

3,release NSObject.mm line:1524-1551

table.lock();

引用計數(shù) = table.refcnts.find(this);

if (引用計數(shù) == table.refcnts.end()) {

//標(biāo)記對象為正在釋放

table.refcnts[this] = SIDE_TABLE_DEALLOCATING;

} else if (引用計數(shù) < SIDE_TABLE_DEALLOCATING) {

//這里很有意思,當(dāng)出現(xiàn)小余(1UL<<1) 的情況的時候

//就是前面引用計數(shù)位都是0,后面弱引用標(biāo)記位WEAKLY_REFERENCED可能有弱引用1

//或者沒弱引用0

//為了不去影響WEAKLY_REFERENCED的狀態(tài)

引用計數(shù) |= SIDE_TABLE_DEALLOCATING;

} else if ( SIDE_TABLE_RC_PINNED位為0) {

引用計數(shù) -= SIDE_TABLE_RC_ONE;

}

table.unlock();

如果做完上述操作后如果需要釋放對象,則調(diào)用dealloc

4,dealloc NSObject.mm line:1555-1571

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

SideTable& table = SideTables()[this];

table.lock();

引用計數(shù) = table.refcnts.find(this);

if (引用計數(shù) != table.refcnts.end()) {

if (引用計數(shù)中SIDE_TABLE_WEAKLY_REFERENCED標(biāo)志位為1) {

weak_clear_no_lock(&table.weak_table, (id)this);

}

//從refcnts中刪除引用計數(shù)

table.refcnts.erase(it);

}

table.unlock();

weak_clear_no_lock()是關(guān)鍵,它才是在對象被銷毀的時候處理所有弱引用指針的方法。

weak_clear_no_lock objc-weak.mm line:461-504

void

weak_clear_no_lock(weak_table_t *weak_table, id referent_id)

{

//1、拿到被銷毀對象的指針

objc_object *referent = (objc_object *)referent_id;

//2、通過 指針 在weak_table中查找出對應(yīng)的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;

}

//3、將所有的引用設(shè)置成nil

weak_referrer_t *referrers;

size_t count;

if (entry->out_of_line()) {

//3.1、如果弱引用超過4個則將referrers數(shù)組內(nèi)的弱引用都置成nil。

referrers = entry->referrers;

count = TABLE_SIZE(entry);

}

else {

//3.2、不超過4個則將inline_referrers數(shù)組內(nèi)的弱引用都置成nil

referrers = entry->inline_referrers;

count = WEAK_INLINE_COUNT;

}

//循環(huán)設(shè)置所有的引用為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();

}

}

}

//4、從weak_table中移除entry

weak_entry_remove(weak_table, entry);

}

講到這里我們就已經(jīng)把SideTables的操作流程過一遍了,希望大家看的開心。

歡迎加我的微博http://weibo.com/xuyang186

轉(zhuǎn)載請注明出處,謝謝。

參考文獻(xiàn)

iOS進(jìn)階——iOS(Objective-C)內(nèi)存管理·二

深入淺出ARC(上)

我們的對象會經(jīng)歷什么

Objective-C 引用計數(shù)原理

神經(jīng)病院Objective-C Runtime入院第一天——isa和Class

深入理解Tagged Pointer

Why is weak_table_t a member of SideTable in Objective-C runtime?

How can the Objective-C runtime know whether a weakly referenced object is still alive?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內(nèi)容