iOS內存管理佈局及管理方案-理論篇

蘋果設備備受歡迎的背後離不開iOS優秀的內存管理機制,那iOS的內存佈局及管理方案是怎樣的呢?我們一起研究下。

內存管理分爲五大塊

棧區(stack):線性結構,內存連續,系統自己管理內存,程序運行記錄,每個線程,也就是每個執行序列各有一個(看crash log最容易理解),都是編譯的時候能確定好的,還有一個特點就是這裏面的數據可以不用指針,也不會丟。

堆區(heap):鏈式結構,內存不連續,最靈活的內存區,用途多多,動態分配和釋放,編譯時不能提前確定,我們的Objective-C對象都是這麼來的,都存在這裏,通常堆中的對象都是以指針來訪問的,指針從線程棧中來,但不獨屬於某個線程,堆也是對複雜的運行時處理的基礎支持,還有就是ARC還是MRC、“誰分配誰釋放”說的都是堆上對象的管理。

靜態區(全局區)(bss):初始化數據,簡單理解就是有初始值的變量、常量。

常量區(data):未初始化數據,只聲明未給值的變量,運行前統統爲0,之所以單獨分出來,是出於性能的考慮,因爲這些東西都是0,沒必要放在程序包裏,也不用copy。

代碼區(text):最靜態的,就是隻讀的東西,存儲代碼。

iOS內存管理方案有三種

我們詳細看下每種方案的實現及存在的意義。

一.tagged pointer

沒有這種管理機制會引起內存浪費,爲什麼呢?我們來看下,假設我們要存儲一個NSNumber對象,其值是一個整數。正常情況下,如果這個整數只是一個NSInteger的普通變量,那麼它所佔用的內存是與CPU的位數有關,在32位CPU下佔4個字節,在64位CPU下是佔8個字節的。而指針類型的大小通常也是與CPU位數相關,一個指針所佔用的內存在32位CPU下爲4個字節,在64位CPU下也是8個字節。

所以一個普通的iOS程序,如果沒有Tagged Pointer對象,從32位機器遷移到64位機器中後,雖然邏輯沒有任何變化,但這種NSNumber、NSDate一類的對象所佔用的內存會翻倍。如下圖所示:

Alt
我們再來看看效率上的問題,爲了存儲和訪問一個NSNumber對象,我們需要在堆上爲其分配內存,另外還要維護它的引用計數,管理它的生命期。這些都給程序增加了額外的邏輯,造成運行效率上的損失。

爲了改進上面提到的內存佔用和效率問題,蘋果提出了Tagged Pointer對象。由於NSNumber、NSDate一類的變量本身的值需要佔用的內存大小常常不需要8個字節,拿整數來說,4個字節所能表示的有符號整數就可以達到20多億(注:2^31=2147483648,另外1位作爲符號位),對於絕大多數情況都是可以處理的。

所以我們可以將一個對象的指針拆成兩部分,一部分直接保存數據,另一部分作爲特殊標記,表示這是一個特別的指針,不指向任何一個地址。所以,引入了Tagged Pointer對象之後,64位CPU下NSNumber的內存圖變成了以下這樣:

Alt
當8字節可以承載用於表示的數值時,系統就會以Tagged Pointer的方式生成指針,如果8字節承載不了時,則又用以前的方式來生成普通的指針。以上是關於Tag Pointer的存儲細節。

Tagged Pointer的特點:

  1. 我們也可以在WWDC2013的《Session 404 Advanced in Objective-C》視頻中,看到蘋果對於Tagged Pointer特點的介紹:Tagged Pointer專門用來存儲小的對象,例如NSNumber和NSDate, 當然NSString小於60字節的也可以運用了該手段

  2. Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披着對象皮的普通變量而已,因爲他沒有isa指針。所以,它的內存並不存儲在堆中,也不需要malloc和free。

  3. 在內存讀取上有着3倍的效率,創建時比以前快106倍。

由此可見,蘋果引入Tagged Pointer,不但減少了64位機器下程序的內存佔用,還提高了運行效率。完美地解決了小內存對象在存儲和訪問效率上的問題。

二、Non-pointer iSA–非指針型iSA

在64位系統上只需要32位來儲存內存地址,而剩下的32位就可以用來做其他的內存管理

non_pointer iSA 的判斷條件:

1 : 包含swift代碼;

2:sdk版本低於10.11;

3:runtime讀取image時發現這個image包含__objc_rawi sa段;

4:開發者自己添加了OBJC_DISABLE_NONPOINTER_ISA=YES到環境變量中;

5:某些不能使用Non-pointer的類,GCD等;

6:父類關閉。

三、SideTables,RefcountMap,weak_table_t

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

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

下邊是SideTabel的定義:

SideTable
   struct SideTable {
     //鎖
     spinlock_t slock;
     //強引用相關
     RefcountMap refcnts;
     //弱引用相關
     weak_table_t weak_table;
     ...
     }

當我們通過SideTables[key]來得到SideTable的時候,SideTable的結構如下:

1、一把自旋鎖。spinlock_t slock;

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

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

蘋果在對鎖的選擇上可以說是精益求精。蘋果知道對於引用計數的操作其實是非常快的。所以選擇了雖然不是那麼高級但是確實效率高的自旋鎖

2、引用計數器 RefcountMap * refcnts;

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

這裏注意RefcountMap其實是個C++的Map。爲什麼Hash以後還需要個Map呢?因爲內存中對象的數量實在是太龐大了我們通過第一個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位(就是原地不動):0b0000 0000 0000 0000 0000 0000 0000 0001

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

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

(1UL<<0)???WEAKLY_REFERENCED

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

(1UL<<1)???DEALLOCATING

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

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

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

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

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

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

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

這個是因爲weak的顯著的特徵來決定的: 當weak對象被銷燬的時候,要把所有指向該對象的指針都設爲nil。

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

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

1、referent:被指對象的地址。前面循環遍歷查找的時候就是判斷目標地址是否和他相等。

2、referrers:可變數組,裏面保存着所有指向這個對象的弱引用的地址。當這個對象被釋放的時候,referrers裏的所有指針都會被設置成nil。

3、inline_referrers只有4個元素的數組,默認情況下用它來存儲弱引用的指針。當大於4個的時候使用referrers來存儲指針。

上面我們介紹了蘋果爲了更好的內存管理使用的三種不同的內存管理方案,在內部採用了不同的數據結構以達到更高效內存檢索。

參考鏈接: https://www.jianshu.com/p/dcbf48a733f9
http://www.cocoachina.com/articles/13449
http://www.cocoachina.com/articles/24119

參考書籍:Objective-C高級編程:iOS與OS X多線程和內存管理

歡迎點擊“京東雲”瞭解更多精彩內容

Alt

發佈了162 篇原創文章 · 獲贊 60 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章