說說iOS與內(nèi)存管理(上)
譯文說起內(nèi)存管理,看似老生常談,而真正掌握內(nèi)存管理的核心其實(shí)并不簡單。ARC/MRR以及“誰分配誰就負(fù)責(zé)釋放”這種基本原則是很重要的,但不是本 文要討論的重點(diǎn)。之前本人還沒在小站發(fā)過相關(guān)的文章,本篇文章中,我本人是想結(jié)合實(shí)際開發(fā)和調(diào)試中遇到的一些細(xì)節(jié)問題,來談?wù)刬OS的內(nèi)存管理內(nèi)在機(jī)制和 調(diào)試方法。
上一篇文章已經(jīng)是4月份的了,時(shí)間飛快又過去了好久,小站5月份沒有文章更新,罪過罪過。最近小站的站長我又轉(zhuǎn)換到新團(tuán) 隊(duì)新崗位,在支付寶做客戶端開發(fā)感受頗多,不過身在一個(gè)技術(shù)流團(tuán)隊(duì),工作很有挑戰(zhàn),自己感覺很充實(shí)、很“幸福”。iOS開發(fā)當(dāng)中的內(nèi)存管理,可深可淺,一 般應(yīng)用程序開發(fā)過程當(dāng)中可能并不需要關(guān)注太多,如果不是來到支付寶,也許就不會有這么多心得來整理此文。
關(guān)于內(nèi)存,我準(zhǔn)備分為內(nèi)存管理的基本原則、原理和調(diào)試方法、實(shí)際問題幾部分整理。那么接下來我就和大家一起復(fù)習(xí)和稍微深入一下iOS的內(nèi)存管理的原理和原則。
0. 概述
內(nèi) 存,簡單來說就是內(nèi)部存儲,復(fù)雜來說要從馮·諾依曼計(jì)算機(jī)結(jié)構(gòu)說起。馮·諾依曼結(jié)構(gòu),也稱做普林斯頓結(jié)構(gòu),目前和哈佛結(jié)構(gòu)相對,指出了計(jì)算機(jī)由運(yùn)算器、控 制器、存儲器、輸入和輸出設(shè)備幾大部件組成。如今我們個(gè)人用的機(jī)器估計(jì)都是這個(gè)套路,而且運(yùn)算器和控制器都合在一起,就是CPU,中央處理器。那么內(nèi)存就 是CPU能直接讀寫訪問數(shù)據(jù)的地方(寄存器是在CPU內(nèi)的,不算哈),有些朋友說誰誰誰的iPhone內(nèi)存16G、64G,我只能說這個(gè)理解方法僅限于存 儲部件放在手機(jī)里(內(nèi))了,嚴(yán)格來講這算“外存”,我們要討論的不是這個(gè)。
馮·諾依曼結(jié)構(gòu)還說了,內(nèi)存是用來存啥的呢?指令+數(shù)據(jù)?。ü鸬目峙戮筒灰粯恿耍τ谖覀冮_發(fā)者來說,指令基本就是代碼邏輯,至于數(shù)據(jù)么變量常量肯定都算是的了。
內(nèi)存有多大?不大,現(xiàn)今主流的個(gè)人機(jī)器也就幾G的樣子。iPhone? 統(tǒng)統(tǒng)1G。
我們操作系統(tǒng)都是運(yùn)行在內(nèi)存之上的,1G好像不算大,所以為了支持多進(jìn)程,也為了支持大程序,抽象的虛擬存儲的概念誕生了。
簡要的概念先陳述到這,下面詳細(xì)說。哦,對了,ARC和MRR我還是得提一下,這個(gè)要是真不知道還真的自己先去了解一下去。
1. 通用內(nèi)存基本原理
說iOS的內(nèi)存,有必要先看看一般的計(jì)算機(jī)都是怎么干的,iPhone也是計(jì)算機(jī),通用的道理一樣要遵循。這里提兩方面:虛存的概念,內(nèi)存內(nèi)容的大致分布。
虛 擬存儲系統(tǒng)。剛剛提到了,物理內(nèi)存就那么大點(diǎn),但是還要跑多個(gè)程序,還要接受消耗很大內(nèi)存的程序,這怎么辦?涼拌。搞計(jì)算機(jī)的人都是很聰明的,在操作系統(tǒng) 層面做了物理地址和邏輯地址之間的映射轉(zhuǎn)換,當(dāng)然處理器硬件上也做了支持。一個(gè)程序在運(yùn)行時(shí),實(shí)際要用到的指令和數(shù)據(jù)都是很有限的,不可能從頭到尾同時(shí) 用。那么對于一個(gè)程序來說,假裝自己有非常大的空間,實(shí)際上只要有條理的把暫時(shí)要用到的部分放進(jìn)物理內(nèi)存供CPU訪問就好,這樣第二個(gè)問題解決了。那既然 每個(gè)程序(進(jìn)程)只用一小塊,那整個(gè)物理內(nèi)存就可以分給多個(gè)程序(進(jìn)程)用了,***個(gè)問題也迎刃而解。當(dāng)然,這樣做的前提是,數(shù)據(jù)和指令的動態(tài)進(jìn)出,用完 了的暫時(shí)不用的踢出內(nèi)存,需要用的及時(shí)加載進(jìn)來。這個(gè)具體的實(shí)現(xiàn)方式就多種多樣了,很多實(shí)現(xiàn)方式是在外存中開了個(gè)交換區(qū)供換入換出,但iOS可略有不同。
內(nèi) 存的大致分布。不久以前,我發(fā)了一篇文章整理了Mach-O文件的格式分析,里面很復(fù)雜地放了好多東西,包括我們Build打包時(shí)的代碼和數(shù)據(jù)。而 Mach-O文件正是我們開發(fā)內(nèi)容的一個(gè)靜態(tài)展現(xiàn)形式,要想在運(yùn)行的時(shí)候看樣子,就得看這文件里包含的東西是怎么放進(jìn)內(nèi)存的。Objective-C是基 于C的,不放看下C程序進(jìn)程的內(nèi)存分布:
一個(gè)運(yùn)行時(shí)進(jìn)程的典型內(nèi)存分布
最簡單來說分為兩大部分:指令+數(shù)據(jù)。再細(xì)分一點(diǎn),五部分:代碼(指令),初始化數(shù)據(jù)區(qū),未初始化數(shù)據(jù)區(qū),堆,棧。代碼(指令,text)就不用說了,最靜態(tài)的,就是只讀的東西;
初始化數(shù)據(jù),簡單理解就是有初始值的變量、常量;
未初始化數(shù)據(jù),只聲明未給值的變量,運(yùn)行前統(tǒng)統(tǒng)為0,之所以單獨(dú)分出來,估計(jì)是性能考慮,因?yàn)檫@些東西都是0,沒必要放在程序包里,也不用copy;
棧,程序運(yùn)行記錄,每個(gè)線程,也就是每個(gè)執(zhí)行序列各有一個(gè)(看crash log最容易理解),都是編譯的時(shí)候能確定好的,還有一個(gè)特點(diǎn)就是這里面的數(shù)據(jù)可以不用指針,也不會丟;
堆, 最靈活的內(nèi)存區(qū),用途多多,動態(tài)分配和釋放,編譯時(shí)不能提前確定,我們的Objective-C對象都是這么來的,都存在這里,通常堆中的對象都是以指針 來訪問的,指針從線程棧中來,但不獨(dú)屬于某個(gè)線程,堆也是對復(fù)雜的運(yùn)行時(shí)處理的基礎(chǔ)支持,還有就是ARC還是MRR、“誰分配誰釋放”說的都是堆上對象的 管理;
其實(shí),這個(gè)內(nèi)存中的布局方式大部分操作系統(tǒng)中的大部分進(jìn)程都是類似的。Objective-C的程序包對運(yùn)行時(shí)有著復(fù)雜的支持和內(nèi)容劃分,但也都是在這個(gè)大的框架下進(jìn)行的。
2. iOS的內(nèi)存管理
其實(shí),iOS的內(nèi)存管理和其它操作系統(tǒng)大同小異。這里按照蘋果文檔所述,重點(diǎn)對堆內(nèi)存分配整理下。
首先,iOS和其它系統(tǒng)一樣,內(nèi)存分頁,每頁4K。多個(gè)頁構(gòu)成一個(gè)region統(tǒng)一管理,負(fù)責(zé)管理的對象是VM object,其中包含了pager、size、resident pages等諸多屬性。
不管是Objective-C的[NSObject alloc],還是C代碼的對內(nèi)存分配,最終重任都會落到malloc庫上,釋放也是如此,最終都將使用malloc庫中的free()。
malloc 庫中有很多malloc的同族函數(shù)可以動態(tài)分配內(nèi)存,會結(jié)合參數(shù)在free pages中進(jìn)行最適分配。如果分配的內(nèi)存比較大,可以直接使用vm_allocate,得到一個(gè)VM對象(與Linux類似),這個(gè)在實(shí)際使用前不分配 物理內(nèi)存。malloc的內(nèi)部實(shí)現(xiàn)都是開源的,感興趣的可以去了解去看。
此外,對于malloc,還有一個(gè)Zone的概念(貌似與 Linux的概念不完全相同),可以簡單理解為一組free page單元,可以統(tǒng)一管理操作。默認(rèn)情況,在***次調(diào)用malloc時(shí),系統(tǒng)會生成一個(gè)default zone,后續(xù)的默認(rèn)分配在此進(jìn)行。比如,malloc_zone_xxx()函數(shù)都是對特定的zone進(jìn)行分配操作,執(zhí)行 zone->xxx()。
***強(qiáng)調(diào)一下iOS特別需要注意的點(diǎn):
當(dāng)前的主流iPhone實(shí)際物理內(nèi)存都不超過1G,可以說不算大。不過和Android機(jī)比起來,我不得不為蘋果的設(shè)計(jì)稱贊,1G空間利用得如此高效,性能不差,也控制了發(fā)熱。
那 么在這僅有的1G內(nèi)存中,iOS的操作系統(tǒng)更是拋棄了不必要的復(fù)雜——系統(tǒng)層面不支持App內(nèi)存頁換出。當(dāng)內(nèi)存吃緊時(shí),對于可以重新載入的只讀數(shù)據(jù)來說, 直接清理掉,而對于可寫的數(shù)據(jù),只能通過App自己去管理維護(hù)。內(nèi)存緊張時(shí),iOS會向App發(fā)起memory warning,不配合釋放足夠內(nèi)存者,殺!
App調(diào)試時(shí)的物理內(nèi)存情況
上 圖是使用Activity Monitor調(diào)試時(shí)的一個(gè)截圖,可以看到在盡量不釋放自身內(nèi)存的情況下(為了bug調(diào)試特意這么做的),支付寶錢包的內(nèi)存可以做到502M物理內(nèi)存占 用。再稍微高一點(diǎn)點(diǎn),系統(tǒng)就會連前臺運(yùn)行的App一起Kill掉。留下一個(gè)Unknown的log。
3. 其它
基本的原理就簡要整理到此,如下是一些參考:
Memory Layout of C Programs
Anatomyof a program in memory
What and where are the stack and heap?
Memory Usage Performance Guidelines
A look at how malloc works on the Mac