詳解iOS內存管理機制內部原理

iOS中內存管理機制是開發中一項很重要的知識,了解iOS中內存管理的規則不管是在開發中還是在學習中都能很大程度的幫助我們提升效率。下面我就根據自己的理解,詳細梳理一下內存管理相關的知識。

在說內存管理之前,我們首先要了解什么內存。一塊內存條,是一個從下至上地址依次遞增的結構,內存條中主要分為幾大類:棧區(stack)、堆區(heap)、常量區、代碼區(.text)、保留區。常量區分為未初始化區域(.bss)和已初始化區域(.data),棧區stack存儲順序是由高地址存向低地址,而堆區是由低地址向高地址存儲。內存條中地址由低到高的區域分別為:保留區,代碼區,已初始化區(.data),未初始化區(.bss),堆區(heap),棧區(stack),內核區。而程序員操作的主要是棧區與堆區還有常量區。

關于iOS內存管理的方案其實并非只有散列表一種,還有一種更為高效且為內存高效節省空間的方法叫做TaggedPointer,表明加標記的指針,我們可以理解為在是指針內部增加一些特殊的信息。

那么為什么要使用taggedPointer這種內存管理方法呢,其如何達到節省內存的目的呢。舉個例子,比如在OC中一個NSNumber對象,在32位中的系統中占用4個字節的空間,但是遷移至64位系統中后,其占用空間達到了8字節,以此類推,所有在64位系統中占用空間會翻倍的對象,在遷移后會導致系統內存劇增,即時他們根本用不到這么多的空間,所以蘋果對于一些小型數據,采用了taggedPointer這種方式管理內存。


其主要的原理就是在對象的指針中加入特定需要記錄的信息,以及對象所對應的值,在64位的系統中,一個指針所占用的內存空間為8個字節,已足以存下一些小型的數據量了,當對象指針的空間中存滿后,再對指針所指向的內存區域進行存儲,這就是taggedPointer。距離NSNumber,最低4位用于標記是什么類型的數據(long為3,float則為4,Int為2,double為5),而最高4位的“b”表示是NSNumber類型;其余56位則用來存儲數值本身內容。

之前runtime文章中有提到過objc_objcet對象中isa指針分為指針型isa與非指針型isa(NONPOINTER_ISA),運用的便是類似這種技術。下面詳細解讀一下NONPOINTER_ISA:

在一個64位的指針內存中,第0位存儲的是indexed標識符,它代表一個指針是否為NONPOINTER型,0代表不是,1代表是。第1位has_assoc,顧名思義,1代表其指向的實例變量含有關聯對象,0則為否。第2位為has_cxx_dtor,表明該對象是否包含C++相關的內容或者該對象是否使用ARC來管理內存,如果含有C++相關內容或者使用了ARC來管理對象,這一塊都表示為YES,第3-35位shiftcls存儲的就是這個指針的地址。第42位為weakly_referenced,表明該指針對象是否有弱引用的指針指向。第43位為deallocing,表明該對象是否正在被回收。第44位為has_sidetable_rc,顧名思義,該指針是否引用了sidetable散列表。第45-63位extra_rc裝的就是這個實例變量的引用計數,當對象被引用時,其引用計數+1,但少量的引用計數是不會直接存放在sideTables表中的,對象的引用計數會先存在NONPOINTER_ISA的指針中的45-63位,當其被存滿后,才會相應存入sideTables散列表中。

所以綜上所述,iOS中內存管理的方式主要有三大,1.taggedPointer,2.NONPOINTER_ISA,3.散列表。

下面再進行散列表的分析:

散列表在系統中的提現是一個sideTables的哈希映射表,其中所有對象的引用計數(除上述存在NONPOINTER_ISA中的外)都存在這個sideTables散列表中,而一個散列表中又包含眾多sideTable。每個SideTable中又包含了三個元素,spinlock_t自旋鎖,RefcountMap引用計數表,weak_table_t弱引用表。所以既然SideTables是一個哈希映射的表,為什么不用SideTables直接包含自旋鎖,引用技術表和弱引用表呢?因為在眾多線程同時訪問這個SideTables表的時候,為了保證數據安全,需要給其加上自旋鎖,如果只有一張SideTable的表,那么所有數據訪問都會出一個進一個,單線程進行,非常影響效率,而且會帶來不好的用戶體驗,針對這種情況,將一張SideTables分為多張表的SideTable,再各自加鎖保證數據的安全,這樣就增加了并發量,提高了數據訪問的效率,所以這就是一張SideTables表下涵蓋眾多SideTable表的原因。

