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