說起記憶體管理,看似老生常談,而真正掌握記憶體管理的核心其實並不簡單。ARC/MRR以及“誰分配誰就負責釋放”這種基本原則是很重要的,但不是本文要討論的重點。之前本人還沒在小站發過相關的文章,本篇文章中,我本人是想結合實際開發和除錯中遇到的一些細節問題,來談談iOS的記憶體管理內在機制和除錯方法。
上一篇文章已經是4月份的了,時間飛快又過去了好久,小站5月份沒有文章更新,罪過罪過。iOS開發當中的記憶體管理,可深可淺,一般應用程式開發過程當中可能並不需要關注太多,如果不是最近的除錯,也許就不會有這麼多心得來整理此文。
關於記憶體,我準備分為記憶體管理的基本原則、原理和除錯方法、實際問題幾部分整理。那麼接下來我就和大家一起復習和稍微深入一下iOS的記憶體管理的原理和原則。
0. 概述
記憶體,簡單來說就是內部儲存,複雜來說要從馮·諾依曼計算機結構說起。馮·諾依曼結構,也稱做普林斯頓結構,目前和哈佛結構相對,指出了計算機由運算器、控制器、儲存器、輸入和輸出裝置幾大部件組成。如今我們個人用的機器估計都是這個套路,而且運算器和控制器都合在一起,就是CPU,中央處理器。那麼記憶體就是CPU能直接讀寫訪問資料的地方(暫存器是在CPU內的,不算哈),有些朋友說誰誰誰的iPhone記憶體16G、64G,我只能說這個理解方法僅限於儲存部件放在手機裡(內)了,嚴格來講這算“外存”,我們要討論的不是這個。
馮·諾依曼結構還說了,記憶體是用來存啥的呢?指令+資料!(哈佛的恐怕就不一樣了)對於我們開發者來說,指令基本就是程式碼邏輯,至於資料麼變數常量肯定都算是的了。
記憶體有多大?不大,現今主流的個人機器也就幾G的樣子。iPhone? 統統1G。
我們作業系統都是執行在記憶體之上的,1G好像不算大,所以為了支援多程式,也為了支援大程式,抽象的虛擬儲存的概念誕生了。
簡要的概念先陳述到這,下面詳細說。哦,對了,ARC和MRR我還是得提一下,這個要是真不知道還真的自己先去了解一下去。
1. 通用記憶體基本原理
說iOS的記憶體,有必要先看看一般的計算機都是怎麼幹的,iPhone也是計算機,通用的道理一樣要遵循。這裡提兩方面:虛存的概念,記憶體內容的大致分佈。
虛擬儲存系統。剛剛提到了,實體記憶體就那麼大點,但是還要跑多個程式,還要接受消耗很大記憶體的程式,這怎麼辦?涼拌。搞計算機的人都是很聰明的,在作業系統層面做了實體地址和邏輯地址之間的對映轉換,當然處理器硬體上也做了支援。一個程式在執行時,實際要用到的指令和資料都是很有限的,不可能從頭到尾同時用。那麼對於一個程式來說,假裝自己有非常大的空間,實際上只要有條理的把暫時要用到的部分放進實體記憶體供CPU訪問就好,這樣第二個問題解決了。那既然每個程式(程式)只用一小塊,那整個實體記憶體就可以分給多個程式(程式)用了,第一個問題也迎刃而解。當然,這樣做的前提是,資料和指令的動態進出,用完了的暫時不用的踢出記憶體,需要用的及時載入進來。這個具體的實現方式就多種多樣了,很多實現方式是在外存中開了個交換區供換入換出,但iOS可略有不同。
記憶體的大致分佈。不久以前,我發了一篇文章整理了Mach-O檔案的格式分析,裡面很複雜地放了好多東西,包括我們Build打包時的程式碼和資料。而Mach-O檔案正是我們開發內容的一個靜態展現形式,要想在執行的時候看樣子,就得看這檔案裡包含的東西是怎麼放進記憶體的。Objective-C是基於C的,不放看下C程式程式的記憶體分佈:
一個執行時程式的典型記憶體分佈(iOS大同小異)
最簡單來說分為兩大部分:指令+資料。再細分一點,五部分:程式碼(指令),初始化資料區,未初始化資料區,堆,棧。
- 程式碼(指令,text)就不用說了,最靜態的,就是隻讀的東西;
- 初始化資料,簡單理解就是有初始值的變數、常量;
- 未初始化資料,只宣告未給值的變數,執行前統統為0,之所以單獨分出來,估計是效能考慮,因為這些東西都是0,沒必要放在程式包裡,也不用copy;
- 棧,程式執行記錄,每個執行緒,也就是每個執行序列各有一個(看crash log最容易理解),都是編譯的時候能確定好的,還有一個特點就是這裡面的資料可以不用指標,也不會丟;
- 堆,最靈活的記憶體區,用途多多,動態分配和釋放,編譯時不能提前確定,我們的Objective-C物件都是這麼來的,都存在這裡,通常堆中的物件都是以指標來訪問的,指標從執行緒棧中來,但不獨屬於某個執行緒,堆也是對複雜的執行時處理的基礎支援,還有就是ARC還是MRR、“誰分配誰釋放”說的都是堆上物件的管理;
其實,這個記憶體中的佈局方式大部分作業系統中的大部分程式都是類似的。Objective-C的程式包對執行時有著複雜的支援和內容劃分,但也都是在這個大的框架下進行的。
2. iOS的記憶體管理
其實,iOS的記憶體管理和其它作業系統大同小異。這裡按照蘋果文件所述,重點對堆記憶體分配整理下。
iOS的記憶體管理分為幾個層面,從系統到libmalloc,ARC環境下,編譯器也會幫助開發者做“力所能及”的優化處理。
首先,iOS和其它系統一樣,作業系統核心會做虛擬儲存到實體記憶體的對映管理,並做記憶體分頁,每頁4K。多個頁構成一個記憶體區塊統一管理,負責管理的物件是VM object,其中包含了pager、size、resident pages等諸多屬性。所有的記憶體分配最終都將交由系統來處理(比如vm_allocate/mach_vm_allocate)。
而開發中,在系統核心的基礎上,iOS使用libmalloc。不管是Objective-C的[NSObject alloc],還是C程式碼的對記憶體分配,重任都會落到malloc庫上,釋放也是如此,最終都將使用malloc庫中的free()。malloc庫中有很多malloc的同族函式可以動態分配記憶體。malloc庫中定義了zone的概念,並實現了不同的zone(如nano zone和scalable zone),並根據記憶體需求的大小使用不通演算法對nano、tiny、small、large量級的記憶體進行分配和釋放管理。預設情況,在第一次呼叫malloc時,系統會生成一個default zone,後續的預設分配在此進行。比如,malloc_zone_xxx()函式最終都對特定的zone進行分配操作,執行zone->xxx()。每個zone都以連結串列的形式對已分配過的記憶體做cache處理,避免頻繁對核心系統發起申請。malloc的內部實現都是開源的,感興趣的可以去了解去看。
最後強調一下iOS特別需要注意的點:
當前的主流iPhone實際實體記憶體都不超過1G,可以說不算大。不過和Android機比起來,我不得不為蘋果的設計稱讚,1G空間利用得如此高效,效能不差,也控制了發熱。
那麼在這僅有的1G記憶體中,iOS的作業系統更是拋棄了不必要的複雜——系統層面不支援App記憶體頁換出。當記憶體吃緊時,對於可以重新載入的只讀資料來說,直接清理掉,而對於可寫的資料,只能通過App自己去管理維護。記憶體緊張時,iOS會向App發起memory warning,不配合釋放足夠記憶體者,殺!
關於Instruments及記憶體除錯,會在後續文章詳細整理出來。
3. 其它
基本的原理就簡要整理到此,如下是一些參考:
What and where are the stack and heap?
Memory Usage Performance Guidelines