基于此,我們進行SideTable的表分析,那么當一個對象的引用計數增加或減少時,需要去查找對應的SideTable并進行引用計數或者弱引用計數的操作時,系統又是怎樣實現的呢。

當一個對象訪問SideTables時,首先會取到對象的地址,將地址進行哈希運算,與SideTables的個數取余,最后得到的結果就是該對象所要訪問的SideTable所在SideTables中的位置,隨后在取到的SideTable中的RefcountMap表中再次進行一次哈希查找,找到該對象在引用計數表中所對應的位置,如果該位置存在對應的引用計數,則對其進行操作,如果沒有對應的引用計數,則創建一個對應的size_t對象,其實就是一個uint類型的無符號整型。

對于Spinlock_t自旋鎖,其本質是一種“忙等”的鎖,所謂“忙等”就是當一條線程被加上Spinlock自旋鎖后,當線程執行時,會不斷的去獲取這個鎖的信息,一旦獲取到這個鎖,便進行線程的執行。這對于一般的高性能鎖比如信號量不同,信號量是當線程獲取到信號量小于等0時,便自動進行休眠,當信號量發出時,對線程進行喚醒操作,這樣就致使了兩種鎖的性質不同。Spinlock自旋鎖只適用于一些小型數據操作,耗時很少的線程操作。

對于每張SideTable表中的弱引用表weak_table_t,其也是一張哈希表的結構,其內部包含了每個對象對應的弱引用表weak_entry_t,而weak_entry_t是一個結構體數組,其中包含的則是每一個對象弱引用的對象所對應的弱引用指針。

以上大概就是內存在ios中的管理方式,以及關于內存管理相關的一些數據類型。

關于iOS的兩種管理方式:MRC與ARC

MRC是上古時期iOS開發程序員用的一種手動管理對象引用計數的方式,但這也是內存管理的立足之本,ARC就是現代程序員常用的對象引用計數管理方式,ARC是由編譯器和runtime協作,共同完成對對象引用計數的控制,而不需要程序員自己手動控制。在MRC中可以調用alloc,retain,release,retainCount,dealloc等方法,這些方法在ARC中只能調用alloc方法,調用其他的會引起編譯報錯,不過在ARC模式中可以重寫dealloc方法。相比起MRC,在ARC中新增了weak和strong等屬性關鍵字。下面詳細解讀一下MRC ARC中的各個方法。

alloc:這個方法實質上是經過了一系列封裝調用之后的calloc,需要注意的是調用該方法時,對象的引用計數并沒有+1.

retain:這個方法是先在SideTables中通過哈希查找找到對象所在的那張SideTable表,隨后在SideTable中的引用計數表中再次通過哈希查找找到對象所對應的size_t,再加上一個系統的(引用計數+1宏)。為什么這里沒有+1而是加上一個系統的宏呢,因為在size_t結構中,前兩位不是儲存引用計數的,第一位存儲的是是否有弱引用指針指向,第二位存儲的是對象是否在被回收中。所以,在增加其引用計數時需要右移兩位再進行增加,所以用到了這個系統的宏SIDE_TABLE_RC_ONE。

release:這個方法跟retain方法原理一樣,只不過是減一個系統的宏SIDE_TABLE_RC_ONE

retainCount:這個方法的實現同樣是先查找系統的SideTables表,并找到對象對應的SideTable表,但在之前要先申明一個size_t為1的對象,隨后在對應的引用計數表中找到了對象對應的引用計數后,通過右移找到的count對象,與之前創建好的1相加,最后返回其結果便是引用計數。所以這就是為什么系統在調用alloc方法后并沒有給對象的引用計數+1,但retainCount方法調用后對象的引用計數就是1的原因。

dealloc:對象在被回收時,就會調用dealloc方法,其內部實現流程首先要調用一個_objc_rootDealloc()方法,再方法內部再調用一個rootDealloc()方法,此時在rootDealloc中會判斷該對象的isa指針,依次判斷指針內的內容:nonpointer_isa,weakly_referenced,has_assoc,has_cxx_dtor,has_sidetable_rc,如果判斷結果為:該isa指針不是非指針型的isa指針,沒有弱引用的指針指向,沒有相應的關聯對象,沒有c++相關的內容,沒有使用ARC模式,沒有關聯到散列表中,即判斷的內容都為否,則可以直接調用c語言中的free()函數進行相應的內存釋放,否則就會調用objc_dispose()這個函數。

