iOS記憶體管理佈局及管理方案-理論篇

京東科技開發者發表於2020-01-06
蘋果裝置備受歡迎的背後離不開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一類的物件所佔用的記憶體會翻倍。如下圖所示:

iOS記憶體管理佈局及管理方案-理論篇


我們再來看看效率上的問題,為了儲存和訪問一個NSNumber物件,我們需要在堆上為其分配記憶體,另外還要維護它的引用計數,管理它的生命期。這些都給程式增加了額外的邏輯,造成執行效率上的損失。

為了改進上面提到的記憶體佔用和效率問題,蘋果提出了Tagged Pointer物件。由於NSNumber、NSDate一類的變數本身的值需要佔用的記憶體大小常常不需要8個位元組,拿整數來說,4個位元組所能表示的有符號整數就可以達到20多億(注:2^31=2147483648,另外1位作為符號位),對於絕大多數情況都是可以處理的。
所以我們可以將一個物件的指標拆成兩部分,一部分直接儲存資料,另一部分作為特殊標記,表示這是一個特別的指標,不指向任何一個地址。所以,引入了Tagged Pointer物件之後,64位CPU下NSNumber的記憶體圖變成了以下這樣:

iOS記憶體管理佈局及管理方案-理論篇


當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的類,G/C/D等;
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來儲存指標。
上面我們介紹了蘋果為了更好的記憶體管理使用的三種不同的記憶體管理方案,在內部採用了不同的資料結構以達到更高效記憶體檢索。

參考連結:

參考書籍 Objective-C高階程式設計:iOS與OS X多執行緒和記憶體管理
歡迎點選“ 京東雲 ”瞭解更多精彩內容

iOS記憶體管理佈局及管理方案-理論篇


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912185/viewspace-2672061/,如需轉載,請註明出處,否則將追究法律責任。

相關文章