而objc_dispose()函數的內部是經由objc_destructInstence()函數調用,隨后調用c函數的free()的,顧名思義,objc_destructInstence()函數就是一個銷毀對象的函數,那么objc_destructInstence()函數內部實現是什么樣的結構呢?

首先,destructInstence()函數內部會來判斷該對象是否有C++相關內容以及ARC相關的內容,如果有的話就會調用object_cxxDestruct函數來銷毀相應的內容,隨后會判斷改對象是否有關聯對象相關的內容,如果有的話就會調用_object_remove_associations()這個方法來清楚相關的關聯對象內容,在這兩個判斷步驟完成之后,調用clearDeallocating()方法。

在clearDeallocating()方法中,會調用一個sidetable_clearDeallocating()的方法,在方法內部會調用兩個方法,1.weak_clear_no_lock()這個方法會將每個弱引用表中的指向該對象的弱引用指針,置為nil。2.table.refcnts.erase()方法,這個方法會從引用計數表中,擦除該對象的引用計數。最后再調用c函數的free()方法,完成一次對象的回收。

所以總結一下dealloc方法的內容大致就是:1.先調用_objc_rootDealloc()方法——>2.在方法內部調用rootDealloc()方法——>3.依次判斷5個要素:是否非指著型isa,是否含有關聯對象相關內容,是否含有弱引用指針的指向,是否含有c++相關內容以及在ARC模式下使用,是否使用了散列表,如果判斷都為否,則直接調用C函數的free()釋放對象空間,反之則調用object_dispose()方法。

在object_dispose()方法中的調用流程為:1.調用objc_destructInsetence()方法——>2.調用C函數的free()釋放對象空間

在objc_destructInstence()方法內部實現原理為:1.判斷是否含有C++相關內容以及使用了ARC模式——>2.調用objcet_cxxDestruct()函數進行清除相關內容——>3.判斷是否含有關聯對象相關內容——>4.調用_object_remove_associations()方法清除關聯對象相關內容——>5.調用clearDeallocating()方法。

在clearDeallocating()方法內部,調用了一個sidetable_clearDeallocating()方法,旨在清除散列表相關的數據信息。

在sidetable_clearDeallocating()方法內部,進行了兩個步驟:1.調用weak_clear_no_lock()方法的調用,旨在將對象對應的弱引用表中對應的弱引用結構體數組中的指針全部置為nil——>2.調用table.refcnts.erase(),將對象對應的引用計數表中的引用計數擦除。

完成之后回到之前的步驟,調用free()函數,完成對一個對象的內存回收。

其實通過以上對一些內存管理數據結構的解釋以及對象回收的一個完整過程,相應的弱引用創建的的過程以及回收的過程自然也一目了然了。

當我們創建一個弱引用變量weakPointer的時候在編譯器中可以這么寫

id __weak weakPointer = object;

這行代碼實際上在系統內部實現的時候轉化為了兩行代碼:

id weakPointer;

objc_initWeak(&weakPointer,object);

首先定義了一個變量weakPointer,其次調用objc_initWeak方法來給weakPointer的這個弱引用指針來賦值。

內部原理與上面相似,當弱引用指針指向這個objcet變量時,首先去SideTables散列表中通過哈希查找,來找到object這個對象的SideTable表,再通過一次哈希查找,利用object對象的地址,在SideTable中的弱引用表中找到其對應的弱引用結構體數組,如果這個數組存在則在里面添加一個之前weakPointer的地址作為弱引用指針指向object,如果沒有這個結構體數組,則創建一個數組,將這個指針添加到第0個元素,同時給第2,3,4個元素設置為nil。這樣就完成了一個弱引用指針的定義實現過程了。

關于如何清除弱引用指針的,Dealloc方法調用過程已經說的很明白了,過程也與上面一行說的類似,就是在最后調用sidetable_clearDeallocating()方法中將對象對應的弱引用列表找到,將所有弱引用指針置為nil的時候就把相應的弱引用指針擦除了,這樣說就一目了然了。

希望通過這篇文章也可以幫助到同樣在學習iOS內存管理的你!




本文章由作者原創,未經允許禁止轉載

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

推薦閱讀更多精彩內容